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.source;
16  
17  import static java.nio.charset.StandardCharsets.ISO_8859_1;
18  
19  import java.io.BufferedReader;
20  import java.io.BufferedWriter;
21  import java.io.File;
22  import java.io.FileReader;
23  import java.io.FileWriter;
24  import java.io.IOException;
25  import java.lang.reflect.Method;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Collections;
29  import java.util.HashMap;
30  import java.util.HashSet;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.TreeMap;
35  import java.util.TreeSet;
36  import java.util.regex.Matcher;
37  import java.util.regex.Pattern;
38  
39  import org.apache.commons.io.FileUtils;
40  import org.apache.commons.lang3.reflect.MethodUtils;
41  import org.htmlunit.WebDriverTestCase;
42  import org.htmlunit.junit.annotation.NotYetImplemented;
43  import org.htmlunit.junit.annotation.TestedBrowser;
44  import org.htmlunit.libraries.JQuery1x8x2Test;
45  
46  /**
47   * Extracts the needed expectation from the real browsers output, this is done by waiting the browser to finish
48   * all the tests, then select all visible text and copy it to a local file.
49   *
50   * Steps to generate the tests:
51   * <ol>
52   *   <li>Call {@link #extractExpectations(File, File)}, where <tt>input</tt> is the raw file from the browser</li>
53   *   <li>Have a quick look on the output files, and compare them to verify there are only minimal differences</li>
54   *   <li>Rename all outputs to browser names e.g. {@code results.IE.txt}, {@code results.FF60.txt}, etc</li>
55   *   <li>Put all outputs in one folder and call {@link #generateTestCases(Class, File)}</li>
56   * </ol>
57   *
58   * @author Ahmed Ashour
59   * @author Marc Guillemot
60   * @author Ronald Brill
61   */
62  public final class JQueryExtractor {
63      private static final String RERUN_ID = "Rerun [";
64  
65      private JQueryExtractor() {
66      }
67  
68      /**
69       * Main method.
70       * @param args program arguments
71       * @throws Exception s
72       */
73      public static void main(final String[] args) throws Exception {
74          final Class<? extends WebDriverTestCase> testClass = JQuery1x8x2Test.class;
75          // final Class<? extends WebDriverTestCase> testClass = JQuery1x11x3Test.class;
76          // final Class<? extends WebDriverTestCase> testClass = JQuery3x3x1Test.class;
77  
78          final String version = (String) MethodUtils.invokeExactMethod(testClass.newInstance(), "getVersion");
79          final File baseDir = new File("src/test/resources/libraries/jQuery/" + version + "/expectations");
80  
81          for (final String browser : new String[] {"CHROME", "EDGE", "FF", "FF_ESR"}) {
82              final File out = new File(baseDir, browser + ".out");
83              final File results = new File(baseDir, "results." + browser + ".txt");
84              extractExpectations(out, results);
85          }
86  
87          generateTestCases(testClass, baseDir);
88      }
89  
90      /**
91       * Transforms the raw expectation, to the needed one by HtmlUnit.
92       * This methods puts only the main line of the test output, without the details.
93       *
94       * @param input the raw file of real browser, with header and footer removed
95       * @param output the expectation
96       * @throws IOException if an error occurs
97       */
98      public static void extractExpectations(final File input, final File output) throws IOException {
99          try (BufferedReader reader = new BufferedReader(new FileReader(input))) {
100             try (BufferedWriter writer = new BufferedWriter(new FileWriter(output))) {
101                 int testNumber = 1;
102                 String line;
103                 while ((line = reader.readLine()) != null) {
104                     line = line.trim();
105 
106                     // for jQuery 3.3.1 we have patched the test output
107                     // to make the hash visible
108                     if (line.contains(RERUN_ID)) {
109                         // cleanup ie output
110                         line = line.replace(".skipped", ".");
111 
112                         final int start = line.indexOf(RERUN_ID) + RERUN_ID.length();
113                         final int end = line.indexOf(']', start);
114                         final String testId = line.substring(start, end);
115 
116                         line = line.substring(0, line.indexOf(RERUN_ID)).trim();
117                         final String prefix = "" + testNumber + ".";
118                         if (line.startsWith(prefix)) {
119                             line = line.substring(prefix.length());
120                         }
121                         line = "" + testNumber + '.' + ' ' + line + " [" + testId + ']';
122                         writer.write(line + System.lineSeparator());
123                         testNumber++;
124                     }
125 
126                     // the test number is at least for 1.11.3 no longer part of the output
127                     // instead a ordered list is used by qunit
128                     // if (line.startsWith("" + testNumber + '.') && endPos > -1) {
129                     else if (line.indexOf("Rerun") > -1) {
130                         line = line.substring(0, line.indexOf("Rerun"))
131                                 + " [" + testNumber + ']';
132                         writer.write(line + System.lineSeparator());
133                         testNumber++;
134                     }
135                     else if (line.endsWith("Rerun")) {
136                         if (line.indexOf("" + testNumber + '.', 4) != -1) {
137                             System.out.println("Incorrect line for test# " + testNumber
138                                     + ", please correct it manually");
139                             break;
140                         }
141                         line = "" + testNumber + '.'
142                                 + ' ' + line.substring(0, line.length() - 5)
143                                 + " [" + testNumber + ']';
144                         writer.write(line + System.lineSeparator());
145                         testNumber++;
146                     }
147                 }
148                 System.out.println("Last output #" + (testNumber - 1));
149             }
150         }
151     }
152 
153     /**
154      * Generates the java code of the test cases.
155      * @param testClass the class containing the tests
156      * @param dir the directory which holds the expectations
157      * @throws Exception if an error occurs.
158      */
159     public static void generateTestCases(final Class<? extends WebDriverTestCase> testClass,
160             final File dir) throws Exception {
161         final TestedBrowser[] browsers = TestedBrowser.values();
162         // main browsers regardless of version e.g. "FF"
163         final List<String> mainNames = new ArrayList<>();
164         for (final TestedBrowser b : browsers) {
165             final String name = b.name();
166             if (!"NONE".equals(name) && Character.isLetter(name.charAt(name.length() - 1))) {
167                 mainNames.add(name);
168             }
169         }
170 
171         final Map<String, List<String>> browserVersions = new HashMap<>();
172         for (final TestedBrowser b : browsers) {
173             final String name = b.name();
174             for (final String mainName : mainNames) {
175                 if (!name.equals(mainName) && name.startsWith(mainName)) {
176                     List<String> list = browserVersions.get(mainName);
177                     if (list == null) {
178                         list = new ArrayList<>();
179                         browserVersions.put(mainName, list);
180                     }
181                     list.add(name);
182                 }
183             }
184         }
185         final Map<String, Expectations> browserExpectations = new HashMap<>();
186         final File[] files = dir.listFiles();
187         if (files != null) {
188             for (final File file : files) {
189                 if (file.isFile() && file.getName().endsWith(".txt")) {
190                     for (final TestedBrowser b : browsers) {
191                         final String browserName = b.name();
192                         if (file.getName().equalsIgnoreCase("results." + browserName.replace('_', '.') + ".txt")) {
193                             browserExpectations.put(browserName, Expectations.readExpectations(file));
194                         }
195                     }
196                 }
197             }
198         }
199 
200         // gather all the tests (some tests don't get executed for all browsers)
201         final List<Test> allTests = computeTestsList(browserExpectations);
202 
203         final Collection<String> availableBrowserNames = new TreeSet<>(browserExpectations.keySet());
204         for (final Test test : allTests) {
205             final Map<String, String> testExpectation = new TreeMap<>();
206             final Map<Integer, List<String>> lineToBrowser = new TreeMap<>();
207             for (final String browserName : availableBrowserNames) {
208                 final Expectation expectation = browserExpectations.get(browserName).getExpectation(test);
209                 if (expectation != null) {
210                     List<String> browsersForLine = lineToBrowser.get(expectation.getLine());
211                     if (browsersForLine == null) {
212                         browsersForLine = new ArrayList<>();
213                         lineToBrowser.put(expectation.getLine(), browsersForLine);
214                     }
215                     browsersForLine.add(browserName);
216                     final String str = expectation.getTestResult();
217                     testExpectation.put(browserName,
218                             str.replaceAll("\\\\", "\\\\\\\\").replaceAll("\\\"", "\\\\\""));
219                 }
220             }
221             System.out.println("    /**");
222             System.out.println("     * Test " + lineToBrowser + ".");
223             System.out.println("     * @throws Exception if an error occurs");
224             System.out.println("     */");
225             System.out.println("    @Test");
226             System.out.print("    @Alerts(");
227 
228             final boolean allSame = testExpectation.size() == availableBrowserNames.size()
229                     && new HashSet<>(testExpectation.values()).size() == 1;
230             if (allSame) {
231                 final String first = testExpectation.keySet().iterator().next();
232                 String expectation = testExpectation.get(first);
233                 if (expectation.length() > 100) {
234                     expectation = expectation.substring(0, 50) + "\"\n            + \"" + expectation.substring(50);
235                 }
236                 System.out.print("\"" + expectation + '"');
237             }
238             else {
239                 final List<String> cleanedBrowserNames = new ArrayList<>(testExpectation.keySet());
240                 Collections.sort(cleanedBrowserNames);
241 
242                 if (testExpectation.size() == availableBrowserNames.size()) {
243                     // Hack a bit to avoid redundant alerts
244                     // find the best default
245                     int matches = 0;
246                     ArrayList<String> defaultBrowsers = null;
247 
248                     for (final String browser : cleanedBrowserNames) {
249                         final String expectation = testExpectation.get(browser);
250 
251                         final ArrayList<String> matchBrowsers = new ArrayList<>();
252                         int matchCount = 0;
253                         for (final String otherBrowser : cleanedBrowserNames) {
254                             if (!browser.equals(otherBrowser)
255                                     && expectation.equals(testExpectation.get(otherBrowser))) {
256                                 matchCount++;
257                                 matchBrowsers.add(otherBrowser);
258                             }
259                         }
260                         if (matches < matchCount) {
261                             matches = matchCount;
262                             matchBrowsers.add(browser);
263                             defaultBrowsers = matchBrowsers;
264                         }
265                     }
266 
267                     if (matches > 1) {
268                         testExpectation.put("DEFAULT", testExpectation.get(defaultBrowsers.get(0)));
269                         cleanedBrowserNames.add(0, "DEFAULT");
270                         for (final String browser : defaultBrowsers) {
271                             testExpectation.remove(browser);
272                             cleanedBrowserNames.remove(browser);
273                         }
274                     }
275                 }
276 
277                 boolean first = true;
278                 if (cleanedBrowserNames.size() == 1 && "DEFAULT".equals(cleanedBrowserNames.get(0))) {
279                     System.out.print("\"" + testExpectation.get("DEFAULT") + '"');
280                 }
281                 else {
282                     for (final String browserName : cleanedBrowserNames) {
283                         final String expectation = testExpectation.get(browserName);
284                         if (expectation == null) {
285                             continue; // test didn't run for this browser
286                         }
287                         if (!first) {
288                             System.out.println(",");
289                             System.out.print("            ");
290                         }
291                         System.out.print(browserName + " = \"" + expectation + '"');
292                         first = false;
293                     }
294                 }
295             }
296             System.out.println(")");
297 
298             final String methodName = test.getName().replaceAll("\\W", "_");
299             try {
300                 final Method method = testClass.getMethod(methodName);
301                 final NotYetImplemented notYetImplemented = method.getAnnotation(NotYetImplemented.class);
302                 if (null != notYetImplemented) {
303                     final TestedBrowser[] notYetImplementedBrowsers = notYetImplemented.value();
304                     if (notYetImplementedBrowsers.length > 0) {
305                         final List<String> browserNames = new ArrayList<>(notYetImplementedBrowsers.length);
306                         for (final TestedBrowser browser : notYetImplementedBrowsers) {
307                             browserNames.add(browser.name());
308                         }
309                         Collections.sort(browserNames);
310 
311                         // TODO dirty hack
312                         if (browserNames.size() == 5
313                                 && browserNames.contains("CHROME")
314                                 && browserNames.contains("EDGE")
315                                 && browserNames.contains("FF")
316                                 && browserNames.contains("FF_ESR")) {
317                             System.out.println("    @NotYetImplemented");
318                         }
319                         else {
320                             System.out.print("    @NotYetImplemented(");
321                             if (browserNames.size() > 1) {
322                                 System.out.print("{ ");
323                             }
324                             System.out.print(String.join(", ", browserNames));
325                             if (browserNames.size() > 1) {
326                                 System.out.print(" }");
327                             }
328                             System.out.println(")");
329                         }
330                     }
331                 }
332             }
333             catch (final NoSuchMethodException e) {
334                 // ignore
335             }
336 
337             System.out.println("    public void " + methodName + "() throws Exception {");
338             System.out.println("        runTest(\"" + test.getName().replace("\"", "\\\"") + "\");");
339             System.out.println("    }");
340             System.out.println();
341         }
342     }
343 
344     private static List<Test> computeTestsList(final Map<String, Expectations> browserExpectations) {
345         final Map<String, Test> map = new HashMap<>();
346         for (final Expectations expectations : browserExpectations.values()) {
347             for (final Expectation expectation : expectations) {
348                 final String testName = expectation.getTestName();
349                 Test test = map.get(testName);
350                 if (test == null) {
351                     test = new Test(testName);
352                     map.put(testName, test);
353                 }
354                 test.addLine(expectation.getLine());
355             }
356         }
357 
358         final List<Test> tests = new ArrayList<>(map.values());
359         Collections.sort(tests);
360 
361         return tests;
362     }
363 
364     static class Expectations implements Iterable<Expectation> {
365         static Expectations readExpectations(final File file) throws IOException {
366             final Expectations expectations = new Expectations();
367             final List<String> lines = FileUtils.readLines(file, ISO_8859_1);
368             for (int i = 0; i < lines.size(); i++) {
369                 expectations.add(new Expectation(file, i + 1, lines.get(i)));
370             }
371 
372             return expectations;
373         }
374 
375         private final Map<String, Expectation> expectations_ = new HashMap<>();
376 
377         public Expectation getExpectation(final Test test) {
378             return expectations_.get(test.getName());
379         }
380 
381         private void add(final Expectation expectation) {
382             expectations_.put(expectation.getTestName(), expectation);
383         }
384 
385         @Override
386         public Iterator<Expectation> iterator() {
387             return expectations_.values().iterator();
388         }
389     }
390 
391     static class Expectation {
392 
393         private static final Pattern PATTERN =
394                 Pattern.compile("(\\d+\\. ?)?(.+)\\((\\d+(, \\d+, \\d+)?)\\) \\[([0-9a-f]{1,8})\\]");
395 
396         private final int line_;
397         private final String testId_;
398         private final String testName_;
399         private final String testResult_;
400 
401         Expectation(final File file, final int line, final String string) {
402             line_ = line;
403             final Matcher matcher = PATTERN.matcher(string);
404             if (!matcher.matches()) {
405                 throw new RuntimeException("Invalid line " + line + ": '" + string
406                         + "' in file: " + file.getAbsolutePath());
407             }
408             final String testNumber = matcher.group(1);
409             if (testNumber != null && !testNumber.trim().equals(line + ".")) {
410                 throw new RuntimeException("Invalid test number for line " + line + ": " + string
411                         + " in file: " + file.getAbsolutePath());
412             }
413 
414             testName_ = matcher.group(2).trim();
415             testResult_ = matcher.group(3);
416             testId_ = matcher.group(4);
417         }
418 
419         public int getLine() {
420             return line_;
421         }
422 
423         public String getTestId() {
424             return testId_;
425         }
426 
427         public String getTestName() {
428             return testName_;
429         }
430 
431         public String getTestResult() {
432             return testResult_;
433         }
434     }
435 
436     static class Test implements Comparable<Test> {
437         private final List<Integer> lines_ = new ArrayList<>();
438         private final String name_;
439 
440         Test(final String name) {
441             name_ = name;
442         }
443 
444         public String getName() {
445             return name_;
446         }
447 
448         void addLine(final int line) {
449             if (!lines_.contains(line)) {
450                 lines_.add(line);
451                 Collections.sort(lines_);
452             }
453         }
454 
455         @Override
456         public int compareTo(final Test o) {
457             int diff = lines_.get(0) - o.lines_.get(0);
458             if (diff == 0) {
459                 diff = lines_.size() - o.lines_.size();
460                 if (diff == 0) {
461                     diff = name_.compareTo(o.name_);
462                 }
463             }
464             return diff;
465         }
466 
467         @Override
468         public int hashCode() {
469             return name_.hashCode();
470         }
471 
472         @Override
473         public boolean equals(final Object obj) {
474             return (obj instanceof Test) && name_.equals(((Test) obj).name_);
475         }
476     }
477 }