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.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
52
53
54
55
56
57
58
59 public class JavaScriptConfigurationTest {
60
61 private static final Log LOG = LogFactory.getLog(JavaScriptConfigurationTest.class);
62
63
64
65
66
67
68 @Test
69 public void configurationMapExpands() throws Exception {
70
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
97
98
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
132
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
149 || "WorkerGlobalScope".equals(klass.getSimpleName())
150 || "DedicatedWorkerGlobalScope".equals(klass.getSimpleName())
151 || "WorkerLocation".equals(klass.getSimpleName())
152 || "WorkerNavigator".equals(klass.getSimpleName())
153
154
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
179
180
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
247
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
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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318 }
319 Assert.assertEquals(all, defined);
320 }
321
322
323
324
325
326
327 @Test
328 public void original() throws Exception {
329 final BrowserVersion browserVersion = BrowserVersion.CHROME;
330
331 test(browserVersion);
332 }
333
334
335
336
337
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
349
350
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 }