|
1 /* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */ |
|
2 // Test timeout (seconds) |
|
3 var gTimeoutSeconds = 45; |
|
4 var gConfig; |
|
5 |
|
6 if (Cc === undefined) { |
|
7 var Cc = Components.classes; |
|
8 var Ci = Components.interfaces; |
|
9 var Cu = Components.utils; |
|
10 } |
|
11 |
|
12 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
13 Cu.import("resource://gre/modules/Task.jsm"); |
|
14 |
|
15 XPCOMUtils.defineLazyModuleGetter(this, "Services", |
|
16 "resource://gre/modules/Services.jsm"); |
|
17 |
|
18 XPCOMUtils.defineLazyModuleGetter(this, "BrowserNewTabPreloader", |
|
19 "resource:///modules/BrowserNewTabPreloader.jsm", "BrowserNewTabPreloader"); |
|
20 |
|
21 XPCOMUtils.defineLazyModuleGetter(this, "CustomizationTabPreloader", |
|
22 "resource:///modules/CustomizationTabPreloader.jsm", "CustomizationTabPreloader"); |
|
23 |
|
24 const SIMPLETEST_OVERRIDES = |
|
25 ["ok", "is", "isnot", "ise", "todo", "todo_is", "todo_isnot", "info", "expectAssertions"]; |
|
26 |
|
27 window.addEventListener("load", testOnLoad, false); |
|
28 |
|
29 function testOnLoad() { |
|
30 window.removeEventListener("load", testOnLoad, false); |
|
31 |
|
32 gConfig = readConfig(); |
|
33 if (gConfig.testRoot == "browser" || |
|
34 gConfig.testRoot == "metro" || |
|
35 gConfig.testRoot == "webapprtChrome") { |
|
36 // Make sure to launch the test harness for the first opened window only |
|
37 var prefs = Services.prefs; |
|
38 if (prefs.prefHasUserValue("testing.browserTestHarness.running")) |
|
39 return; |
|
40 |
|
41 prefs.setBoolPref("testing.browserTestHarness.running", true); |
|
42 |
|
43 if (prefs.prefHasUserValue("testing.browserTestHarness.timeout")) |
|
44 gTimeoutSeconds = prefs.getIntPref("testing.browserTestHarness.timeout"); |
|
45 |
|
46 var sstring = Cc["@mozilla.org/supports-string;1"]. |
|
47 createInstance(Ci.nsISupportsString); |
|
48 sstring.data = location.search; |
|
49 |
|
50 Services.ww.openWindow(window, "chrome://mochikit/content/browser-harness.xul", "browserTest", |
|
51 "chrome,centerscreen,dialog=no,resizable,titlebar,toolbar=no,width=800,height=600", sstring); |
|
52 } else { |
|
53 // This code allows us to redirect without requiring specialpowers for chrome and a11y tests. |
|
54 let messageHandler = function(m) { |
|
55 messageManager.removeMessageListener("chromeEvent", messageHandler); |
|
56 var url = m.json.data; |
|
57 |
|
58 // Window is the [ChromeWindow] for messageManager, so we need content.window |
|
59 // Currently chrome tests are run in a content window instead of a ChromeWindow |
|
60 var webNav = content.window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) |
|
61 .getInterface(Components.interfaces.nsIWebNavigation); |
|
62 webNav.loadURI(url, null, null, null, null); |
|
63 }; |
|
64 |
|
65 var listener = 'data:,function doLoad(e) { var data=e.detail&&e.detail.data;removeEventListener("contentEvent", function (e) { doLoad(e); }, false, true);sendAsyncMessage("chromeEvent", {"data":data}); };addEventListener("contentEvent", function (e) { doLoad(e); }, false, true);'; |
|
66 messageManager.loadFrameScript(listener, true); |
|
67 messageManager.addMessageListener("chromeEvent", messageHandler); |
|
68 } |
|
69 if (gConfig.e10s) { |
|
70 e10s_init(); |
|
71 } |
|
72 } |
|
73 |
|
74 function Tester(aTests, aDumper, aCallback) { |
|
75 this.dumper = aDumper; |
|
76 this.tests = aTests; |
|
77 this.callback = aCallback; |
|
78 this.openedWindows = {}; |
|
79 this.openedURLs = {}; |
|
80 |
|
81 this._scriptLoader = Services.scriptloader; |
|
82 this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", this.EventUtils); |
|
83 var simpleTestScope = {}; |
|
84 this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/specialpowersAPI.js", simpleTestScope); |
|
85 this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/SpecialPowersObserverAPI.js", simpleTestScope); |
|
86 this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromePowers.js", simpleTestScope); |
|
87 this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", simpleTestScope); |
|
88 this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/MemoryStats.js", simpleTestScope); |
|
89 this._scriptLoader.loadSubScript("chrome://mochikit/content/chrome-harness.js", simpleTestScope); |
|
90 this.SimpleTest = simpleTestScope.SimpleTest; |
|
91 this.MemoryStats = simpleTestScope.MemoryStats; |
|
92 this.Task = Task; |
|
93 this.Promise = Components.utils.import("resource://gre/modules/Promise.jsm", null).Promise; |
|
94 this.Assert = Components.utils.import("resource://testing-common/Assert.jsm", null).Assert; |
|
95 |
|
96 this.SimpleTestOriginal = {}; |
|
97 SIMPLETEST_OVERRIDES.forEach(m => { |
|
98 this.SimpleTestOriginal[m] = this.SimpleTest[m]; |
|
99 }); |
|
100 |
|
101 this._uncaughtErrorObserver = function({message, date, fileName, stack, lineNumber}) { |
|
102 let text = "Once bug 991040 has landed, THIS ERROR WILL CAUSE A TEST FAILURE.\n" + message; |
|
103 let error = text; |
|
104 if (fileName || lineNumber) { |
|
105 error = { |
|
106 fileName: fileName, |
|
107 lineNumber: lineNumber, |
|
108 message: text, |
|
109 toString: function() { |
|
110 return text; |
|
111 } |
|
112 }; |
|
113 } |
|
114 this.currentTest.addResult( |
|
115 new testResult( |
|
116 /*success*/ true, |
|
117 /*name*/"A promise chain failed to handle a rejection", |
|
118 /*error*/error, |
|
119 /*known*/true, |
|
120 /*stack*/stack)); |
|
121 }.bind(this); |
|
122 } |
|
123 Tester.prototype = { |
|
124 EventUtils: {}, |
|
125 SimpleTest: {}, |
|
126 Task: null, |
|
127 Promise: null, |
|
128 Assert: null, |
|
129 |
|
130 repeat: 0, |
|
131 runUntilFailure: false, |
|
132 checker: null, |
|
133 currentTestIndex: -1, |
|
134 lastStartTime: null, |
|
135 openedWindows: null, |
|
136 lastAssertionCount: 0, |
|
137 |
|
138 get currentTest() { |
|
139 return this.tests[this.currentTestIndex]; |
|
140 }, |
|
141 get done() { |
|
142 return this.currentTestIndex == this.tests.length - 1; |
|
143 }, |
|
144 |
|
145 start: function Tester_start() { |
|
146 // Check whether this window is ready to run tests. |
|
147 if (window.BrowserChromeTest) { |
|
148 BrowserChromeTest.runWhenReady(this.actuallyStart.bind(this)); |
|
149 return; |
|
150 } |
|
151 this.actuallyStart(); |
|
152 }, |
|
153 |
|
154 actuallyStart: function Tester_actuallyStart() { |
|
155 //if testOnLoad was not called, then gConfig is not defined |
|
156 if (!gConfig) |
|
157 gConfig = readConfig(); |
|
158 |
|
159 if (gConfig.runUntilFailure) |
|
160 this.runUntilFailure = true; |
|
161 |
|
162 if (gConfig.repeat) |
|
163 this.repeat = gConfig.repeat; |
|
164 |
|
165 this.dumper.dump("*** Start BrowserChrome Test Results ***\n"); |
|
166 Services.console.registerListener(this); |
|
167 Services.obs.addObserver(this, "chrome-document-global-created", false); |
|
168 Services.obs.addObserver(this, "content-document-global-created", false); |
|
169 this._globalProperties = Object.keys(window); |
|
170 this._globalPropertyWhitelist = [ |
|
171 "navigator", "constructor", "top", |
|
172 "Application", |
|
173 "__SS_tabsToRestore", "__SSi", |
|
174 "webConsoleCommandController", |
|
175 ]; |
|
176 |
|
177 this.Promise.Debugging.clearUncaughtErrorObservers(); |
|
178 this.Promise.Debugging.addUncaughtErrorObserver(this._uncaughtErrorObserver); |
|
179 |
|
180 if (this.tests.length) |
|
181 this.nextTest(); |
|
182 else |
|
183 this.finish(); |
|
184 }, |
|
185 |
|
186 waitForWindowsState: function Tester_waitForWindowsState(aCallback) { |
|
187 let timedOut = this.currentTest && this.currentTest.timedOut; |
|
188 let baseMsg = timedOut ? "Found a {elt} after previous test timed out" |
|
189 : this.currentTest ? "Found an unexpected {elt} at the end of test run" |
|
190 : "Found an unexpected {elt}"; |
|
191 |
|
192 // Remove stale tabs |
|
193 if (this.currentTest && window.gBrowser && gBrowser.tabs.length > 1) { |
|
194 while (gBrowser.tabs.length > 1) { |
|
195 let lastTab = gBrowser.tabContainer.lastChild; |
|
196 let msg = baseMsg.replace("{elt}", "tab") + |
|
197 ": " + lastTab.linkedBrowser.currentURI.spec; |
|
198 this.currentTest.addResult(new testResult(false, msg, "", false)); |
|
199 gBrowser.removeTab(lastTab); |
|
200 } |
|
201 } |
|
202 |
|
203 // Replace the last tab with a fresh one |
|
204 if (window.gBrowser) { |
|
205 gBrowser.addTab("about:blank", { skipAnimation: true }); |
|
206 gBrowser.removeCurrentTab(); |
|
207 gBrowser.stop(); |
|
208 } |
|
209 |
|
210 // Remove stale windows |
|
211 this.dumper.dump("TEST-INFO | checking window state\n"); |
|
212 let windowsEnum = Services.wm.getEnumerator(null); |
|
213 while (windowsEnum.hasMoreElements()) { |
|
214 let win = windowsEnum.getNext(); |
|
215 if (win != window && !win.closed && |
|
216 win.document.documentElement.getAttribute("id") != "browserTestHarness") { |
|
217 let type = win.document.documentElement.getAttribute("windowtype"); |
|
218 switch (type) { |
|
219 case "navigator:browser": |
|
220 type = "browser window"; |
|
221 break; |
|
222 case null: |
|
223 type = "unknown window"; |
|
224 break; |
|
225 } |
|
226 let msg = baseMsg.replace("{elt}", type); |
|
227 if (this.currentTest) |
|
228 this.currentTest.addResult(new testResult(false, msg, "", false)); |
|
229 else |
|
230 this.dumper.dump("TEST-UNEXPECTED-FAIL | (browser-test.js) | " + msg + "\n"); |
|
231 |
|
232 win.close(); |
|
233 } |
|
234 } |
|
235 |
|
236 // Make sure the window is raised before each test. |
|
237 this.SimpleTest.waitForFocus(aCallback); |
|
238 }, |
|
239 |
|
240 finish: function Tester_finish(aSkipSummary) { |
|
241 this.Promise.Debugging.flushUncaughtErrors(); |
|
242 |
|
243 var passCount = this.tests.reduce(function(a, f) a + f.passCount, 0); |
|
244 var failCount = this.tests.reduce(function(a, f) a + f.failCount, 0); |
|
245 var todoCount = this.tests.reduce(function(a, f) a + f.todoCount, 0); |
|
246 |
|
247 if (this.repeat > 0) { |
|
248 --this.repeat; |
|
249 this.currentTestIndex = -1; |
|
250 this.nextTest(); |
|
251 } |
|
252 else{ |
|
253 Services.console.unregisterListener(this); |
|
254 Services.obs.removeObserver(this, "chrome-document-global-created"); |
|
255 Services.obs.removeObserver(this, "content-document-global-created"); |
|
256 this.Promise.Debugging.clearUncaughtErrorObservers(); |
|
257 this.dumper.dump("\nINFO TEST-START | Shutdown\n"); |
|
258 |
|
259 if (this.tests.length) { |
|
260 this.dumper.dump("Browser Chrome Test Summary\n"); |
|
261 |
|
262 this.dumper.dump("\tPassed: " + passCount + "\n" + |
|
263 "\tFailed: " + failCount + "\n" + |
|
264 "\tTodo: " + todoCount + "\n"); |
|
265 } else { |
|
266 this.dumper.dump("TEST-UNEXPECTED-FAIL | (browser-test.js) | " + |
|
267 "No tests to run. Did you pass an invalid --test-path?\n"); |
|
268 } |
|
269 this.dumper.dump("\n*** End BrowserChrome Test Results ***\n"); |
|
270 |
|
271 this.dumper.done(); |
|
272 |
|
273 // Tests complete, notify the callback and return |
|
274 this.callback(this.tests); |
|
275 this.callback = null; |
|
276 this.tests = null; |
|
277 this.openedWindows = null; |
|
278 } |
|
279 }, |
|
280 |
|
281 haltTests: function Tester_haltTests() { |
|
282 // Do not run any further tests |
|
283 this.currentTestIndex = this.tests.length - 1; |
|
284 this.repeat = 0; |
|
285 }, |
|
286 |
|
287 observe: function Tester_observe(aSubject, aTopic, aData) { |
|
288 if (!aTopic) { |
|
289 this.onConsoleMessage(aSubject); |
|
290 } else if (this.currentTest) { |
|
291 this.onDocumentCreated(aSubject); |
|
292 } |
|
293 }, |
|
294 |
|
295 onDocumentCreated: function Tester_onDocumentCreated(aWindow) { |
|
296 let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
297 .getInterface(Ci.nsIDOMWindowUtils); |
|
298 let outerID = utils.outerWindowID; |
|
299 let innerID = utils.currentInnerWindowID; |
|
300 |
|
301 if (!(outerID in this.openedWindows)) { |
|
302 this.openedWindows[outerID] = this.currentTest; |
|
303 } |
|
304 this.openedWindows[innerID] = this.currentTest; |
|
305 |
|
306 let url = aWindow.location.href || "about:blank"; |
|
307 this.openedURLs[outerID] = this.openedURLs[innerID] = url; |
|
308 }, |
|
309 |
|
310 onConsoleMessage: function Tester_onConsoleMessage(aConsoleMessage) { |
|
311 // Ignore empty messages. |
|
312 if (!aConsoleMessage.message) |
|
313 return; |
|
314 |
|
315 try { |
|
316 var msg = "Console message: " + aConsoleMessage.message; |
|
317 if (this.currentTest) |
|
318 this.currentTest.addResult(new testMessage(msg)); |
|
319 else |
|
320 this.dumper.dump("TEST-INFO | (browser-test.js) | " + msg.replace(/\n$/, "") + "\n"); |
|
321 } catch (ex) { |
|
322 // Swallow exception so we don't lead to another error being reported, |
|
323 // throwing us into an infinite loop |
|
324 } |
|
325 }, |
|
326 |
|
327 nextTest: Task.async(function*() { |
|
328 if (this.currentTest) { |
|
329 // Run cleanup functions for the current test before moving on to the |
|
330 // next one. |
|
331 let testScope = this.currentTest.scope; |
|
332 while (testScope.__cleanupFunctions.length > 0) { |
|
333 let func = testScope.__cleanupFunctions.shift(); |
|
334 try { |
|
335 yield func.apply(testScope); |
|
336 } |
|
337 catch (ex) { |
|
338 this.currentTest.addResult(new testResult(false, "Cleanup function threw an exception", ex, false)); |
|
339 } |
|
340 }; |
|
341 |
|
342 this.Promise.Debugging.flushUncaughtErrors(); |
|
343 |
|
344 let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) |
|
345 .getInterface(Ci.nsIDOMWindowUtils); |
|
346 if (winUtils.isTestControllingRefreshes) { |
|
347 this.currentTest.addResult(new testResult(false, "test left refresh driver under test control", "", false)); |
|
348 winUtils.restoreNormalRefresh(); |
|
349 } |
|
350 |
|
351 if (this.SimpleTest.isExpectingUncaughtException()) { |
|
352 this.currentTest.addResult(new testResult(false, "expectUncaughtException was called but no uncaught exception was detected!", "", false)); |
|
353 } |
|
354 |
|
355 Object.keys(window).forEach(function (prop) { |
|
356 if (parseInt(prop) == prop) { |
|
357 // This is a string which when parsed as an integer and then |
|
358 // stringified gives the original string. As in, this is in fact a |
|
359 // string representation of an integer, so an index into |
|
360 // window.frames. Skip those. |
|
361 return; |
|
362 } |
|
363 if (this._globalProperties.indexOf(prop) == -1) { |
|
364 this._globalProperties.push(prop); |
|
365 if (this._globalPropertyWhitelist.indexOf(prop) == -1) |
|
366 this.currentTest.addResult(new testResult(false, "leaked window property: " + prop, "", false)); |
|
367 } |
|
368 }, this); |
|
369 |
|
370 // Clear document.popupNode. The test could have set it to a custom value |
|
371 // for its own purposes, nulling it out it will go back to the default |
|
372 // behavior of returning the last opened popup. |
|
373 document.popupNode = null; |
|
374 |
|
375 // Notify a long running test problem if it didn't end up in a timeout. |
|
376 if (this.currentTest.unexpectedTimeouts && !this.currentTest.timedOut) { |
|
377 let msg = "This test exceeded the timeout threshold. It should be " + |
|
378 "rewritten or split up. If that's not possible, use " + |
|
379 "requestLongerTimeout(N), but only as a last resort."; |
|
380 this.currentTest.addResult(new testResult(false, msg, "", false)); |
|
381 } |
|
382 |
|
383 // If we're in a debug build, check assertion counts. This code |
|
384 // is similar to the code in TestRunner.testUnloaded in |
|
385 // TestRunner.js used for all other types of mochitests. |
|
386 let debugsvc = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); |
|
387 if (debugsvc.isDebugBuild) { |
|
388 let newAssertionCount = debugsvc.assertionCount; |
|
389 let numAsserts = newAssertionCount - this.lastAssertionCount; |
|
390 this.lastAssertionCount = newAssertionCount; |
|
391 |
|
392 let max = testScope.__expectedMaxAsserts; |
|
393 let min = testScope.__expectedMinAsserts; |
|
394 if (numAsserts > max) { |
|
395 let msg = "Assertion count " + numAsserts + |
|
396 " is greater than expected range " + |
|
397 min + "-" + max + " assertions."; |
|
398 // TEST-UNEXPECTED-FAIL (TEMPORARILY TEST-KNOWN-FAIL) |
|
399 //this.currentTest.addResult(new testResult(false, msg, "", false)); |
|
400 this.currentTest.addResult(new testResult(true, msg, "", true)); |
|
401 } else if (numAsserts < min) { |
|
402 let msg = "Assertion count " + numAsserts + |
|
403 " is less than expected range " + |
|
404 min + "-" + max + " assertions."; |
|
405 // TEST-UNEXPECTED-PASS |
|
406 this.currentTest.addResult(new testResult(false, msg, "", true)); |
|
407 } else if (numAsserts > 0) { |
|
408 let msg = "Assertion count " + numAsserts + |
|
409 " is within expected range " + |
|
410 min + "-" + max + " assertions."; |
|
411 // TEST-KNOWN-FAIL |
|
412 this.currentTest.addResult(new testResult(true, msg, "", true)); |
|
413 } |
|
414 } |
|
415 |
|
416 // Dump memory stats for main thread. |
|
417 if (Cc["@mozilla.org/xre/runtime;1"] |
|
418 .getService(Ci.nsIXULRuntime) |
|
419 .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) |
|
420 { |
|
421 this.MemoryStats.dump((l) => { this.dumper.dump(l + "\n"); }, |
|
422 this.currentTestIndex, |
|
423 this.currentTest.path, |
|
424 gConfig.dumpOutputDirectory, |
|
425 gConfig.dumpAboutMemoryAfterTest, |
|
426 gConfig.dumpDMDAfterTest); |
|
427 } |
|
428 |
|
429 // Note the test run time |
|
430 let time = Date.now() - this.lastStartTime; |
|
431 this.dumper.dump("INFO TEST-END | " + this.currentTest.path + " | finished in " + time + "ms\n"); |
|
432 this.currentTest.setDuration(time); |
|
433 |
|
434 if (this.runUntilFailure && this.currentTest.failCount > 0) { |
|
435 this.haltTests(); |
|
436 } |
|
437 |
|
438 // Restore original SimpleTest methods to avoid leaks. |
|
439 SIMPLETEST_OVERRIDES.forEach(m => { |
|
440 this.SimpleTest[m] = this.SimpleTestOriginal[m]; |
|
441 }); |
|
442 |
|
443 testScope.destroy(); |
|
444 this.currentTest.scope = null; |
|
445 } |
|
446 |
|
447 // Check the window state for the current test before moving to the next one. |
|
448 // This also causes us to check before starting any tests, since nextTest() |
|
449 // is invoked to start the tests. |
|
450 this.waitForWindowsState((function () { |
|
451 if (this.done) { |
|
452 // Uninitialize a few things explicitly so that they can clean up |
|
453 // frames and browser intentionally kept alive until shutdown to |
|
454 // eliminate false positives. |
|
455 if (gConfig.testRoot == "browser") { |
|
456 // Replace the document currently loaded in the browser's sidebar. |
|
457 // This will prevent false positives for tests that were the last |
|
458 // to touch the sidebar. They will thus not be blamed for leaking |
|
459 // a document. |
|
460 let sidebar = document.getElementById("sidebar"); |
|
461 sidebar.setAttribute("src", "data:text/html;charset=utf-8,"); |
|
462 sidebar.docShell.createAboutBlankContentViewer(null); |
|
463 sidebar.setAttribute("src", "about:blank"); |
|
464 |
|
465 // Do the same for the social sidebar. |
|
466 let socialSidebar = document.getElementById("social-sidebar-browser"); |
|
467 socialSidebar.setAttribute("src", "data:text/html;charset=utf-8,"); |
|
468 socialSidebar.docShell.createAboutBlankContentViewer(null); |
|
469 socialSidebar.setAttribute("src", "about:blank"); |
|
470 |
|
471 // Destroy BackgroundPageThumbs resources. |
|
472 let {BackgroundPageThumbs} = |
|
473 Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", {}); |
|
474 BackgroundPageThumbs._destroy(); |
|
475 |
|
476 BrowserNewTabPreloader.uninit(); |
|
477 CustomizationTabPreloader.uninit(); |
|
478 SocialFlyout.unload(); |
|
479 SocialShare.uninit(); |
|
480 TabView.uninit(); |
|
481 } |
|
482 |
|
483 // Simulate memory pressure so that we're forced to free more resources |
|
484 // and thus get rid of more false leaks like already terminated workers. |
|
485 Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize"); |
|
486 |
|
487 // Schedule GC and CC runs before finishing in order to detect |
|
488 // DOM windows leaked by our tests or the tested code. Note that we |
|
489 // use a shrinking GC so that the JS engine will discard JIT code and |
|
490 // JIT caches more aggressively. |
|
491 |
|
492 let checkForLeakedGlobalWindows = aCallback => { |
|
493 Cu.schedulePreciseShrinkingGC(() => { |
|
494 let analyzer = new CCAnalyzer(); |
|
495 analyzer.run(() => { |
|
496 let results = []; |
|
497 for (let obj of analyzer.find("nsGlobalWindow ")) { |
|
498 let m = obj.name.match(/^nsGlobalWindow #(\d+)/); |
|
499 if (m && m[1] in this.openedWindows) |
|
500 results.push({ name: obj.name, url: m[1] }); |
|
501 } |
|
502 aCallback(results); |
|
503 }); |
|
504 }); |
|
505 }; |
|
506 |
|
507 let reportLeaks = aResults => { |
|
508 for (let result of aResults) { |
|
509 let test = this.openedWindows[result.url]; |
|
510 let msg = "leaked until shutdown [" + result.name + |
|
511 " " + (this.openedURLs[result.url] || "NULL") + "]"; |
|
512 test.addResult(new testResult(false, msg, "", false)); |
|
513 } |
|
514 }; |
|
515 |
|
516 checkForLeakedGlobalWindows(aResults => { |
|
517 if (aResults.length == 0) { |
|
518 this.finish(); |
|
519 return; |
|
520 } |
|
521 // After the first check, if there are reported leaked windows, sleep |
|
522 // for a while, to allow off-main-thread work to complete and free up |
|
523 // main-thread objects. Then check again. |
|
524 setTimeout(() => { |
|
525 checkForLeakedGlobalWindows(aResults => { |
|
526 reportLeaks(aResults); |
|
527 this.finish(); |
|
528 }); |
|
529 }, 1000); |
|
530 }); |
|
531 |
|
532 return; |
|
533 } |
|
534 |
|
535 this.currentTestIndex++; |
|
536 this.execTest(); |
|
537 }).bind(this)); |
|
538 }), |
|
539 |
|
540 execTest: function Tester_execTest() { |
|
541 this.dumper.dump("TEST-START | " + this.currentTest.path + "\n"); |
|
542 |
|
543 this.SimpleTest.reset(); |
|
544 |
|
545 // Load the tests into a testscope |
|
546 let currentScope = this.currentTest.scope = new testScope(this, this.currentTest); |
|
547 let currentTest = this.currentTest; |
|
548 |
|
549 // Import utils in the test scope. |
|
550 this.currentTest.scope.EventUtils = this.EventUtils; |
|
551 this.currentTest.scope.SimpleTest = this.SimpleTest; |
|
552 this.currentTest.scope.gTestPath = this.currentTest.path; |
|
553 this.currentTest.scope.Task = this.Task; |
|
554 this.currentTest.scope.Promise = this.Promise; |
|
555 // Pass a custom report function for mochitest style reporting. |
|
556 this.currentTest.scope.Assert = new this.Assert(function(err, message, stack) { |
|
557 let res; |
|
558 if (err) { |
|
559 res = new testResult(false, err.message, err.stack, false, err.stack); |
|
560 } else { |
|
561 res = new testResult(true, message, "", false, stack); |
|
562 } |
|
563 currentTest.addResult(res); |
|
564 }); |
|
565 |
|
566 // Allow Assert.jsm methods to be tacked to the current scope. |
|
567 this.currentTest.scope.export_assertions = function() { |
|
568 for (let func in this.Assert) { |
|
569 this[func] = this.Assert[func].bind(this.Assert); |
|
570 } |
|
571 }; |
|
572 |
|
573 // Override SimpleTest methods with ours. |
|
574 SIMPLETEST_OVERRIDES.forEach(function(m) { |
|
575 this.SimpleTest[m] = this[m]; |
|
576 }, this.currentTest.scope); |
|
577 |
|
578 //load the tools to work with chrome .jar and remote |
|
579 try { |
|
580 this._scriptLoader.loadSubScript("chrome://mochikit/content/chrome-harness.js", this.currentTest.scope); |
|
581 } catch (ex) { /* no chrome-harness tools */ } |
|
582 |
|
583 // Import head.js script if it exists. |
|
584 var currentTestDirPath = |
|
585 this.currentTest.path.substr(0, this.currentTest.path.lastIndexOf("/")); |
|
586 var headPath = currentTestDirPath + "/head.js"; |
|
587 try { |
|
588 this._scriptLoader.loadSubScript(headPath, this.currentTest.scope); |
|
589 } catch (ex) { |
|
590 // Ignore if no head.js exists, but report all other errors. Note this |
|
591 // will also ignore an existing head.js attempting to import a missing |
|
592 // module - see bug 755558 for why this strategy is preferred anyway. |
|
593 if (ex.toString() != 'Error opening input stream (invalid filename?)') { |
|
594 this.currentTest.addResult(new testResult(false, "head.js import threw an exception", ex, false)); |
|
595 } |
|
596 } |
|
597 |
|
598 // Import the test script. |
|
599 try { |
|
600 this._scriptLoader.loadSubScript(this.currentTest.path, |
|
601 this.currentTest.scope); |
|
602 this.Promise.Debugging.flushUncaughtErrors(); |
|
603 // Run the test |
|
604 this.lastStartTime = Date.now(); |
|
605 if (this.currentTest.scope.__tasks) { |
|
606 // This test consists of tasks, added via the `add_task()` API. |
|
607 if ("test" in this.currentTest.scope) { |
|
608 throw "Cannot run both a add_task test and a normal test at the same time."; |
|
609 } |
|
610 this.Task.spawn(function() { |
|
611 let task; |
|
612 while ((task = this.__tasks.shift())) { |
|
613 this.SimpleTest.info("Entering test " + task.name); |
|
614 try { |
|
615 yield task(); |
|
616 } catch (ex) { |
|
617 let isExpected = !!this.SimpleTest.isExpectingUncaughtException(); |
|
618 let stack = (typeof ex == "object" && "stack" in ex)?ex.stack:null; |
|
619 let name = "Uncaught exception"; |
|
620 let result = new testResult(isExpected, name, ex, false, stack); |
|
621 currentTest.addResult(result); |
|
622 } |
|
623 this.Promise.Debugging.flushUncaughtErrors(); |
|
624 this.SimpleTest.info("Leaving test " + task.name); |
|
625 } |
|
626 this.finish(); |
|
627 }.bind(currentScope)); |
|
628 } else if ("generatorTest" in this.currentTest.scope) { |
|
629 if ("test" in this.currentTest.scope) { |
|
630 throw "Cannot run both a generator test and a normal test at the same time."; |
|
631 } |
|
632 |
|
633 // This test is a generator. It will not finish immediately. |
|
634 this.currentTest.scope.waitForExplicitFinish(); |
|
635 var result = this.currentTest.scope.generatorTest(); |
|
636 this.currentTest.scope.__generator = result; |
|
637 result.next(); |
|
638 } else { |
|
639 this.currentTest.scope.test(); |
|
640 } |
|
641 } catch (ex) { |
|
642 let isExpected = !!this.SimpleTest.isExpectingUncaughtException(); |
|
643 if (!this.SimpleTest.isIgnoringAllUncaughtExceptions()) { |
|
644 this.currentTest.addResult(new testResult(isExpected, "Exception thrown", ex, false)); |
|
645 this.SimpleTest.expectUncaughtException(false); |
|
646 } else { |
|
647 this.currentTest.addResult(new testMessage("Exception thrown: " + ex)); |
|
648 } |
|
649 this.currentTest.scope.finish(); |
|
650 } |
|
651 |
|
652 // If the test ran synchronously, move to the next test, otherwise the test |
|
653 // will trigger the next test when it is done. |
|
654 if (this.currentTest.scope.__done) { |
|
655 this.nextTest(); |
|
656 } |
|
657 else { |
|
658 var self = this; |
|
659 this.currentTest.scope.__waitTimer = setTimeout(function timeoutFn() { |
|
660 if (--self.currentTest.scope.__timeoutFactor > 0) { |
|
661 // We were asked to wait a bit longer. |
|
662 self.currentTest.scope.info( |
|
663 "Longer timeout required, waiting longer... Remaining timeouts: " + |
|
664 self.currentTest.scope.__timeoutFactor); |
|
665 self.currentTest.scope.__waitTimer = |
|
666 setTimeout(timeoutFn, gTimeoutSeconds * 1000); |
|
667 return; |
|
668 } |
|
669 |
|
670 // If the test is taking longer than expected, but it's not hanging, |
|
671 // mark the fact, but let the test continue. At the end of the test, |
|
672 // if it didn't timeout, we will notify the problem through an error. |
|
673 // To figure whether it's an actual hang, compare the time of the last |
|
674 // result or message to half of the timeout time. |
|
675 // Though, to protect against infinite loops, limit the number of times |
|
676 // we allow the test to proceed. |
|
677 const MAX_UNEXPECTED_TIMEOUTS = 10; |
|
678 if (Date.now() - self.currentTest.lastOutputTime < (gTimeoutSeconds / 2) * 1000 && |
|
679 ++self.currentTest.unexpectedTimeouts <= MAX_UNEXPECTED_TIMEOUTS) { |
|
680 self.currentTest.scope.__waitTimer = |
|
681 setTimeout(timeoutFn, gTimeoutSeconds * 1000); |
|
682 return; |
|
683 } |
|
684 |
|
685 self.currentTest.addResult(new testResult(false, "Test timed out", "", false)); |
|
686 self.currentTest.timedOut = true; |
|
687 self.currentTest.scope.__waitTimer = null; |
|
688 self.nextTest(); |
|
689 }, gTimeoutSeconds * 1000); |
|
690 } |
|
691 }, |
|
692 |
|
693 QueryInterface: function(aIID) { |
|
694 if (aIID.equals(Ci.nsIConsoleListener) || |
|
695 aIID.equals(Ci.nsISupports)) |
|
696 return this; |
|
697 |
|
698 throw Components.results.NS_ERROR_NO_INTERFACE; |
|
699 } |
|
700 }; |
|
701 |
|
702 function testResult(aCondition, aName, aDiag, aIsTodo, aStack) { |
|
703 this.msg = aName || ""; |
|
704 |
|
705 this.info = false; |
|
706 this.pass = !!aCondition; |
|
707 this.todo = aIsTodo; |
|
708 |
|
709 if (this.pass) { |
|
710 if (aIsTodo) |
|
711 this.result = "TEST-KNOWN-FAIL"; |
|
712 else |
|
713 this.result = "TEST-PASS"; |
|
714 } else { |
|
715 if (aDiag) { |
|
716 if (typeof aDiag == "object" && "fileName" in aDiag) { |
|
717 // we have an exception - print filename and linenumber information |
|
718 this.msg += " at " + aDiag.fileName + ":" + aDiag.lineNumber; |
|
719 } |
|
720 this.msg += " - " + aDiag; |
|
721 } |
|
722 if (aStack) { |
|
723 this.msg += "\nStack trace:\n"; |
|
724 var frame = aStack; |
|
725 while (frame) { |
|
726 this.msg += " " + frame + "\n"; |
|
727 frame = frame.caller; |
|
728 } |
|
729 } |
|
730 if (aIsTodo) |
|
731 this.result = "TEST-UNEXPECTED-PASS"; |
|
732 else |
|
733 this.result = "TEST-UNEXPECTED-FAIL"; |
|
734 |
|
735 if (gConfig.debugOnFailure) { |
|
736 // You've hit this line because you requested to break into the |
|
737 // debugger upon a testcase failure on your test run. |
|
738 debugger; |
|
739 } |
|
740 } |
|
741 } |
|
742 |
|
743 function testMessage(aName) { |
|
744 this.msg = aName || ""; |
|
745 this.info = true; |
|
746 this.result = "TEST-INFO"; |
|
747 } |
|
748 |
|
749 // Need to be careful adding properties to this object, since its properties |
|
750 // cannot conflict with global variables used in tests. |
|
751 function testScope(aTester, aTest) { |
|
752 this.__tester = aTester; |
|
753 |
|
754 var self = this; |
|
755 this.ok = function test_ok(condition, name, diag, stack) { |
|
756 aTest.addResult(new testResult(condition, name, diag, false, |
|
757 stack ? stack : Components.stack.caller)); |
|
758 }; |
|
759 this.is = function test_is(a, b, name) { |
|
760 self.ok(a == b, name, "Got " + a + ", expected " + b, false, |
|
761 Components.stack.caller); |
|
762 }; |
|
763 this.isnot = function test_isnot(a, b, name) { |
|
764 self.ok(a != b, name, "Didn't expect " + a + ", but got it", false, |
|
765 Components.stack.caller); |
|
766 }; |
|
767 this.ise = function test_ise(a, b, name) { |
|
768 self.ok(a === b, name, "Got " + a + ", strictly expected " + b, false, |
|
769 Components.stack.caller); |
|
770 }; |
|
771 this.todo = function test_todo(condition, name, diag, stack) { |
|
772 aTest.addResult(new testResult(!condition, name, diag, true, |
|
773 stack ? stack : Components.stack.caller)); |
|
774 }; |
|
775 this.todo_is = function test_todo_is(a, b, name) { |
|
776 self.todo(a == b, name, "Got " + a + ", expected " + b, |
|
777 Components.stack.caller); |
|
778 }; |
|
779 this.todo_isnot = function test_todo_isnot(a, b, name) { |
|
780 self.todo(a != b, name, "Didn't expect " + a + ", but got it", |
|
781 Components.stack.caller); |
|
782 }; |
|
783 this.info = function test_info(name) { |
|
784 aTest.addResult(new testMessage(name)); |
|
785 }; |
|
786 |
|
787 this.executeSoon = function test_executeSoon(func) { |
|
788 Services.tm.mainThread.dispatch({ |
|
789 run: function() { |
|
790 func(); |
|
791 } |
|
792 }, Ci.nsIThread.DISPATCH_NORMAL); |
|
793 }; |
|
794 |
|
795 this.nextStep = function test_nextStep(arg) { |
|
796 if (self.__done) { |
|
797 aTest.addResult(new testResult(false, "nextStep was called too many times", "", false)); |
|
798 return; |
|
799 } |
|
800 |
|
801 if (!self.__generator) { |
|
802 aTest.addResult(new testResult(false, "nextStep called with no generator", "", false)); |
|
803 self.finish(); |
|
804 return; |
|
805 } |
|
806 |
|
807 try { |
|
808 self.__generator.send(arg); |
|
809 } catch (ex if ex instanceof StopIteration) { |
|
810 // StopIteration means test is finished. |
|
811 self.finish(); |
|
812 } catch (ex) { |
|
813 var isExpected = !!self.SimpleTest.isExpectingUncaughtException(); |
|
814 if (!self.SimpleTest.isIgnoringAllUncaughtExceptions()) { |
|
815 aTest.addResult(new testResult(isExpected, "Exception thrown", ex, false)); |
|
816 self.SimpleTest.expectUncaughtException(false); |
|
817 } else { |
|
818 aTest.addResult(new testMessage("Exception thrown: " + ex)); |
|
819 } |
|
820 self.finish(); |
|
821 } |
|
822 }; |
|
823 |
|
824 this.waitForExplicitFinish = function test_waitForExplicitFinish() { |
|
825 self.__done = false; |
|
826 }; |
|
827 |
|
828 this.waitForFocus = function test_waitForFocus(callback, targetWindow, expectBlankPage) { |
|
829 self.SimpleTest.waitForFocus(callback, targetWindow, expectBlankPage); |
|
830 }; |
|
831 |
|
832 this.waitForClipboard = function test_waitForClipboard(expected, setup, success, failure, flavor) { |
|
833 self.SimpleTest.waitForClipboard(expected, setup, success, failure, flavor); |
|
834 }; |
|
835 |
|
836 this.registerCleanupFunction = function test_registerCleanupFunction(aFunction) { |
|
837 self.__cleanupFunctions.push(aFunction); |
|
838 }; |
|
839 |
|
840 this.requestLongerTimeout = function test_requestLongerTimeout(aFactor) { |
|
841 self.__timeoutFactor = aFactor; |
|
842 }; |
|
843 |
|
844 this.copyToProfile = function test_copyToProfile(filename) { |
|
845 self.SimpleTest.copyToProfile(filename); |
|
846 }; |
|
847 |
|
848 this.expectUncaughtException = function test_expectUncaughtException(aExpecting) { |
|
849 self.SimpleTest.expectUncaughtException(aExpecting); |
|
850 }; |
|
851 |
|
852 this.ignoreAllUncaughtExceptions = function test_ignoreAllUncaughtExceptions(aIgnoring) { |
|
853 self.SimpleTest.ignoreAllUncaughtExceptions(aIgnoring); |
|
854 }; |
|
855 |
|
856 this.expectAssertions = function test_expectAssertions(aMin, aMax) { |
|
857 let min = aMin; |
|
858 let max = aMax; |
|
859 if (typeof(max) == "undefined") { |
|
860 max = min; |
|
861 } |
|
862 if (typeof(min) != "number" || typeof(max) != "number" || |
|
863 min < 0 || max < min) { |
|
864 throw "bad parameter to expectAssertions"; |
|
865 } |
|
866 self.__expectedMinAsserts = min; |
|
867 self.__expectedMaxAsserts = max; |
|
868 }; |
|
869 |
|
870 this.finish = function test_finish() { |
|
871 self.__done = true; |
|
872 if (self.__waitTimer) { |
|
873 self.executeSoon(function() { |
|
874 if (self.__done && self.__waitTimer) { |
|
875 clearTimeout(self.__waitTimer); |
|
876 self.__waitTimer = null; |
|
877 self.__tester.nextTest(); |
|
878 } |
|
879 }); |
|
880 } |
|
881 }; |
|
882 } |
|
883 testScope.prototype = { |
|
884 __done: true, |
|
885 __generator: null, |
|
886 __tasks: null, |
|
887 __waitTimer: null, |
|
888 __cleanupFunctions: [], |
|
889 __timeoutFactor: 1, |
|
890 __expectedMinAsserts: 0, |
|
891 __expectedMaxAsserts: 0, |
|
892 |
|
893 EventUtils: {}, |
|
894 SimpleTest: {}, |
|
895 Task: null, |
|
896 Promise: null, |
|
897 Assert: null, |
|
898 |
|
899 /** |
|
900 * Add a test function which is a Task function. |
|
901 * |
|
902 * Task functions are functions fed into Task.jsm's Task.spawn(). They are |
|
903 * generators that emit promises. |
|
904 * |
|
905 * If an exception is thrown, an assertion fails, or if a rejected |
|
906 * promise is yielded, the test function aborts immediately and the test is |
|
907 * reported as a failure. Execution continues with the next test function. |
|
908 * |
|
909 * To trigger premature (but successful) termination of the function, simply |
|
910 * return or throw a Task.Result instance. |
|
911 * |
|
912 * Example usage: |
|
913 * |
|
914 * add_task(function test() { |
|
915 * let result = yield Promise.resolve(true); |
|
916 * |
|
917 * ok(result); |
|
918 * |
|
919 * let secondary = yield someFunctionThatReturnsAPromise(result); |
|
920 * is(secondary, "expected value"); |
|
921 * }); |
|
922 * |
|
923 * add_task(function test_early_return() { |
|
924 * let result = yield somethingThatReturnsAPromise(); |
|
925 * |
|
926 * if (!result) { |
|
927 * // Test is ended immediately, with success. |
|
928 * return; |
|
929 * } |
|
930 * |
|
931 * is(result, "foo"); |
|
932 * }); |
|
933 */ |
|
934 add_task: function(aFunction) { |
|
935 if (!this.__tasks) { |
|
936 this.waitForExplicitFinish(); |
|
937 this.__tasks = []; |
|
938 } |
|
939 this.__tasks.push(aFunction.bind(this)); |
|
940 }, |
|
941 |
|
942 destroy: function test_destroy() { |
|
943 for (let prop in this) |
|
944 delete this[prop]; |
|
945 } |
|
946 }; |