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