View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.junit;
16  
17  import static java.nio.charset.StandardCharsets.UTF_8;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.lang.reflect.Method;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.HashSet;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Set;
30  
31  import org.apache.commons.io.FileUtils;
32  import org.apache.commons.text.StringEscapeUtils;
33  import org.htmlunit.BrowserVersion;
34  import org.htmlunit.CodeStyleTest;
35  import org.htmlunit.WebDriverTestCase;
36  import org.htmlunit.general.HostExtractor;
37  import org.htmlunit.junit.annotation.HtmlUnitNYI;
38  import org.junit.jupiter.api.extension.ExtensionContext;
39  import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
40  import org.opentest4j.AssertionFailedError;
41  
42  /**
43   * This is meant to automatically correct the test case to put either the real browser expectations,
44   * or the {@link HtmlUnitNYI} annotation for HtmlUnit.
45   *
46   * @author Ahmed Ashour
47   * @author Ronald Brill
48   */
49  public final class TestCaseCorrector implements TestExecutionExceptionHandler {
50  
51      /**
52       * {@inheritDoc}
53       */
54      @Override
55      public void handleTestExecutionException(final ExtensionContext context,
56                      final Throwable throwable) throws Throwable {
57  
58          final Object testInstance = context.getRequiredTestInstance();
59  
60          if (Boolean.parseBoolean(System.getProperty(WebDriverTestCase.AUTOFIX_))
61                  && testInstance instanceof WebDriverTestCase) {
62              final WebDriverTestCase webDriverTestCase = (WebDriverTestCase) testInstance;
63              final Method testMethod = context.getRequiredTestMethod();
64              final boolean realBrowser = webDriverTestCase.useRealBrowser();
65              final BrowserVersion browserVersion = webDriverTestCase.getBrowserVersion();
66  
67              // TODO @HtmlUnitNYI !realBrowser
68              correct(testInstance, testMethod, realBrowser, browserVersion, false, throwable);
69          }
70  
71          throw throwable;
72      }
73  
74      static void correct(final Object testInstance, final Method method,
75                      final boolean realBrowser, final BrowserVersion browserVersion,
76                      final boolean notYetImplemented, final Throwable t) throws IOException {
77  
78  
79          final String testRoot = "src/test/java/";
80          String browserString = browserVersion.getNickname().toUpperCase(Locale.ROOT);
81          browserString = browserString.replace('-', '_'); // FF-ESR-> FF_ESR
82  
83          final File file = new File(testRoot + method.getDeclaringClass().getName().replace('.', '/') + ".java");
84          final List<String> lines = FileUtils.readLines(file, UTF_8);
85          final String methodLine = "    public void " + method.getName() + "()";
86          if (realBrowser) {
87              String defaultExpectation = null;
88              for (int i = 0; i < lines.size(); i++) {
89                  if ("    @Default".equals(lines.get(i))) {
90                      defaultExpectation = getDefaultExpectation(lines, i);
91                  }
92                  if (lines.get(i).startsWith(methodLine)) {
93                      i = addExpectation(lines, i, browserString, (AssertionFailedError) t);
94                      break;
95                  }
96                  if (i == lines.size() - 2) {
97                      addMethodWithExpectation(lines, i, browserString, method.getName(), (AssertionFailedError) t,
98                              defaultExpectation);
99                      break;
100                 }
101             }
102         }
103         else if (!notYetImplemented) {
104             String defaultExpectation = null;
105             for (int i = 0; i < lines.size(); i++) {
106                 if ("    @Default".equals(lines.get(i))) {
107                     defaultExpectation = getDefaultExpectation(lines, i);
108                 }
109                 if (lines.get(i).startsWith(methodLine)) {
110                     addNotYetImplemented(lines, i, browserString);
111                     break;
112                 }
113                 if (i == lines.size() - 2) {
114                     addNotYetImplementedMethod(lines, i, browserString, method.getName(), defaultExpectation);
115                     break;
116                 }
117             }
118         }
119         else {
120             for (int i = 0; i < lines.size(); i++) {
121                 if (lines.get(i).startsWith(methodLine)) {
122                     removeNotYetImplemented(lines, i, browserString);
123                     break;
124                 }
125             }
126         }
127         FileUtils.writeLines(file, UTF_8.name(), lines);
128     }
129 
130     private static String getDefaultExpectation(final List<String> lines, final int defaultIndex) {
131         int index = defaultIndex;
132         while (index >= 0 && !lines.get(index).contains("Alerts")) {
133             index--;
134         }
135         if (index >= 0) {
136             final String line = lines.get(index);
137             return line.substring(line.indexOf('"') + 1, line.lastIndexOf('"'));
138         }
139         return null;
140     }
141 
142     private static int addExpectation(final List<String> lines, int i,
143             final String browserString, final AssertionFailedError comparisonFailure) {
144         while (!lines.get(i).startsWith("    @Alerts")) {
145             i--;
146         }
147         final List<String> alerts = CodeStyleTest.alertsToList(lines, i);
148         for (final Iterator<String> it = alerts.iterator(); it.hasNext();) {
149             if (it.next().startsWith(browserString + " = ")) {
150                 it.remove();
151             }
152         }
153         alerts.add(browserString + " = " + getActualString(comparisonFailure));
154         lines.remove(i);
155         while (lines.get(i).startsWith("        ")) {
156             lines.remove(i);
157         }
158 
159         Collections.sort(alerts);
160         String defaultAlert = null;
161         for (final String alert : alerts) {
162             if (alert.startsWith("DEFAULT = ")) {
163                 defaultAlert = alert;
164                 break;
165             }
166         }
167 
168         if (defaultAlert != null) {
169             alerts.remove(defaultAlert);
170             alerts.add(0, defaultAlert);
171         }
172 
173         for (int x = 0; x < alerts.size(); x++) {
174             String line = alerts.get(x);
175             if (x == 0) {
176                 if (!line.contains(" = ")) {
177                     line = "DEFAULT = " + line;
178                 }
179                 line = "    @Alerts(" + line;
180             }
181             else {
182                 line = "            " + line;
183             }
184             if (x < alerts.size() - 1) {
185                 line += ",";
186             }
187             else {
188                 line += ")";
189             }
190             lines.add(i++, line);
191         }
192         return i;
193     }
194 
195     private static String getActualString(final AssertionFailedError failure) {
196         final int lineLength = 96;
197 
198         String actual = failure.getActual().getStringRepresentation();
199         actual = actual.substring(0, actual.length() - 1);
200         actual = StringEscapeUtils.escapeJava(actual);
201         if (actual.length() > lineLength) {
202             final StringBuilder builder = new StringBuilder();
203             while (!actual.isEmpty()) {
204                 int length = actual.lastIndexOf(',', lineLength) + 1;
205                 if (length == 0 && !actual.isEmpty()) {
206                     length = Math.min(lineLength, actual.length());
207                 }
208                 if (builder.length() != 0) {
209                     builder.append(System.lineSeparator()).append("                + ");
210                 }
211                 builder.append('"').append(actual.substring(0, length)).append('"');
212                 actual = actual.substring(length);
213             }
214             return builder.toString();
215         }
216         return "\"" + actual + "\"";
217     }
218 
219     private static void removeNotYetImplemented(final List<String> lines,
220             final int i, final String browserString) {
221         final String previous = lines.get(i - 1);
222         if (previous.contains("@NotYetImplemented")) {
223             if (previous.indexOf('(') != -1) {
224                 final int p0 = previous.indexOf('(') + 1;
225                 final int p1 = previous.lastIndexOf(')');
226                 String browsers = previous.substring(p0, p1);
227                 if (browsers.indexOf('{') != -1) {
228                     browsers = browsers.substring(1, browsers.length() - 1).trim();
229                 }
230                 final Set<String> browserSet = new HashSet<>();
231                 for (final String browser : browsers.split(",")) {
232                     browserSet.add(browser.trim());
233                 }
234                 browserSet.remove(browserString);
235                 if (browserSet.size() == 1) {
236                     lines.set(i - 1, "    @NotYetImplemented(" + browserSet.iterator().next() + ")");
237                 }
238                 else if (browserSet.size() > 1) {
239                     lines.set(i - 1, "    @NotYetImplemented({" + String.join(", ", browserSet) + "})");
240                 }
241                 else {
242                     lines.remove(i - 1);
243                 }
244             }
245             else {
246                 final List<String> allBrowsers = new ArrayList<>(Arrays.asList("CHROME", "EDGE", "FF", "FF_ESR"));
247                 for (final Iterator<String> it = allBrowsers.iterator(); it.hasNext();) {
248                     if (it.next().equals(browserString)) {
249                         it.remove();
250                     }
251                 }
252                 lines.set(i - 1, "    @NotYetImplemented({" + String.join(", ", allBrowsers) + "})");
253             }
254         }
255     }
256 
257     private static void addNotYetImplementedMethod(final List<String> lines,
258             int i, final String browserString, final String methodName, final String defaultExpectations) {
259         String parent = methodName;
260         final String child = parent.substring(parent.lastIndexOf('_') + 1);
261         parent = parent.substring(1, parent.indexOf('_', 1));
262 
263         if (!lines.get(i).isEmpty()) {
264             i++;
265         }
266         lines.add(i++, "");
267         lines.add(i++, "    /**");
268         lines.add(i++, "     * @throws Exception if the test fails");
269         lines.add(i++, "     */");
270         lines.add(i++, "    @Test");
271         lines.add(i++, "    @Alerts(\"" + defaultExpectations + "\")");
272         lines.add(i++, "    @NotYetImplemented(" + browserString + ")");
273         lines.add(i++, "    public void _" + parent + "_" + child + "() throws Exception {");
274         lines.add(i++, "        test(\"" + parent + "\", \"" + child + "\");");
275         lines.add(i++, "    }");
276         lines.add(i++, "}");
277         while (lines.size() > i) {
278             lines.remove(i);
279         }
280     }
281 
282     private static void addNotYetImplemented(final List<String> lines, final int i, final String browserString) {
283         final String previous = lines.get(i - 1);
284         if (previous.contains("@NotYetImplemented")) {
285             if (previous.indexOf('(') != -1 && !previous.contains(browserString)) {
286                 final int p0 = previous.indexOf('(') + 1;
287                 final int p1 = previous.lastIndexOf(')');
288                 String browsers = previous.substring(p0, p1);
289                 if (browsers.indexOf('{') != -1) {
290                     browsers = browsers.substring(1, browsers.length() - 1).trim();
291                 }
292                 browsers += ", " + browserString;
293                 lines.set(i - 1, "    @NotYetImplemented({" + browsers + "})");
294             }
295         }
296         else {
297             lines.add(i, "    @NotYetImplemented(" + browserString + ")");
298         }
299     }
300 
301     private static void addMethodWithExpectation(final List<String> lines,
302             int i, final String browserString, final String methodName, final AssertionFailedError comparisonFailure,
303             final String defaultExpectations) {
304         String parent = methodName;
305         final String child = parent.substring(parent.lastIndexOf('_') + 1);
306         final int index = parent.indexOf('_', 1);
307         if (index != -1) {
308             parent = parent.substring(1, index);
309         }
310         else {
311             parent = parent.substring(1);
312         }
313 
314         if (!lines.get(i).isEmpty()) {
315             i++;
316         }
317         lines.add(i++, "");
318         lines.add(i++, "    /**");
319         lines.add(i++, "     * @throws Exception if the test fails");
320         lines.add(i++, "     */");
321         lines.add(i++, "    @Test");
322         lines.add(i++, "    @Alerts(DEFAULT = \"" + defaultExpectations + "\",");
323         lines.add(i++, "            " + browserString + " = " + getActualString(comparisonFailure) + ")");
324         if (index != -1) {
325             lines.add(i++, "    public void _" + parent + "_" + child + "() throws Exception {");
326             lines.add(i++, "        test(\"" + parent + "\", \"" + child + "\");");
327         }
328         else {
329             String method = parent;
330             for (final String prefix : HostExtractor.PREFIXES_) {
331                 if (method.startsWith(prefix)) {
332                     method = prefix.toLowerCase(Locale.ROOT) + method.substring(prefix.length());
333                     break;
334                 }
335             }
336             if (Character.isUpperCase(method.charAt(0))) {
337                 method = Character.toLowerCase(method.charAt(0)) + method.substring(1);
338             }
339             lines.add(i++, "    public void " + method + "() throws Exception {");
340             lines.add(i++, "        test(\"" + parent + "\");");
341         }
342         lines.add(i++, "    }");
343         lines.add(i++, "}");
344         while (lines.size() > i) {
345             lines.remove(i);
346         }
347     }
348 }