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