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