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.javascript.configuration;
16  
17  import static org.htmlunit.BrowserVersion.FIREFOX;
18  import static org.junit.Assert.assertEquals;
19  import static org.junit.Assert.assertTrue;
20  import static org.junit.Assert.fail;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.lang.reflect.Field;
25  import java.net.URISyntaxException;
26  import java.net.URL;
27  import java.text.MessageFormat;
28  import java.util.ArrayList;
29  import java.util.Collections;
30  import java.util.Enumeration;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Locale;
34  import java.util.Map;
35  import java.util.jar.JarEntry;
36  import java.util.jar.JarFile;
37  
38  import org.apache.commons.logging.Log;
39  import org.apache.commons.logging.LogFactory;
40  import org.apache.commons.text.RandomStringGenerator;
41  import org.htmlunit.BrowserVersion;
42  import org.htmlunit.MockWebConnection;
43  import org.htmlunit.WebClient;
44  import org.htmlunit.WebTestCase;
45  import org.htmlunit.javascript.HtmlUnitScriptable;
46  import org.htmlunit.javascript.JavaScriptEngine;
47  import org.junit.Assert;
48  import org.junit.Test;
49  
50  /**
51   * Tests for {@link JavaScriptConfiguration}.
52   *
53   * @author Chris Erskine
54   * @author Ahmed Ashour
55   * @author Ronald Brill
56   * @author Frank Danek
57   * @author Joerg Werner
58   */
59  public class JavaScriptConfigurationTest {
60  
61      private static final Log LOG = LogFactory.getLog(JavaScriptConfigurationTest.class);
62  
63      /**
64       * Test if configuration map expands with each new instance of BrowserVersion used.
65       *
66       * @throws Exception if the test fails
67       */
68      @Test
69      public void configurationMapExpands() throws Exception {
70          // get a reference to the leaky map
71          final Field field = JavaScriptConfiguration.class.getDeclaredField("CONFIGURATION_MAP_");
72          field.setAccessible(true);
73          final Map<?, ?> leakyMap = (Map<?, ?>) field.get(null);
74  
75          leakyMap.clear();
76          final int knownBrowsers = leakyMap.size();
77  
78          BrowserVersion browserVersion = new BrowserVersion.BrowserVersionBuilder(BrowserVersion.FIREFOX_ESR)
79                                                  .setApplicationVersion("App")
80                                                  .setApplicationVersion("Version")
81                                                  .setUserAgent("User agent")
82                                                  .build();
83          JavaScriptConfiguration.getInstance(browserVersion);
84  
85          browserVersion = new BrowserVersion.BrowserVersionBuilder(BrowserVersion.FIREFOX_ESR)
86                              .setApplicationVersion("App2")
87                              .setApplicationVersion("Version2")
88                              .setUserAgent("User agent2")
89                              .build();
90          JavaScriptConfiguration.getInstance(browserVersion);
91  
92          assertEquals(knownBrowsers + 1, leakyMap.size());
93      }
94  
95      /**
96       * Regression test for Bug #899.
97       * This test was throwing an OutOfMemoryError when the bug existed.
98       * @throws Exception if an error occurs
99       */
100     @Test
101     public void memoryLeak() throws Exception {
102         final RandomStringGenerator generator = new RandomStringGenerator.Builder().withinRange('a', 'z').get();
103 
104         long count = 0;
105         while (count++ < 3000) {
106             final BrowserVersion browserVersion = new BrowserVersion.BrowserVersionBuilder(BrowserVersion.FIREFOX_ESR)
107                                                     .setApplicationVersion("App" + generator.generate(20))
108                                                     .setApplicationVersion("Version" + generator.generate(20))
109                                                     .setUserAgent("User Agent" + generator.generate(20))
110                                                     .build();
111             JavaScriptConfiguration.getInstance(browserVersion);
112             if (LOG.isInfoEnabled()) {
113                 LOG.info("count: " + count + "; memory stats: " + getMemoryStats());
114             }
115         }
116         System.gc();
117     }
118 
119     private static String getMemoryStats() {
120         final Runtime rt = Runtime.getRuntime();
121         final long free = rt.freeMemory() / 1024;
122         final long total = rt.totalMemory() / 1024;
123         final long max = rt.maxMemory() / 1024;
124         final long used = total - free;
125         final String format = "used: {0,number,0}K, free: {1,number,0}K, total: {2,number,0}K, max: {3,number,0}K";
126         return MessageFormat.format(format,
127                 Long.valueOf(used), Long.valueOf(free), Long.valueOf(total), Long.valueOf(max));
128     }
129 
130     /**
131      * Tests that all classes in *.javascript.* which have {@link JsxClasses}/{@link JsxClass} annotation,
132      * are included in {@link JavaScriptConfiguration#CLASSES_}.
133      */
134     @Test
135     public void jsxClasses() {
136         final List<String> foundJsxClasses = new ArrayList<>();
137         for (final String className : getClassesForPackage(JavaScriptEngine.class)) {
138             if (!className.contains("$")) {
139                 Class<?> klass = null;
140                 try {
141                     klass = Class.forName(className);
142                 }
143                 catch (final Throwable e) {
144                     continue;
145                 }
146                 if ("org.htmlunit.javascript.host.intl".equals(klass.getPackage().getName())
147 
148                         // Worker
149                         || "WorkerGlobalScope".equals(klass.getSimpleName())
150                         || "DedicatedWorkerGlobalScope".equals(klass.getSimpleName())
151                         || "WorkerLocation".equals(klass.getSimpleName())
152                         || "WorkerNavigator".equals(klass.getSimpleName())
153 
154                         // ProxyConfig
155                         || "ProxyAutoConfig".equals(klass.getSimpleName())) {
156                     continue;
157                 }
158                 if (klass.getAnnotation(JsxClasses.class) != null) {
159                     foundJsxClasses.add(className);
160                 }
161                 else if (klass.getAnnotation(JsxClass.class) != null) {
162                     foundJsxClasses.add(className);
163                 }
164             }
165         }
166 
167         final List<String> definedClasses = new ArrayList<>();
168         for (final Class<?> klass : JavaScriptConfiguration.CLASSES_) {
169             definedClasses.add(klass.getName());
170         }
171         foundJsxClasses.removeAll(definedClasses);
172         if (!foundJsxClasses.isEmpty()) {
173             fail("Class " + foundJsxClasses.get(0) + " is not in JavaScriptConfiguration.CLASSES_");
174         }
175     }
176 
177     /**
178      * Return the classes inside the specified package and its sub-packages.
179      * @param klass a class inside that package
180      * @return a list of class names
181      */
182     public static List<String> getClassesForPackage(final Class<?> klass) {
183         final List<String> list = new ArrayList<>();
184 
185         File directory = null;
186         final String relPath = klass.getName().replace('.', '/') + ".class";
187 
188         final URL resource = JavaScriptConfiguration.class.getClassLoader().getResource(relPath);
189 
190         if (resource == null) {
191             throw new RuntimeException("No resource for " + relPath);
192         }
193         final String fullPath = resource.getFile();
194 
195         try {
196             directory = new File(resource.toURI()).getParentFile();
197         }
198         catch (final URISyntaxException e) {
199             throw new RuntimeException(klass.getName() + " (" + resource + ") does not appear to be a valid URL", e);
200         }
201         catch (final IllegalArgumentException e) {
202             directory = null;
203         }
204 
205         if (directory != null && directory.exists()) {
206             addClasses(directory, klass.getPackage().getName(), list);
207         }
208         else {
209             try {
210                 String jarPath = fullPath.replaceFirst("[.]jar[!].*", ".jar").replaceFirst("file:", "");
211                 if (System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win")) {
212                     jarPath = jarPath.replace("%20", " ");
213                 }
214                 try (JarFile jarFile = new JarFile(jarPath)) {
215                     for (final Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
216                         final String entryName = entries.nextElement().getName();
217                         if (entryName.endsWith(".class")) {
218                             list.add(entryName.replace('/', '.').replace('\\', '.').replace(".class", ""));
219                         }
220                     }
221                 }
222             }
223             catch (final IOException e) {
224                 throw new RuntimeException(klass.getPackage().getName() + " does not appear to be a valid package", e);
225             }
226         }
227         return list;
228     }
229 
230     private static void addClasses(final File directory, final String packageName, final List<String> list) {
231         final File[] files = directory.listFiles();
232         if (files != null) {
233             for (final File file : files) {
234                 final String name = file.getName();
235                 if (name.endsWith(".class")) {
236                     list.add(packageName + '.' + name.substring(0, name.length() - 6));
237                 }
238                 else if (file.isDirectory() && !".git".equals(file.getName())) {
239                     addClasses(file, packageName + '.' + file.getName(), list);
240                 }
241             }
242         }
243     }
244 
245     /**
246      * Tests that all classes included in {@link JavaScriptConfiguration#CLASSES_} defining an
247      * {@link JsxClasses}/{@link JsxClass} annotation for at least one browser.
248      */
249     @Test
250     public void obsoleteJsxClasses() {
251         final JavaScriptConfiguration config = JavaScriptConfiguration.getInstance(FIREFOX);
252 
253         for (final Class<? extends HtmlUnitScriptable> klass : config.getClasses()) {
254             boolean found = false;
255             for (final BrowserVersion browser : BrowserVersion.ALL_SUPPORTED_BROWSERS) {
256                 if (AbstractJavaScriptConfiguration.getClassConfiguration(klass, browser) != null) {
257                     found = true;
258                     break;
259                 }
260             }
261             assertTrue("Class " + klass
262                     + " is member of JavaScriptConfiguration.CLASSES_ but does not define @JsxClasses/@JsxClass",
263                     found);
264         }
265     }
266 
267     /**
268      * Test of order.
269      */
270     @Test
271     public void treeOrder() {
272         final List<String> defined = new ArrayList<>(JavaScriptConfiguration.CLASSES_.length);
273 
274         final HashMap<Integer, List<String>> levels = new HashMap<>();
275         for (final Class<?> c : JavaScriptConfiguration.CLASSES_) {
276             defined.add(c.getSimpleName());
277 
278             int level = 1;
279             Class<?> parent = c.getSuperclass();
280             while (parent != HtmlUnitScriptable.class) {
281                 level++;
282                 parent = parent.getSuperclass();
283             }
284 
285             List<String> clsAtLevel = levels.get(level);
286             if (clsAtLevel == null) {
287                 clsAtLevel = new ArrayList<>();
288                 levels.put(level, clsAtLevel);
289             }
290             clsAtLevel.add(c.getSimpleName());
291         }
292 
293         final List<String> all = new ArrayList<>(JavaScriptConfiguration.CLASSES_.length);
294         for (int level = 1; level <= levels.size(); level++) {
295             final List<String> clsAtLevel = levels.get(level);
296             Collections.sort(clsAtLevel);
297             all.addAll(clsAtLevel);
298 
299             // dump
300             /*
301             final String indent = "       ";
302             System.out.println(indent + " // level " + level);
303 
304             System.out.print(indent);
305             int chars = indent.length();
306             for (final String cls : clsAtLevel) {
307                 final String toPrint = " " + cls + ".class,";
308                 chars += toPrint.length();
309                 if (chars > 120) {
310                     System.out.println();
311                     System.out.print(indent);
312                     chars = indent.length() + toPrint.length();
313                 }
314                 System.out.print(toPrint);
315             }
316             System.out.println();
317             */
318         }
319         Assert.assertEquals(all, defined);
320     }
321 
322     /**
323      * See issue 1890.
324      *
325      * @throws Exception if the test fails
326      */
327     @Test
328     public void original() throws Exception {
329         final BrowserVersion browserVersion = BrowserVersion.CHROME;
330 
331         test(browserVersion);
332     }
333 
334     /**
335      * See issue 1890.
336      *
337      * @throws Exception if the test fails
338      */
339     @Test
340     public void cloned() throws Exception {
341         final BrowserVersion browserVersion = new BrowserVersion.BrowserVersionBuilder(BrowserVersion.FIREFOX)
342                                                     .build();
343 
344         test(browserVersion);
345     }
346 
347     /**
348      * See issue 1890.
349      *
350      * @throws Exception if the test fails
351      */
352     @Test
353     public void clonedAndModified() throws Exception {
354         final BrowserVersion browserVersion = new BrowserVersion.BrowserVersionBuilder(BrowserVersion.FIREFOX)
355                                                     .setUserAgent("foo")
356                                                     .build();
357 
358         test(browserVersion);
359     }
360 
361     private static void test(final BrowserVersion browserVersion) throws IOException {
362         try (WebClient webClient = new WebClient(browserVersion)) {
363             final MockWebConnection conn = new MockWebConnection();
364             conn.setDefaultResponse(WebTestCase.DOCTYPE_HTML
365                     + "<html><body onload='document.body.firstChild'></body></html>");
366             webClient.setWebConnection(conn);
367 
368             webClient.getPage("http://localhost/");
369         }
370     }
371 
372 }