browser-test.js (67183B)
1 /* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */ 2 3 /* import-globals-from chrome-harness.js */ 4 /* import-globals-from mochitest-e10s-utils.js */ 5 6 // Test timeout (seconds) 7 var gTimeoutSeconds = Services.prefs.getIntPref( 8 "testing.browserTestHarness.timeout" 9 ); 10 var gConfig; 11 12 var { AppConstants } = ChromeUtils.importESModule( 13 "resource://gre/modules/AppConstants.sys.mjs" 14 ); 15 16 ChromeUtils.defineESModuleGetters(this, { 17 AddonManager: "resource://gre/modules/AddonManager.sys.mjs", 18 }); 19 20 // saveScopeVariablesAsJSON is an experimental feature which will attempt to 21 // save the values of all variables when a test fails (ie an assert fails). 22 // It relies on the DevTools Debugger API to inspect variables and for the 23 // moment will only be enabled if either: 24 // - the preference devtools.testing.testScopes is set to true 25 // - the environment variable MOZ_DEVTOOLS_TEST_SCOPES is set to 1 26 // 27 // For instance for a try push, you can enable it with 28 // `./mach try fuzzy --env MOZ_DEVTOOLS_TEST_SCOPES=1` 29 ChromeUtils.defineLazyGetter(this, "saveScopeVariablesAsJSON", () => { 30 const isDevToolsTestScopesEnabled = 31 Services.prefs.getBoolPref("devtools.testing.testScopes", false) || 32 Services.env.get("MOZ_DEVTOOLS_TEST_SCOPES") === "1"; 33 return () => { 34 if (isDevToolsTestScopesEnabled) { 35 ChromeUtils.importESModule( 36 "resource://devtools/shared/test-helpers/dump-scope.sys.mjs", 37 { global: "devtools" } 38 ).dumpScope(); 39 } 40 }; 41 }); 42 43 const SIMPLETEST_OVERRIDES = [ 44 "ok", 45 "record", 46 "is", 47 "isnot", 48 "todo", 49 "todo_is", 50 "todo_isnot", 51 "info", 52 "expectAssertions", 53 "requestCompleteLog", 54 ]; 55 56 setTimeout(testInit, 0); 57 58 var TabDestroyObserver = { 59 outstanding: new Set(), 60 promiseResolver: null, 61 62 init() { 63 Services.obs.addObserver(this, "message-manager-close"); 64 Services.obs.addObserver(this, "message-manager-disconnect"); 65 }, 66 67 destroy() { 68 Services.obs.removeObserver(this, "message-manager-close"); 69 Services.obs.removeObserver(this, "message-manager-disconnect"); 70 }, 71 72 observe(subject, topic) { 73 if (topic == "message-manager-close") { 74 this.outstanding.add(subject); 75 } else if (topic == "message-manager-disconnect") { 76 this.outstanding.delete(subject); 77 if (!this.outstanding.size && this.promiseResolver) { 78 this.promiseResolver(); 79 } 80 } 81 }, 82 83 wait() { 84 if (!this.outstanding.size) { 85 return Promise.resolve(); 86 } 87 88 return new Promise(resolve => { 89 this.promiseResolver = resolve; 90 }); 91 }, 92 }; 93 94 function testInit() { 95 gConfig = readConfig(); 96 97 if (gConfig.testRoot == "browser") { 98 // Make sure to launch the test harness for the first opened window only 99 if (Services.prefs.prefHasUserValue("testing.browserTestHarness.running")) { 100 return; 101 } 102 Services.prefs.setBoolPref("testing.browserTestHarness.running", true); 103 104 var sstring = Cc["@mozilla.org/supports-string;1"].createInstance( 105 Ci.nsISupportsString 106 ); 107 sstring.data = location.search; 108 109 Services.ww.openWindow( 110 window, 111 "chrome://mochikit/content/browser-harness.xhtml", 112 "browserTest", 113 "chrome,centerscreen,dialog=no,resizable,titlebar,toolbar=no,width=800,height=600", 114 sstring 115 ); 116 } else { 117 // This code allows us to redirect without requiring specialpowers for chrome and a11y tests. 118 let messageHandler = function (m) { 119 // eslint-disable-next-line no-undef 120 messageManager.removeMessageListener("chromeEvent", messageHandler); 121 var url = m.json.data; 122 123 // Window is the [ChromeWindow] for messageManager, so we need content.window 124 // Currently chrome tests are run in a content window instead of a ChromeWindow 125 // eslint-disable-next-line no-undef 126 var webNav = content.window.docShell.QueryInterface(Ci.nsIWebNavigation); 127 let loadURIOptions = { 128 triggeringPrincipal: 129 Services.scriptSecurityManager.getSystemPrincipal(), 130 }; 131 webNav.fixupAndLoadURIString(url, loadURIOptions); 132 }; 133 134 var listener = 135 '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);'; 136 // eslint-disable-next-line no-undef 137 messageManager.addMessageListener("chromeEvent", messageHandler); 138 // eslint-disable-next-line no-undef 139 messageManager.loadFrameScript(listener, true); 140 } 141 if (gConfig.e10s) { 142 e10s_init(); 143 144 let processCount = Services.prefs.getIntPref("dom.ipc.processCount", 1); 145 if (processCount > 1) { 146 // Currently starting a content process is slow, to avoid timeouts, let's 147 // keep alive content processes. 148 Services.prefs.setIntPref("dom.ipc.keepProcessesAlive.web", processCount); 149 } 150 151 Services.mm.loadFrameScript( 152 "chrome://mochikit/content/shutdown-leaks-collector.js", 153 true 154 ); 155 } else { 156 // In non-e10s, only run the ShutdownLeaksCollector in the parent process. 157 ChromeUtils.importESModule( 158 "chrome://mochikit/content/ShutdownLeaksCollector.sys.mjs" 159 ); 160 } 161 } 162 163 function isGenerator(value) { 164 return value && typeof value === "object" && typeof value.next === "function"; 165 } 166 167 function Tester(aTests, structuredLogger, aCallback) { 168 this.structuredLogger = structuredLogger; 169 this.tests = aTests; 170 this.callback = aCallback; 171 172 this._scriptLoader = Services.scriptloader; 173 this.EventUtils = {}; 174 this._scriptLoader.loadSubScript( 175 "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", 176 this.EventUtils 177 ); 178 179 // Make sure our SpecialPowers actor is instantiated, in case it was 180 // registered after our DOMWindowCreated event was fired (which it 181 // most likely was). 182 void window.windowGlobalChild.getActor("SpecialPowers"); 183 184 var simpleTestScope = {}; 185 this._scriptLoader.loadSubScript( 186 "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", 187 simpleTestScope 188 ); 189 this._scriptLoader.loadSubScript( 190 "chrome://mochikit/content/tests/SimpleTest/MemoryStats.js", 191 simpleTestScope 192 ); 193 this._scriptLoader.loadSubScript( 194 "chrome://mochikit/content/chrome-harness.js", 195 simpleTestScope 196 ); 197 this.SimpleTest = simpleTestScope.SimpleTest; 198 199 window.SpecialPowers.SimpleTest = this.SimpleTest; 200 window.SpecialPowers.setAsDefaultAssertHandler(); 201 202 this._scriptLoader.loadSubScript( 203 "chrome://mochikit/content/tests/SimpleTest/AccessibilityUtils.js", 204 // AccessibilityUtils are integrated with EventUtils to perform additional 205 // accessibility checks for certain user interactions (clicks, etc). Load 206 // them into the EventUtils scope here. 207 this.EventUtils 208 ); 209 this.AccessibilityUtils = this.EventUtils.AccessibilityUtils; 210 211 this.AccessibilityUtils.init(this.SimpleTest); 212 213 var extensionUtilsScope = { 214 registerCleanupFunction: fn => { 215 this.currentTest.scope.registerCleanupFunction(fn); 216 }, 217 }; 218 extensionUtilsScope.SimpleTest = this.SimpleTest; 219 this._scriptLoader.loadSubScript( 220 "chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js", 221 extensionUtilsScope 222 ); 223 this.ExtensionTestUtils = extensionUtilsScope.ExtensionTestUtils; 224 225 this.SimpleTest.harnessParameters = gConfig; 226 227 this.MemoryStats = simpleTestScope.MemoryStats; 228 this.ContentTask = ChromeUtils.importESModule( 229 "resource://testing-common/ContentTask.sys.mjs" 230 ).ContentTask; 231 this.BrowserTestUtils = ChromeUtils.importESModule( 232 "resource://testing-common/BrowserTestUtils.sys.mjs" 233 ).BrowserTestUtils; 234 this.TestUtils = ChromeUtils.importESModule( 235 "resource://testing-common/TestUtils.sys.mjs" 236 ).TestUtils; 237 this.PromiseTestUtils = ChromeUtils.importESModule( 238 "resource://testing-common/PromiseTestUtils.sys.mjs" 239 ).PromiseTestUtils; 240 this.Assert = ChromeUtils.importESModule( 241 "resource://testing-common/Assert.sys.mjs" 242 ).Assert; 243 this.PerTestCoverageUtils = ChromeUtils.importESModule( 244 "resource://testing-common/PerTestCoverageUtils.sys.mjs" 245 ).PerTestCoverageUtils; 246 247 this.PromiseTestUtils.init(); 248 249 this.SimpleTestOriginal = {}; 250 SIMPLETEST_OVERRIDES.forEach(m => { 251 this.SimpleTestOriginal[m] = this.SimpleTest[m]; 252 }); 253 254 this._coverageCollector = null; 255 256 const { XPCOMUtils } = ChromeUtils.importESModule( 257 "resource://gre/modules/XPCOMUtils.sys.mjs" 258 ); 259 260 // Avoid failing tests when XPCOMUtils.defineLazyScriptGetter is used. 261 XPCOMUtils.overrideScriptLoaderForTests({ 262 loadSubScript: (url, obj) => { 263 let before = Object.keys(window); 264 try { 265 return this._scriptLoader.loadSubScript(url, obj); 266 } finally { 267 for (let property of Object.keys(window)) { 268 if ( 269 !before.includes(property) && 270 !this._globalProperties.includes(property) 271 ) { 272 this._globalProperties.push(property); 273 this.SimpleTest.info( 274 `Global property added while loading ${url}: ${property}` 275 ); 276 } 277 } 278 } 279 }, 280 loadSubScriptWithOptions: this._scriptLoader.loadSubScriptWithOptions.bind( 281 this._scriptLoader 282 ), 283 }); 284 285 // ensure the mouse is reset before each test run 286 if (Services.env.exists("MOZ_AUTOMATION")) { 287 this.EventUtils.synthesizeNativeMouseEvent({ 288 type: "mousemove", 289 screenX: 1000, 290 screenY: 10, 291 }); 292 } 293 } 294 Tester.prototype = { 295 EventUtils: {}, 296 AccessibilityUtils: {}, 297 SimpleTest: {}, 298 ContentTask: null, 299 ExtensionTestUtils: null, 300 Assert: null, 301 302 repeat: 0, 303 a11y_checks: false, 304 runUntilFailure: false, 305 checker: null, 306 currentTestIndex: -1, 307 lastStartTime: null, 308 lastStartTimestamp: null, 309 lastAssertionCount: 0, 310 failuresFromInitialWindowState: 0, 311 312 get currentTest() { 313 return this.tests[this.currentTestIndex]; 314 }, 315 get done() { 316 return this.currentTestIndex == this.tests.length - 1 && this.repeat <= 0; 317 }, 318 319 start: function Tester_start() { 320 TabDestroyObserver.init(); 321 322 // if testOnLoad was not called, then gConfig is not defined 323 if (!gConfig) { 324 gConfig = readConfig(); 325 } 326 327 if (gConfig.runUntilFailure) { 328 this.runUntilFailure = true; 329 } 330 331 if (gConfig.a11y_checks != undefined) { 332 this.a11y_checks = gConfig.a11y_checks; 333 } 334 335 if (gConfig.repeat) { 336 this.repeat = gConfig.repeat; 337 } 338 339 if (gConfig.jscovDirPrefix) { 340 let coveragePath = gConfig.jscovDirPrefix; 341 let { CoverageCollector } = ChromeUtils.importESModule( 342 "resource://testing-common/CoverageUtils.sys.mjs" 343 ); 344 this._coverageCollector = new CoverageCollector(coveragePath); 345 } 346 347 if (gConfig.debugger || gConfig.debuggerInteractive || gConfig.jsdebugger) { 348 gTimeoutSeconds = 24 * 60 * 60 * 1000; // 24 hours 349 } 350 351 this.structuredLogger.info("*** Start BrowserChrome Test Results ***"); 352 Services.console.registerListener(this); 353 this._globalProperties = Object.keys(window); 354 this._globalPropertyWhitelist = [ 355 "navigator", 356 "constructor", 357 "top", 358 "Application", 359 "__SS_tabsToRestore", 360 "__SSi", 361 "webConsoleCommandController", 362 // Thunderbird 363 "MailMigrator", 364 "SearchIntegration", 365 // lit 366 "reactiveElementVersions", 367 "litHtmlVersions", 368 "litElementVersions", 369 ]; 370 371 this._repeatingTimers = this._getRepeatingTimers(); 372 373 this.PerTestCoverageUtils.beforeTestSync(); 374 375 if (this.tests.length) { 376 this.waitForWindowsReady().then(() => { 377 this.nextTest(); 378 }); 379 } else { 380 this.finish(); 381 } 382 }, 383 384 _getRepeatingTimers() { 385 const kNonRepeatingTimerTypes = [ 386 Ci.nsITimer.TYPE_ONE_SHOT, 387 Ci.nsITimer.TYPE_ONE_SHOT_LOW_PRIORITY, 388 ]; 389 return Cc["@mozilla.org/timer-manager;1"] 390 .getService(Ci.nsITimerManager) 391 .getTimers() 392 .filter(t => !kNonRepeatingTimerTypes.includes(t.type)); 393 }, 394 395 async waitForWindowsReady() { 396 await this.setupDefaultTheme(); 397 await new Promise(resolve => 398 this.waitForGraphicsTestWindowToBeGone(resolve) 399 ); 400 await this.promiseMainWindowReady(); 401 }, 402 403 async promiseMainWindowReady() { 404 if (window.gBrowserInit) { 405 await window.gBrowserInit.idleTasksFinished.promise; 406 } 407 }, 408 409 async setupDefaultTheme() { 410 // Developer Edition enables the wrong theme by default. Make sure 411 // the ordinary default theme is enabled. 412 let theme = await AddonManager.getAddonByID("default-theme@mozilla.org"); 413 await theme.enable(); 414 }, 415 416 waitForGraphicsTestWindowToBeGone(aCallback) { 417 for (let win of Services.wm.getEnumerator(null)) { 418 if ( 419 win != window && 420 !win.closed && 421 win.document.documentURI == 422 "chrome://gfxsanity/content/sanityparent.html" 423 ) { 424 this.BrowserTestUtils.domWindowClosed(win).then(aCallback); 425 return; 426 } 427 } 428 // graphics test window is already gone, just call callback immediately 429 aCallback(); 430 }, 431 432 checkWindowsState: function Tester_checkWindowsState() { 433 let timedOut = this.currentTest && this.currentTest.timedOut; 434 // eslint-disable-next-line no-nested-ternary 435 let baseMsg = timedOut 436 ? "Found a {elt} after previous test timed out" 437 : this.currentTest 438 ? "Found an unexpected {elt} at the end of test run" 439 : "Found an unexpected {elt}"; 440 441 // Clean up the Firefox window. 442 // But not the Thunderbird window, it doesn't have these things! 443 if (AppConstants.MOZ_APP_NAME != "thunderbird") { 444 // Remove stale tabs 445 if (this.currentTest && window.gBrowser && gBrowser.tabs.length > 1) { 446 let lastURI = ""; 447 let lastURIcount = 0; 448 while (gBrowser.tabs.length > 1) { 449 let lastTab = gBrowser.tabs[gBrowser.tabs.length - 1]; 450 if (!lastTab.closing) { 451 // Report the stale tab as an error only when they're not closing. 452 // Tests can finish without waiting for the closing tabs. 453 if (lastURI != lastTab.linkedBrowser.currentURI.spec) { 454 lastURI = lastTab.linkedBrowser.currentURI.spec; 455 } else { 456 lastURIcount++; 457 if (lastURIcount >= 3) { 458 this.currentTest.addResult( 459 new testResult({ 460 name: "terminating browser early - unable to close tabs; skipping remaining tests in folder", 461 allowFailure: this.currentTest.allowFailure, 462 }) 463 ); 464 this.finish(); 465 } 466 } 467 this.currentTest.addResult( 468 new testResult({ 469 name: 470 baseMsg.replace("{elt}", "tab") + 471 ": " + 472 lastTab.linkedBrowser.currentURI.spec, 473 allowFailure: this.currentTest.allowFailure, 474 }) 475 ); 476 } 477 gBrowser.removeTab(lastTab); 478 } 479 } 480 481 // Tests shouldn't leave sidebars open 482 if (this.currentTest) { 483 this.currentTest.addResult( 484 new testMessage("checking for open sidebars") 485 ); 486 } else { 487 this.structuredLogger.info("checking for open sidebars"); 488 } 489 const sidebarContainer = document.getElementById("sidebar-box"); 490 if (!sidebarContainer.hidden) { 491 window.SidebarController.hide({ dismissPanel: true }); 492 this.currentTest.addResult( 493 new testResult({ 494 name: baseMsg.replace("{elt}", "open sidebar"), 495 allowFailure: this.currentTest.allowFailure, 496 }) 497 ); 498 } 499 } 500 501 // Remove stale windows 502 if (this.currentTest) { 503 this.currentTest.addResult(new testMessage("checking window state")); 504 } else { 505 this.structuredLogger.info("checking window state"); 506 } 507 for (let win of Services.wm.getEnumerator(null)) { 508 let type = win.document.documentElement.getAttribute("windowtype"); 509 if ( 510 win != window && 511 !win.closed && 512 win.document.documentElement.getAttribute("id") != 513 "browserTestHarness" && 514 type != "devtools:webconsole" 515 ) { 516 switch (type) { 517 case "navigator:browser": 518 type = "browser window"; 519 break; 520 case "mail:3pane": 521 type = "mail window"; 522 break; 523 case null: 524 type = 525 "unknown window with document URI: " + 526 win.document.documentURI + 527 " and title: " + 528 win.document.title; 529 break; 530 } 531 let msg = baseMsg.replace("{elt}", type); 532 if (this.currentTest) { 533 this.currentTest.addResult( 534 new testResult({ 535 name: msg, 536 allowFailure: this.currentTest.allowFailure, 537 }) 538 ); 539 } else { 540 this.failuresFromInitialWindowState++; 541 this.structuredLogger.error("browser-test.js | " + msg); 542 } 543 544 win.close(); 545 } 546 } 547 }, 548 549 finish: function Tester_finish() { 550 var passCount = this.tests.reduce((a, f) => a + f.passCount, 0); 551 var failCount = this.tests.reduce((a, f) => a + f.failCount, 0); 552 var todoCount = this.tests.reduce((a, f) => a + f.todoCount, 0); 553 554 // Include failures from window state checking prior to running the first test 555 failCount += this.failuresFromInitialWindowState; 556 557 TabDestroyObserver.destroy(); 558 Services.console.unregisterListener(this); 559 560 this.AccessibilityUtils.uninit(); 561 562 // It's important to terminate the module to avoid crashes on shutdown. 563 this.PromiseTestUtils.uninit(); 564 565 // In the main process, we print the ShutdownLeaksCollector message here. 566 let pid = Services.appinfo.processID; 567 dump("Completed ShutdownLeaks collections in process " + pid + "\n"); 568 569 this.structuredLogger.info("TEST-START | Shutdown"); 570 571 if (this.tests.length) { 572 let e10sMode = window.gMultiProcessBrowser ? "e10s" : "non-e10s"; 573 this.structuredLogger.info("Browser Chrome Test Summary"); 574 this.structuredLogger.info("Passed: " + passCount); 575 this.structuredLogger.info("Failed: " + failCount); 576 this.structuredLogger.info("Todo: " + todoCount); 577 this.structuredLogger.info("Mode: " + e10sMode); 578 } else { 579 this.structuredLogger.error( 580 "browser-test.js | No tests to run. Did you pass invalid test_paths?" 581 ); 582 } 583 this.structuredLogger.info("*** End BrowserChrome Test Results ***"); 584 585 // Tests complete, notify the callback and return 586 this.callback(this.tests); 587 this.callback = null; 588 this.tests = null; 589 }, 590 591 haltTests: function Tester_haltTests() { 592 // Do not run any further tests 593 this.currentTestIndex = this.tests.length - 1; 594 this.repeat = 0; 595 }, 596 597 observe: function Tester_observe(aSubject, aTopic) { 598 if (!aTopic) { 599 this.onConsoleMessage(aSubject); 600 } 601 }, 602 603 onConsoleMessage: function Tester_onConsoleMessage(aConsoleMessage) { 604 // Ignore empty messages. 605 if (!aConsoleMessage.message) { 606 return; 607 } 608 609 try { 610 var msg = "Console message: " + aConsoleMessage.message; 611 if (this.currentTest) { 612 this.currentTest.addResult(new testMessage(msg)); 613 } else { 614 this.structuredLogger.info( 615 "TEST-INFO | (browser-test.js) | " + msg.replace(/\n$/, "") + "\n" 616 ); 617 } 618 } catch (ex) { 619 // Swallow exception so we don't lead to another error being reported, 620 // throwing us into an infinite loop 621 } 622 }, 623 624 async ensureVsyncDisabled() { 625 // The WebExtension process keeps vsync enabled forever in headless mode. 626 // See bug 1782541. 627 if (Services.env.get("MOZ_HEADLESS")) { 628 return; 629 } 630 631 try { 632 await this.TestUtils.waitForCondition( 633 () => !ChromeUtils.vsyncEnabled(), 634 "waiting for vsync to be disabled" 635 ); 636 } catch (e) { 637 this.Assert.ok(false, e); 638 this.Assert.ok( 639 false, 640 "vsync remained enabled at the end of the test. " + 641 "Is there an animation still running? " + 642 "Consider talking to the performance team for tips to solve this." 643 ); 644 } 645 }, 646 647 getNewRepeatingTimers() { 648 let repeatingTimers = this._getRepeatingTimers(); 649 let results = []; 650 for (let timer of repeatingTimers) { 651 let { name, delay } = timer; 652 // For now ignore long repeating timers (typically from nsExpirationTracker). 653 if (delay >= 10000) { 654 continue; 655 } 656 657 // Also ignore the nsAvailableMemoryWatcher timer that is started when the 658 // user-interaction-active notification is fired, and stopped when the 659 // user-interaction-inactive notification occurs. 660 // On Linux it's a 5s timer, on other platforms it's 10s, which is already 661 // ignored by the previous case. 662 if ( 663 AppConstants.platform == "linux" && 664 name == "nsAvailableMemoryWatcher" 665 ) { 666 continue; 667 } 668 669 // Ignore ScrollFrameActivityTracker, it's a 4s timer which could begin 670 // shortly after the end of a test and cause failure. See bug 1878627. 671 if (name == "ScrollFrameActivityTracker") { 672 continue; 673 } 674 675 // Same as ScrollFrameActivityTracker and the other expiration trackers 676 // ignored above. 677 if (name == "PopupExpirationTracker") { 678 continue; 679 } 680 681 // Ignore nsHttpConnectionMgr timers which show up on browser mochitests 682 // running with http3. See Bug 1829841. 683 if (name == "nsHttpConnectionMgr") { 684 continue; 685 } 686 687 if ( 688 !this._repeatingTimers.find(t => t.delay == delay && t.name == name) 689 ) { 690 results.push(timer); 691 } 692 } 693 if (results.length) { 694 ChromeUtils.addProfilerMarker( 695 "NewRepeatingTimers", 696 { category: "Test" }, 697 results.map(t => `${t.name}: ${t.delay}ms`).join(", ") 698 ); 699 } 700 return results; 701 }, 702 703 async ensureNoNewRepeatingTimers() { 704 let newTimers; 705 try { 706 await this.TestUtils.waitForCondition( 707 async function () { 708 // The array returned by nsITimerManager.getTimers doesn't include 709 // timers that are queued in the event loop of their target thread. 710 // By waiting for a tick, we ensure the timers that might fire about 711 // at the same time as our waitForCondition timer will be included. 712 await this.TestUtils.waitForTick(); 713 714 newTimers = this.getNewRepeatingTimers(); 715 return !newTimers.length; 716 }.bind(this), 717 "waiting for new repeating timers to be cancelled" 718 ); 719 } catch (e) { 720 this.Assert.ok(false, e); 721 for (let { name, delay } of newTimers) { 722 this.Assert.ok( 723 false, 724 `test left unexpected repeating timer ${name} (duration: ${delay}ms)` 725 ); 726 } 727 // Once the new repeating timers have been reported, add them to 728 // this._repeatingTimers to avoid reporting them again for the next 729 // tests of the manifest. 730 this._repeatingTimers.push(...newTimers); 731 } 732 }, 733 734 async checkPreferencesAfterTest() { 735 if (!this._ignorePrefs) { 736 const ignorePrefsFile = `chrome://mochikit/content/${gConfig.ignorePrefsFile}`; 737 try { 738 const res = await fetch(ignorePrefsFile); 739 this._ignorePrefs = await res.json(); 740 } catch (e) { 741 this.Assert.ok( 742 false, 743 `Failed to load ignorePrefsFile (${ignorePrefsFile}): ${e}` 744 ); 745 } 746 } 747 // This comparison relies on a snapshot of the prefs having been saved by 748 // a call to getBaselinePrefs in SpecialPowersParent's init(). 749 const failures = await window.SpecialPowers.comparePrefsToBaseline( 750 this._ignorePrefs 751 ); 752 753 let testPath = this.currentTest.path; 754 if (testPath.startsWith("chrome://mochitests/content/browser/")) { 755 testPath = testPath.replace("chrome://mochitests/content/browser/", ""); 756 } 757 let changedPrefs = []; 758 for (let p of failures) { 759 this.structuredLogger.error( 760 // We only report unexpected failures when --compare-preferences is set. 761 `TEST-${gConfig.comparePrefs ? "UN" : ""}EXPECTED-FAIL | ${testPath} | changed preference: ${p}` 762 ); 763 changedPrefs.push(p); 764 } 765 766 if (changedPrefs.length && Services.env.exists("MOZ_UPLOAD_DIR")) { 767 let modifiedPrefsPath = PathUtils.join( 768 Services.env.get("MOZ_UPLOAD_DIR"), 769 "modifiedPrefs.json" 770 ); 771 772 if (!this._modifiedPrefs) { 773 try { 774 this._modifiedPrefs = JSON.parse( 775 await IOUtils.readUTF8(modifiedPrefsPath) 776 ); 777 } catch (e) { 778 this._modifiedPrefs = {}; 779 } 780 } 781 782 this._modifiedPrefs[testPath] = changedPrefs; 783 await IOUtils.writeJSON(modifiedPrefsPath, this._modifiedPrefs); 784 } 785 }, 786 787 /** 788 * In the event that a test module left any traces in Session Restore state, 789 * clean those up so that each test module starts execution with the same 790 * fresh Session Restore state. 791 */ 792 resetSessionState() { 793 // Forget all closed windows. 794 while (window.SessionStore.getClosedWindowCount() > 0) { 795 window.SessionStore.forgetClosedWindow(0); 796 } 797 798 // Forget all closed tabs for the test window. 799 const closedTabCount = 800 window.SessionStore.getClosedTabCountForWindow(window); 801 for (let i = 0; i < closedTabCount; i++) { 802 try { 803 window.SessionStore.forgetClosedTab(window, 0); 804 } catch (err) { 805 // This will fail if there are tab groups in here 806 } 807 } 808 809 // Forget saved tab groups. 810 const savedTabGroups = window.SessionStore.getSavedTabGroups(); 811 savedTabGroups.forEach(tabGroup => 812 window.SessionStore.forgetSavedTabGroup(tabGroup.id) 813 ); 814 815 // Forget closed tab groups in the test window. 816 const closedTabGroups = window.SessionStore.getClosedTabGroups(window); 817 closedTabGroups.forEach(tabGroup => 818 window.SessionStore.forgetClosedTabGroup(window, tabGroup.id) 819 ); 820 }, 821 822 async notifyProfilerOfTestEnd() { 823 // Note the test run time 824 let name = this.currentTest.path; 825 name = name.slice(name.lastIndexOf("/") + 1); 826 ChromeUtils.addProfilerMarker( 827 "browser-test", 828 { category: "Test", startTime: this.lastStartTimestamp }, 829 name 830 ); 831 832 // See if we should upload a profile of a failing test. 833 if (this.currentTest.failCount) { 834 // If MOZ_PROFILER_SHUTDOWN is set, the profiler got started from --profiler 835 // and a profile will be shown even if there's no test failure. 836 if ( 837 Services.env.exists("MOZ_UPLOAD_DIR") && 838 !Services.env.exists("MOZ_PROFILER_SHUTDOWN") && 839 Services.profiler.IsActive() 840 ) { 841 let filename = `profile_${name}.json`; 842 let path = Services.env.get("MOZ_UPLOAD_DIR"); 843 let profilePath = PathUtils.join(path, filename); 844 try { 845 const { profile } = 846 await Services.profiler.getProfileDataAsGzippedArrayBuffer(); 847 await IOUtils.write(profilePath, new Uint8Array(profile)); 848 this.currentTest.addResult( 849 new testResult({ 850 name: 851 "Found unexpected failures during the test; profile uploaded in " + 852 filename, 853 }) 854 ); 855 } catch (e) { 856 // If the profile is large, we may encounter out of memory errors. 857 this.currentTest.addResult( 858 new testResult({ 859 name: 860 "Found unexpected failures during the test; failed to upload profile: " + 861 e, 862 }) 863 ); 864 } 865 } 866 } 867 }, 868 869 async nextTest() { 870 // On first call (no currentTest yet), check for initial window state issues 871 if (!this.currentTest) { 872 this.checkWindowsState(); 873 } else { 874 if (this._coverageCollector) { 875 this._coverageCollector.recordTestCoverage(this.currentTest.path); 876 } 877 878 this.PerTestCoverageUtils.afterTestSync(); 879 880 // Run cleanup functions for the current test before moving on to the 881 // next one. 882 let testScope = this.currentTest.scope; 883 while (testScope.__cleanupFunctions.length) { 884 let func = testScope.__cleanupFunctions.shift(); 885 try { 886 let result = await func.apply(testScope); 887 if (isGenerator(result)) { 888 this.SimpleTest.ok(false, "Cleanup function returned a generator"); 889 } 890 } catch (ex) { 891 this.currentTest.addResult( 892 new testResult({ 893 name: "Cleanup function threw an exception", 894 ex, 895 allowFailure: this.currentTest.allowFailure, 896 }) 897 ); 898 } 899 } 900 901 // Spare tests cleanup work. 902 // Reset gReduceMotionOverride in case the test set it. 903 if (typeof gReduceMotionOverride == "boolean") { 904 gReduceMotionOverride = null; 905 } 906 907 Services.obs.notifyObservers(null, "test-complete"); 908 909 // Ensure to reset the clipboard in case the test has modified it, 910 // so it won't affect the next tests. 911 window.SpecialPowers.clipboardCopyString(""); 912 913 if ( 914 this.currentTest.passCount === 0 && 915 this.currentTest.failCount === 0 && 916 this.currentTest.todoCount === 0 917 ) { 918 this.currentTest.addResult( 919 new testResult({ 920 name: 921 "This test contains no passes, no fails and no todos. Maybe" + 922 " it threw a silent exception? Make sure you use" + 923 " waitForExplicitFinish() if you need it.", 924 }) 925 ); 926 } 927 928 let winUtils = window.windowUtils; 929 if (winUtils.isTestControllingRefreshes) { 930 this.currentTest.addResult( 931 new testResult({ 932 name: "test left refresh driver under test control", 933 }) 934 ); 935 winUtils.restoreNormalRefresh(); 936 } 937 938 if (this.SimpleTest.isExpectingUncaughtException()) { 939 this.currentTest.addResult( 940 new testResult({ 941 name: 942 "expectUncaughtException was called but no uncaught" + 943 " exception was detected!", 944 allowFailure: this.currentTest.allowFailure, 945 }) 946 ); 947 } 948 949 this.resolveFinishTestPromise(); 950 this.resolveFinishTestPromise = null; 951 this.TestUtils.promiseTestFinished = null; 952 953 this.PromiseTestUtils.ensureDOMPromiseRejectionsProcessed(); 954 this.PromiseTestUtils.assertNoUncaughtRejections(); 955 this.PromiseTestUtils.assertNoMoreExpectedRejections(); 956 await this.ensureVsyncDisabled(); 957 958 Object.keys(window).forEach(function (prop) { 959 if (parseInt(prop) == prop) { 960 // This is a string which when parsed as an integer and then 961 // stringified gives the original string. As in, this is in fact a 962 // string representation of an integer, so an index into 963 // window.frames. Skip those. 964 return; 965 } 966 if (!this._globalProperties.includes(prop)) { 967 this._globalProperties.push(prop); 968 if (!this._globalPropertyWhitelist.includes(prop)) { 969 this.currentTest.addResult( 970 new testResult({ 971 name: "test left unexpected property on window: " + prop, 972 allowFailure: this.currentTest.allowFailure, 973 }) 974 ); 975 } 976 } 977 }, this); 978 979 await this.ensureNoNewRepeatingTimers(); 980 981 await new Promise(resolve => window.SpecialPowers.flushPrefEnv(resolve)); 982 983 await this.checkPreferencesAfterTest(); 984 985 window.SpecialPowers.cleanupAllClipboard(); 986 987 if (AppConstants.MOZ_APP_NAME != "thunderbird") { 988 this.resetSessionState(); 989 } 990 991 if (gConfig.cleanupCrashes) { 992 let gdir = Services.dirsvc.get("UAppData", Ci.nsIFile); 993 gdir.append("Crash Reports"); 994 gdir.append("pending"); 995 if (gdir.exists()) { 996 let entries = gdir.directoryEntries; 997 while (entries.hasMoreElements()) { 998 let entry = entries.nextFile; 999 if (entry.isFile()) { 1000 let msg = "this test left a pending crash report; "; 1001 try { 1002 entry.remove(false); 1003 msg += "deleted " + entry.path; 1004 } catch (e) { 1005 msg += "could not delete " + entry.path; 1006 } 1007 this.structuredLogger.info(msg); 1008 } 1009 } 1010 } 1011 } 1012 1013 // Notify a long running test problem if it didn't end up in a timeout. 1014 if (this.currentTest.unexpectedTimeouts && !this.currentTest.timedOut) { 1015 this.currentTest.addResult( 1016 new testResult({ 1017 name: 1018 "This test exceeded the timeout threshold. It should be" + 1019 " rewritten or split up. If that's not possible, use" + 1020 " requestLongerTimeout(N), but only as a last resort.", 1021 }) 1022 ); 1023 } 1024 1025 // If we're in a debug build, check assertion counts. This code 1026 // is similar to the code in TestRunner.testUnloaded in 1027 // TestRunner.js used for all other types of mochitests. 1028 let debugsvc = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); 1029 if (debugsvc.isDebugBuild) { 1030 let newAssertionCount = debugsvc.assertionCount; 1031 let numAsserts = newAssertionCount - this.lastAssertionCount; 1032 this.lastAssertionCount = newAssertionCount; 1033 1034 let max = testScope.__expectedMaxAsserts; 1035 let min = testScope.__expectedMinAsserts; 1036 if (numAsserts > max) { 1037 // TEST-UNEXPECTED-FAIL 1038 this.currentTest.addResult( 1039 new testResult({ 1040 name: 1041 "Assertion count " + 1042 numAsserts + 1043 " is greater than expected range " + 1044 min + 1045 "-" + 1046 max + 1047 " assertions.", 1048 pass: true, // TEMPORARILY TEST-KNOWN-FAIL 1049 todo: true, 1050 allowFailure: this.currentTest.allowFailure, 1051 }) 1052 ); 1053 } else if (numAsserts < min) { 1054 // TEST-UNEXPECTED-PASS 1055 this.currentTest.addResult( 1056 new testResult({ 1057 name: 1058 "Assertion count " + 1059 numAsserts + 1060 " is less than expected range " + 1061 min + 1062 "-" + 1063 max + 1064 " assertions.", 1065 todo: true, 1066 allowFailure: this.currentTest.allowFailure, 1067 }) 1068 ); 1069 } else if (numAsserts > 0) { 1070 // TEST-KNOWN-FAIL 1071 this.currentTest.addResult( 1072 new testResult({ 1073 name: 1074 "Assertion count " + 1075 numAsserts + 1076 " is within expected range " + 1077 min + 1078 "-" + 1079 max + 1080 " assertions.", 1081 pass: true, 1082 todo: true, 1083 allowFailure: this.currentTest.allowFailure, 1084 }) 1085 ); 1086 } 1087 } 1088 1089 if ( 1090 this.currentTest.allowFailure && 1091 this.currentTest.allowedFailureCount == 0 1092 ) { 1093 this.currentTest.addResult( 1094 new testResult({ 1095 name: 1096 "We expect at least one assertion to fail because this" + 1097 " test file is marked as fail-if in the manifest.", 1098 todo: true, 1099 knownFailure: this.currentTest.allowFailure, 1100 }) 1101 ); 1102 } 1103 1104 // Dump memory stats for main thread. 1105 if ( 1106 Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT 1107 ) { 1108 this.MemoryStats.dump( 1109 this.currentTestIndex, 1110 this.currentTest.path, 1111 gConfig.dumpOutputDirectory, 1112 gConfig.dumpAboutMemoryAfterTest, 1113 gConfig.dumpDMDAfterTest 1114 ); 1115 } 1116 1117 this.PromiseTestUtils.assertNoUncaughtRejections(); 1118 1119 await this.notifyProfilerOfTestEnd(); 1120 1121 // Check the window state before logging testEnd so that any cleanup 1122 // assertions are included in the test result, not logged after test_end. 1123 this.checkWindowsState(); 1124 1125 let time = Date.now() - this.lastStartTime; 1126 1127 this.structuredLogger.testEnd( 1128 this.currentTest.path, 1129 "OK", 1130 undefined, 1131 "finished in " + time + "ms" 1132 ); 1133 this.currentTest.setDuration(time); 1134 1135 if (this.runUntilFailure && this.currentTest.failCount > 0) { 1136 this.haltTests(); 1137 } 1138 1139 // Restore original SimpleTest methods to avoid leaks. 1140 SIMPLETEST_OVERRIDES.forEach(m => { 1141 this.SimpleTest[m] = this.SimpleTestOriginal[m]; 1142 }); 1143 1144 this.ContentTask.setTestScope(null); 1145 testScope.destroy(); 1146 this.currentTest.scope = null; 1147 } 1148 1149 // Replace the current tab with about:blank. For the first test, this ensures 1150 // we start with a clean slate. For subsequent tests, this must happen AFTER 1151 // test_end is logged, otherwise the new windows created by addTab will be 1152 // tracked by ShutdownLeaks as belonging to the test and cause false leak reports. 1153 if (window.gBrowser) { 1154 gBrowser.addTab("about:blank", { 1155 skipAnimation: true, 1156 triggeringPrincipal: 1157 Services.scriptSecurityManager.getSystemPrincipal(), 1158 }); 1159 gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true }); 1160 gBrowser.stop(); 1161 } 1162 1163 // Make sure the window is raised before starting the next test. 1164 this.SimpleTest.waitForFocus(() => { 1165 if (this.done) { 1166 if (this._coverageCollector) { 1167 this._coverageCollector.finalize(); 1168 } else if ( 1169 !AppConstants.RELEASE_OR_BETA && 1170 !AppConstants.DEBUG && 1171 !AppConstants.MOZ_CODE_COVERAGE && 1172 !AppConstants.ASAN && 1173 !AppConstants.TSAN 1174 ) { 1175 this.finish(); 1176 return; 1177 } 1178 1179 // Uninitialize a few things explicitly so that they can clean up 1180 // frames and browser intentionally kept alive until shutdown to 1181 // eliminate false positives. 1182 if (gConfig.testRoot == "browser") { 1183 // Skip if SeaMonkey 1184 if (AppConstants.MOZ_APP_NAME != "seamonkey") { 1185 // Replace the document currently loaded in the browser's sidebar. 1186 // This will prevent false positives for tests that were the last 1187 // to touch the sidebar. They will thus not be blamed for leaking 1188 // a document. 1189 let sidebar = document.getElementById("sidebar"); 1190 if (sidebar) { 1191 sidebar.setAttribute("src", "data:text/html;charset=utf-8,"); 1192 sidebar.docShell.createAboutBlankDocumentViewer(null, null); 1193 sidebar.setAttribute("src", "about:blank"); 1194 } 1195 } 1196 1197 // Destroy BackgroundPageThumbs resources. 1198 let { BackgroundPageThumbs } = ChromeUtils.importESModule( 1199 "resource://gre/modules/BackgroundPageThumbs.sys.mjs" 1200 ); 1201 BackgroundPageThumbs._destroy(); 1202 1203 if (window.gBrowser) { 1204 NewTabPagePreloading.removePreloadedBrowser(window); 1205 } 1206 } 1207 1208 // Schedule GC and CC runs before finishing in order to detect 1209 // DOM windows leaked by our tests or the tested code. Note that we 1210 // use a shrinking GC so that the JS engine will discard JIT code and 1211 // JIT caches more aggressively. 1212 1213 let shutdownCleanup = aCallback => { 1214 Cu.schedulePreciseShrinkingGC(() => { 1215 // Run the GC and CC a few times to make sure that as much 1216 // as possible is freed. 1217 let numCycles = 3; 1218 for (let i = 0; i < numCycles; i++) { 1219 Cu.forceGC(); 1220 Cu.forceCC(); 1221 } 1222 aCallback(); 1223 }); 1224 }; 1225 1226 let { AsyncShutdown } = ChromeUtils.importESModule( 1227 "resource://gre/modules/AsyncShutdown.sys.mjs" 1228 ); 1229 1230 let barrier = new AsyncShutdown.Barrier( 1231 "ShutdownLeaks: Wait for cleanup to be finished before checking for leaks" 1232 ); 1233 Services.obs.notifyObservers( 1234 { wrappedJSObject: barrier }, 1235 "shutdown-leaks-before-check" 1236 ); 1237 1238 barrier.client.addBlocker( 1239 "ShutdownLeaks: Wait for tabs to finish closing", 1240 TabDestroyObserver.wait() 1241 ); 1242 1243 barrier.wait().then(() => { 1244 // Simulate memory pressure so that we're forced to free more resources 1245 // and thus get rid of more false leaks like already terminated workers. 1246 Services.obs.notifyObservers( 1247 null, 1248 "memory-pressure", 1249 "heap-minimize" 1250 ); 1251 1252 Services.ppmm.broadcastAsyncMessage("browser-test:collect-request"); 1253 1254 shutdownCleanup(() => { 1255 setTimeout(() => { 1256 shutdownCleanup(() => { 1257 this.finish(); 1258 }); 1259 }, 1000); 1260 }); 1261 }); 1262 1263 return; 1264 } 1265 1266 if (this.repeat > 0) { 1267 --this.repeat; 1268 if (this.currentTestIndex < 0) { 1269 this.currentTestIndex = 0; 1270 } 1271 this.execTest(); 1272 } else { 1273 this.currentTestIndex++; 1274 if (gConfig.repeat) { 1275 this.repeat = gConfig.repeat; 1276 } 1277 this.execTest(); 1278 } 1279 }); 1280 }, 1281 1282 async handleTask(task, currentTest, PromiseTestUtils, isSetup = false) { 1283 let currentScope = currentTest.scope; 1284 let desc = isSetup ? "setup" : "test"; 1285 currentScope.SimpleTest.info(`Entering ${desc} ${task.name}`); 1286 let startTimestamp = ChromeUtils.now(); 1287 currentScope.SimpleTest._currentTaskName = task.name; 1288 1289 let controller = new AbortController(); 1290 currentScope.__signal = controller.signal; 1291 if (isSetup) { 1292 currentScope.registerCleanupFunction(() => { 1293 controller.abort(); 1294 }); 1295 } 1296 try { 1297 let result = await task(); 1298 if (isGenerator(result)) { 1299 currentScope.SimpleTest.ok(false, "Task returned a generator"); 1300 } 1301 } catch (ex) { 1302 if (currentTest.timedOut) { 1303 currentTest.addResult( 1304 new testResult({ 1305 name: `Uncaught exception received from previously timed out ${desc} ${task.name}`, 1306 pass: false, 1307 ex, 1308 stack: typeof ex == "object" && "stack" in ex ? ex.stack : null, 1309 allowFailure: currentTest.allowFailure, 1310 }) 1311 ); 1312 // We timed out, so we've already cleaned up for this test, just get outta here. 1313 return; 1314 } 1315 currentTest.addResult( 1316 new testResult({ 1317 name: `Uncaught exception in ${desc}`, 1318 pass: currentScope.SimpleTest.isExpectingUncaughtException(), 1319 ex, 1320 stack: typeof ex == "object" && "stack" in ex ? ex.stack : null, 1321 allowFailure: currentTest.allowFailure, 1322 }) 1323 ); 1324 } finally { 1325 if (!isSetup) { 1326 controller.abort(); 1327 } 1328 } 1329 PromiseTestUtils.assertNoUncaughtRejections(); 1330 ChromeUtils.addProfilerMarker( 1331 isSetup ? "setup-task" : "task", 1332 { category: "Test", startTime: startTimestamp }, 1333 task.name || undefined 1334 ); 1335 currentScope.SimpleTest.info(`Leaving ${desc} ${task.name}`); 1336 currentScope.SimpleTest._currentTaskName = null; 1337 }, 1338 1339 async _runTaskBasedTest(currentTest) { 1340 let currentScope = currentTest.scope; 1341 1342 // First run all the setups: 1343 let setupFn; 1344 while ((setupFn = currentScope.__setups.shift())) { 1345 await this.handleTask( 1346 setupFn, 1347 currentTest, 1348 this.PromiseTestUtils, 1349 true /* is setup task */ 1350 ); 1351 } 1352 1353 // Allow for a task to be skipped; we need only use the structured logger 1354 // for this, whilst deactivating log buffering to ensure that messages 1355 // are always printed to stdout. 1356 let skipTask = (task, reason) => { 1357 let logger = this.structuredLogger; 1358 logger.deactivateBuffering(); 1359 logger.testStatus(this.currentTest.path, task.name, "SKIP"); 1360 let message = "Skipping test " + task.name; 1361 if (reason) { 1362 message += ` because the following conditions were met: (${reason})`; 1363 } 1364 logger.warning(message); 1365 logger.activateBuffering(); 1366 }; 1367 1368 let task; 1369 while ((task = currentScope.__tasks.shift())) { 1370 let reason; 1371 let shouldSkip = false; 1372 if ( 1373 task.__skipMe || 1374 (currentScope.__runOnlyThisTask && 1375 task != currentScope.__runOnlyThisTask) 1376 ) { 1377 shouldSkip = true; 1378 } else if (typeof task.__skip_if === "function" && task.__skip_if()) { 1379 shouldSkip = true; 1380 reason = task.__skip_if.toSource().replace(/\(\)\s*=>\s*/, ""); 1381 } 1382 1383 if (shouldSkip) { 1384 skipTask(task, reason); 1385 continue; 1386 } 1387 await this.handleTask(task, currentTest, this.PromiseTestUtils); 1388 } 1389 currentScope.finish(); 1390 }, 1391 1392 execTest: function Tester_execTest() { 1393 this.structuredLogger.testStart(this.currentTest.path); 1394 1395 this.SimpleTest.reset(); 1396 // Reset accessibility environment. 1397 this.AccessibilityUtils.reset(this.a11y_checks, this.currentTest.path); 1398 1399 // Load the tests into a testscope 1400 let currentScope = (this.currentTest.scope = new testScope( 1401 this, 1402 this.currentTest, 1403 this.currentTest.expected 1404 )); 1405 let currentTest = this.currentTest; 1406 1407 // HTTPS-First (Bug 1704453) TODO: in case a test is annoated 1408 // with https_first_disabled then we explicitly flip the pref 1409 // dom.security.https_first to false for the duration of the test. 1410 if (currentTest.https_first_disabled) { 1411 window.SpecialPowers.pushPrefEnv({ 1412 set: [["dom.security.https_first", false]], 1413 }); 1414 } 1415 1416 if (currentTest.allow_xul_xbl) { 1417 window.SpecialPowers.pushPermissions([ 1418 { type: "allowXULXBL", allow: true, context: "http://mochi.test:8888" }, 1419 { type: "allowXULXBL", allow: true, context: "http://example.org" }, 1420 ]); 1421 } 1422 1423 // Import utils in the test scope. 1424 let { scope } = this.currentTest; 1425 scope.EventUtils = this.EventUtils; 1426 scope.AccessibilityUtils = this.AccessibilityUtils; 1427 scope.SimpleTest = this.SimpleTest; 1428 scope.gTestPath = this.currentTest.path; 1429 scope.ContentTask = this.ContentTask; 1430 scope.BrowserTestUtils = this.BrowserTestUtils; 1431 scope.TestUtils = this.TestUtils; 1432 scope.ExtensionTestUtils = this.ExtensionTestUtils; 1433 // Pass a custom report function for mochitest style reporting. 1434 scope.Assert = new this.Assert(function (err, message, stack) { 1435 currentTest.addResult( 1436 new testResult( 1437 err 1438 ? { 1439 name: err.message, 1440 stack: err.stack, 1441 allowFailure: currentTest.allowFailure, 1442 } 1443 : { 1444 name: message, 1445 pass: true, 1446 stack, 1447 allowFailure: currentTest.allowFailure, 1448 } 1449 ) 1450 ); 1451 }, true); 1452 1453 this.ContentTask.setTestScope(currentScope); 1454 1455 // Import Mochia methods in the test scope 1456 Services.scriptloader.loadSubScript( 1457 "resource://testing-common/Mochia.js", 1458 scope 1459 ); 1460 1461 // Allow Assert.sys.mjs methods to be tacked to the current scope. 1462 scope.export_assertions = function () { 1463 for (let func in this.Assert) { 1464 this[func] = this.Assert[func].bind(this.Assert); 1465 } 1466 }; 1467 1468 // Override SimpleTest methods with ours. 1469 SIMPLETEST_OVERRIDES.forEach(function (m) { 1470 this.SimpleTest[m] = this[m]; 1471 }, scope); 1472 1473 // load the tools to work with chrome .jar and remote 1474 try { 1475 this._scriptLoader.loadSubScript( 1476 "chrome://mochikit/content/chrome-harness.js", 1477 scope 1478 ); 1479 } catch (ex) { 1480 /* no chrome-harness tools */ 1481 } 1482 1483 // Ensure we are not idle at the beginning of the test. If we don't do this, 1484 // the browser may behave differently if the previous tests ran long. 1485 // eg. the session store behavior changes 3 minutes after the last user event. 1486 Cc["@mozilla.org/widget/useridleservice;1"] 1487 .getService(Ci.nsIUserIdleServiceInternal) 1488 .resetIdleTimeOut(0); 1489 1490 // Import head.js script if it exists. 1491 var currentTestDirPath = this.currentTest.path.substr( 1492 0, 1493 this.currentTest.path.lastIndexOf("/") 1494 ); 1495 var headPath = currentTestDirPath + "/head.js"; 1496 try { 1497 this._scriptLoader.loadSubScript(headPath, scope); 1498 } catch (ex) { 1499 // Bug 755558 - Ignore loadSubScript errors due to a missing head.js. 1500 const isImportError = /^Error opening input stream/.test(ex.toString()); 1501 1502 // Bug 1503169 - head.js may call loadSubScript, and generate similar errors. 1503 // Only swallow errors that are strictly related to loading head.js. 1504 const containsHeadPath = ex.toString().includes(headPath); 1505 1506 if (!isImportError || !containsHeadPath) { 1507 this.currentTest.addResult( 1508 new testResult({ 1509 name: "head.js import threw an exception", 1510 ex, 1511 }) 1512 ); 1513 } 1514 } 1515 1516 // Import the test script. 1517 try { 1518 this.lastStartTimestamp = ChromeUtils.now(); 1519 this.TestUtils.promiseTestFinished = new Promise(resolve => { 1520 this.resolveFinishTestPromise = resolve; 1521 }); 1522 this._scriptLoader.loadSubScript(this.currentTest.path, scope); 1523 // Run the test 1524 this.lastStartTime = Date.now(); 1525 if (this.currentTest.scope.__tasks) { 1526 // This test consists of tasks, added via the `add_task()` API. 1527 if ("test" in this.currentTest.scope) { 1528 throw new Error( 1529 "Cannot run both a add_task test and a normal test at the same time." 1530 ); 1531 } 1532 // Spin off the async work without waiting for it to complete. 1533 // It'll call finish() when it's done. 1534 this._runTaskBasedTest(this.currentTest); 1535 } else if (typeof scope.test == "function") { 1536 scope.test(); 1537 } else { 1538 throw new Error( 1539 "This test didn't call add_task, nor did it define a generatorTest() function, nor did it define a test() function, so we don't know how to run it." 1540 ); 1541 } 1542 } catch (ex) { 1543 if (!this.SimpleTest.isIgnoringAllUncaughtExceptions()) { 1544 this.currentTest.addResult( 1545 new testResult({ 1546 name: "Exception thrown", 1547 pass: this.SimpleTest.isExpectingUncaughtException(), 1548 ex, 1549 allowFailure: this.currentTest.allowFailure, 1550 }) 1551 ); 1552 this.SimpleTest.expectUncaughtException(false); 1553 } else { 1554 this.currentTest.addResult(new testMessage("Exception thrown: " + ex)); 1555 } 1556 this.currentTest.scope.finish(); 1557 } 1558 1559 // If the test ran synchronously, move to the next test, otherwise the test 1560 // will trigger the next test when it is done. 1561 if (this.currentTest.scope.__done) { 1562 this.nextTest(); 1563 } else { 1564 var self = this; 1565 var timeoutExpires = Date.now() + gTimeoutSeconds * 1000; 1566 var waitUntilAtLeast = timeoutExpires - 1000; 1567 this.currentTest.scope.__waitTimer = 1568 this.SimpleTest._originalSetTimeout.apply(window, [ 1569 async function timeoutFn() { 1570 // We sometimes get woken up long before the gTimeoutSeconds 1571 // have elapsed (when running in chaos mode for example). This 1572 // code ensures that we don't wrongly time out in that case. 1573 if (Date.now() < waitUntilAtLeast) { 1574 self.currentTest.scope.__waitTimer = setTimeout( 1575 timeoutFn, 1576 timeoutExpires - Date.now() 1577 ); 1578 return; 1579 } 1580 1581 if (--self.currentTest.scope.__timeoutFactor > 0) { 1582 // We were asked to wait a bit longer. 1583 self.currentTest.scope.info( 1584 "Longer timeout required, waiting longer... Remaining timeouts: " + 1585 self.currentTest.scope.__timeoutFactor 1586 ); 1587 self.currentTest.scope.__waitTimer = setTimeout( 1588 timeoutFn, 1589 gTimeoutSeconds * 1000 1590 ); 1591 return; 1592 } 1593 1594 // If the test is taking longer than expected, but it's not hanging, 1595 // mark the fact, but let the test continue. At the end of the test, 1596 // if it didn't timeout, we will notify the problem through an error. 1597 // To figure whether it's an actual hang, compare the time of the last 1598 // result or message to half of the timeout time. 1599 // Though, to protect against infinite loops, limit the number of times 1600 // we allow the test to proceed. 1601 const MAX_UNEXPECTED_TIMEOUTS = 10; 1602 if ( 1603 Date.now() - self.currentTest.lastOutputTime < 1604 (gTimeoutSeconds / 2) * 1000 && 1605 ++self.currentTest.unexpectedTimeouts <= MAX_UNEXPECTED_TIMEOUTS 1606 ) { 1607 self.currentTest.scope.__waitTimer = setTimeout( 1608 timeoutFn, 1609 gTimeoutSeconds * 1000 1610 ); 1611 return; 1612 } 1613 1614 let knownFailure = false; 1615 if (gConfig.timeoutAsPass) { 1616 knownFailure = true; 1617 } 1618 self.currentTest.addResult( 1619 new testResult({ 1620 name: "Test timed out", 1621 allowFailure: knownFailure, 1622 }) 1623 ); 1624 self.currentTest.timedOut = true; 1625 self.currentTest.scope.__waitTimer = null; 1626 if (gConfig.timeoutAsPass) { 1627 self.nextTest(); 1628 } else { 1629 await self.notifyProfilerOfTestEnd(); 1630 self.finish(); 1631 } 1632 }, 1633 gTimeoutSeconds * 1000, 1634 ]); 1635 } 1636 }, 1637 1638 QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener"]), 1639 }; 1640 1641 // Note: duplicated in SimpleTest.js . See also bug 1820150. 1642 function isErrorOrException(err) { 1643 // It'd be nice if we had either `Error.isError(err)` or `Error.isInstance(err)` 1644 // but we don't, so do it ourselves: 1645 if (!err) { 1646 return false; 1647 } 1648 if (err instanceof Ci.nsIException) { 1649 return true; 1650 } 1651 try { 1652 let glob = Cu.getGlobalForObject(err); 1653 return err instanceof glob.Error; 1654 } catch { 1655 // getGlobalForObject can be upset if it doesn't get passed an object. 1656 // Just do a standard instanceof check using this global and cross fingers: 1657 } 1658 return err instanceof Error; 1659 } 1660 1661 /** 1662 * Represents the result of one test assertion. This is described with a string 1663 * in traditional logging, and has a "status" and "expected" property used in 1664 * structured logging. Normally, results are mapped as follows: 1665 * 1666 * pass: todo: Added to: Described as: Status: Expected: 1667 * true false passCount TEST-PASS PASS PASS 1668 * true true todoCount TEST-KNOWN-FAIL FAIL FAIL 1669 * false false failCount TEST-UNEXPECTED-FAIL FAIL PASS 1670 * false true failCount TEST-UNEXPECTED-PASS PASS FAIL 1671 * 1672 * The "allowFailure" argument indicates that this is one of the assertions that 1673 * should be allowed to fail, for example because "fail-if" is true for the 1674 * current test file in the manifest. In this case, results are mapped this way: 1675 * 1676 * pass: todo: Added to: Described as: Status: Expected: 1677 * true false passCount TEST-PASS PASS PASS 1678 * true true todoCount TEST-KNOWN-FAIL FAIL FAIL 1679 * false false todoCount TEST-KNOWN-FAIL FAIL FAIL 1680 * false true todoCount TEST-KNOWN-FAIL FAIL FAIL 1681 */ 1682 function testResult({ name, pass, todo, ex, stack, allowFailure }) { 1683 this.info = false; 1684 this.name = name; 1685 this.msg = ""; 1686 1687 if (allowFailure && !pass) { 1688 this.allowedFailure = true; 1689 this.pass = true; 1690 this.todo = false; 1691 } else if (allowFailure && pass) { 1692 this.pass = true; 1693 this.todo = false; 1694 } else { 1695 this.pass = !!pass; 1696 this.todo = todo; 1697 } 1698 1699 this.expected = this.todo ? "FAIL" : "PASS"; 1700 1701 if (this.pass) { 1702 this.status = this.expected; 1703 return; 1704 } 1705 1706 this.status = this.todo ? "PASS" : "FAIL"; 1707 1708 if (ex) { 1709 if (typeof ex == "object" && "fileName" in ex) { 1710 // Only add "at fileName:lineNumber" if stack doesn't start with same location 1711 let stackMatchesExLocation = false; 1712 1713 if (stack instanceof Ci.nsIStackFrame) { 1714 stackMatchesExLocation = 1715 stack.filename == ex.fileName && stack.lineNumber == ex.lineNumber; 1716 } else if (typeof stack === "string") { 1717 // For string stacks, format is: functionName@fileName:lineNumber:columnNumber 1718 // Check if first line contains fileName:lineNumber 1719 let firstLine = stack.split("\n")[0]; 1720 let expectedLocation = ex.fileName + ":" + ex.lineNumber; 1721 stackMatchesExLocation = firstLine.includes(expectedLocation); 1722 } 1723 1724 if (!stackMatchesExLocation) { 1725 this.msg += "at " + ex.fileName + ":" + ex.lineNumber + " - "; 1726 } 1727 } 1728 1729 if ( 1730 typeof ex == "string" || 1731 (typeof ex == "object" && isErrorOrException(ex)) 1732 ) { 1733 this.msg += String(ex); 1734 } else { 1735 try { 1736 this.msg += JSON.stringify(ex); 1737 } catch { 1738 this.msg += String(ex); 1739 } 1740 } 1741 } 1742 1743 // Store stack separately instead of appending to msg 1744 if (stack) { 1745 let normalized; 1746 if (stack instanceof Ci.nsIStackFrame) { 1747 let frames = []; 1748 for ( 1749 let frame = stack; 1750 frame; 1751 frame = frame.asyncCaller || frame.caller 1752 ) { 1753 let msg = `${frame.filename}:${frame.name}:${frame.lineNumber}`; 1754 frames.push(frame.asyncCause ? `${frame.asyncCause}*${msg}` : msg); 1755 } 1756 normalized = frames.join("\n"); 1757 } else { 1758 normalized = "" + stack; 1759 } 1760 this.stack = normalized; 1761 } 1762 1763 if (gConfig.debugOnFailure) { 1764 // You've hit this line because you requested to break into the 1765 // debugger upon a testcase failure on your test run. 1766 // eslint-disable-next-line no-debugger 1767 debugger; 1768 } 1769 1770 // Optionally, test variables can be saved to a file, which will be uploaded 1771 // as an artifact if the test is running on try. 1772 saveScopeVariablesAsJSON(); 1773 } 1774 1775 function testMessage(msg) { 1776 this.msg = msg || ""; 1777 this.info = true; 1778 } 1779 1780 // Need to be careful adding properties to this object, since its properties 1781 // cannot conflict with global variables used in tests. 1782 function testScope(aTester, aTest, expected) { 1783 this.__tester = aTester; 1784 1785 aTest.allowFailure = expected == "fail"; 1786 1787 var self = this; 1788 this.ok = function test_ok(condition, name) { 1789 if (arguments.length > 2) { 1790 const ex = "Too many arguments passed to ok(condition, name)`."; 1791 self.record(false, name, ex); 1792 } else { 1793 self.record(condition, name); 1794 } 1795 }; 1796 this.record = function test_record(condition, name, ex, stack, expected) { 1797 if (expected == "fail") { 1798 aTest.addResult( 1799 new testResult({ 1800 name, 1801 pass: !condition, 1802 todo: true, 1803 ex, 1804 stack: stack || Components.stack.caller, 1805 allowFailure: aTest.allowFailure, 1806 }) 1807 ); 1808 } else { 1809 aTest.addResult( 1810 new testResult({ 1811 name, 1812 pass: condition, 1813 ex, 1814 stack: stack || Components.stack.caller, 1815 allowFailure: aTest.allowFailure, 1816 }) 1817 ); 1818 } 1819 }; 1820 this.is = function test_is(a, b, name) { 1821 self.record( 1822 Object.is(a, b), 1823 name, 1824 `Got ${self.repr(a)}, expected ${self.repr(b)}`, 1825 false, 1826 Components.stack.caller 1827 ); 1828 }; 1829 this.isfuzzy = function test_isfuzzy(a, b, epsilon, name) { 1830 self.record( 1831 a >= b - epsilon && a <= b + epsilon, 1832 name, 1833 `Got ${self.repr(a)}, expected ${self.repr(b)} epsilon: +/- ${self.repr( 1834 epsilon 1835 )}`, 1836 false, 1837 Components.stack.caller 1838 ); 1839 }; 1840 this.isnot = function test_isnot(a, b, name) { 1841 self.record( 1842 !Object.is(a, b), 1843 name, 1844 `Didn't expect ${self.repr(a)}, but got it`, 1845 false, 1846 Components.stack.caller 1847 ); 1848 }; 1849 this.todo = function test_todo(condition, name, ex, stack) { 1850 aTest.addResult( 1851 new testResult({ 1852 name, 1853 pass: !condition, 1854 todo: true, 1855 ex, 1856 stack: stack || Components.stack.caller, 1857 allowFailure: aTest.allowFailure, 1858 }) 1859 ); 1860 }; 1861 this.todo_is = function test_todo_is(a, b, name) { 1862 self.todo( 1863 Object.is(a, b), 1864 name, 1865 `Got ${self.repr(a)}, expected ${self.repr(b)}`, 1866 Components.stack.caller 1867 ); 1868 }; 1869 this.todo_isnot = function test_todo_isnot(a, b, name) { 1870 self.todo( 1871 !Object.is(a, b), 1872 name, 1873 `Didn't expect ${self.repr(a)}, but got it`, 1874 Components.stack.caller 1875 ); 1876 }; 1877 this.info = function test_info(name) { 1878 aTest.addResult(new testMessage(name)); 1879 }; 1880 this.repr = function repr(o) { 1881 if (typeof o == "undefined") { 1882 return "undefined"; 1883 } else if (o === null) { 1884 return "null"; 1885 } 1886 try { 1887 if (typeof o.__repr__ == "function") { 1888 return o.__repr__(); 1889 } else if (typeof o.repr == "function" && o.repr != repr) { 1890 return o.repr(); 1891 } 1892 } catch (e) {} 1893 try { 1894 if ( 1895 typeof o.NAME == "string" && 1896 (o.toString == Function.prototype.toString || 1897 o.toString == Object.prototype.toString) 1898 ) { 1899 return o.NAME; 1900 } 1901 } catch (e) {} 1902 var ostring; 1903 try { 1904 if (Object.is(o, +0)) { 1905 ostring = "+0"; 1906 } else if (Object.is(o, -0)) { 1907 ostring = "-0"; 1908 } else if (typeof o === "string") { 1909 ostring = JSON.stringify(o); 1910 } else if (Array.isArray(o)) { 1911 ostring = "[" + o.map(val => repr(val)).join(", ") + "]"; 1912 } else { 1913 ostring = String(o); 1914 } 1915 } catch (e) { 1916 return `[${Object.prototype.toString.call(o)}]`; 1917 } 1918 if (typeof o == "function") { 1919 ostring = ostring.replace(/\) \{[^]*/, ") { ... }"); 1920 } 1921 return ostring; 1922 }; 1923 1924 this.executeSoon = function test_executeSoon(func) { 1925 Services.tm.dispatchToMainThread({ 1926 run() { 1927 func(); 1928 }, 1929 }); 1930 }; 1931 1932 this.waitForExplicitFinish = function test_waitForExplicitFinish() { 1933 self.__done = false; 1934 }; 1935 1936 this.waitForFocus = function test_waitForFocus( 1937 callback, 1938 targetWindow, 1939 expectBlankPage 1940 ) { 1941 self.SimpleTest.waitForFocus(callback, targetWindow, expectBlankPage); 1942 }; 1943 1944 this.waitForClipboard = function test_waitForClipboard( 1945 expected, 1946 setup, 1947 success, 1948 failure, 1949 flavor 1950 ) { 1951 self.SimpleTest.waitForClipboard(expected, setup, success, failure, flavor); 1952 }; 1953 1954 this.registerCleanupFunction = function test_registerCleanupFunction( 1955 aFunction 1956 ) { 1957 self.__cleanupFunctions.push(aFunction); 1958 }; 1959 1960 this.requestLongerTimeout = function test_requestLongerTimeout(aFactor) { 1961 self.__timeoutFactor = aFactor; 1962 }; 1963 1964 this.expectUncaughtException = function test_expectUncaughtException( 1965 aExpecting 1966 ) { 1967 self.SimpleTest.expectUncaughtException(aExpecting); 1968 }; 1969 1970 this.ignoreAllUncaughtExceptions = function test_ignoreAllUncaughtExceptions( 1971 aIgnoring 1972 ) { 1973 self.SimpleTest.ignoreAllUncaughtExceptions(aIgnoring); 1974 }; 1975 1976 this.expectAssertions = function test_expectAssertions(aMin, aMax) { 1977 let min = aMin; 1978 let max = aMax; 1979 if (typeof max == "undefined") { 1980 max = min; 1981 } 1982 if ( 1983 typeof min != "number" || 1984 typeof max != "number" || 1985 min < 0 || 1986 max < min 1987 ) { 1988 throw new Error("bad parameter to expectAssertions"); 1989 } 1990 self.__expectedMinAsserts = min; 1991 self.__expectedMaxAsserts = max; 1992 }; 1993 1994 this.finish = function test_finish() { 1995 self.__done = true; 1996 if (self.__waitTimer) { 1997 self.executeSoon(function () { 1998 if (self.__done && self.__waitTimer) { 1999 clearTimeout(self.__waitTimer); 2000 self.__waitTimer = null; 2001 self.__tester.nextTest(); 2002 } 2003 }); 2004 } 2005 }; 2006 2007 this.requestCompleteLog = function test_requestCompleteLog() { 2008 self.__tester.structuredLogger.deactivateBuffering(); 2009 self.registerCleanupFunction(function () { 2010 self.__tester.structuredLogger.activateBuffering(); 2011 }); 2012 }; 2013 2014 return this; 2015 } 2016 2017 function decorateTaskFn(fn) { 2018 let originalName = fn.name; 2019 fn = fn.bind(this); 2020 // Restore original name to avoid "bound " prefix in task name 2021 Object.defineProperty(fn, "name", { value: originalName }); 2022 fn.skip = (val = true) => (fn.__skipMe = val); 2023 fn.only = () => (this.__runOnlyThisTask = fn); 2024 return fn; 2025 } 2026 2027 testScope.prototype = { 2028 __done: true, 2029 __tasks: null, 2030 __setups: [], 2031 __runOnlyThisTask: null, 2032 __waitTimer: null, 2033 __cleanupFunctions: [], 2034 __timeoutFactor: 1, 2035 __expectedMinAsserts: 0, 2036 __expectedMaxAsserts: 0, 2037 /** @type {AbortSignal} */ 2038 __signal: null, 2039 2040 EventUtils: {}, 2041 AccessibilityUtils: {}, 2042 SimpleTest: {}, 2043 ContentTask: null, 2044 BrowserTestUtils: null, 2045 TestUtils: null, 2046 ExtensionTestUtils: null, 2047 Assert: null, 2048 2049 /** 2050 * Add a function which returns a promise (usually an async function) 2051 * as a test task. 2052 * 2053 * The task ends when the promise returned by the function resolves or 2054 * rejects. If the test function throws, or the promise it returns 2055 * rejects, the test is reported as a failure. Execution continues 2056 * with the next test function. 2057 * 2058 * Example usage: 2059 * 2060 * add_task(async function test() { 2061 * let result = await Promise.resolve(true); 2062 * 2063 * ok(result); 2064 * 2065 * let secondary = await someFunctionThatReturnsAPromise(result); 2066 * is(secondary, "expected value"); 2067 * }); 2068 * 2069 * add_task(async function test_early_return() { 2070 * let result = await somethingThatReturnsAPromise(); 2071 * 2072 * if (!result) { 2073 * // Test is ended immediately, with success. 2074 * return; 2075 * } 2076 * 2077 * is(result, "foo"); 2078 * }); 2079 * 2080 * add_task({ 2081 * skip_if: () => !AppConstants.DEBUG, 2082 * }, 2083 * async function test_debug_only() { 2084 * ok(true, "Test ran in a debug build"); 2085 * }); 2086 */ 2087 add_task(properties, func = properties) { 2088 if (!this.__tasks) { 2089 this.waitForExplicitFinish(); 2090 this.__tasks = []; 2091 } 2092 2093 let bound = decorateTaskFn.call(this, func); 2094 2095 if ( 2096 typeof properties === "object" && 2097 typeof properties.skip_if === "function" 2098 ) { 2099 bound.__skip_if = properties.skip_if; 2100 } 2101 2102 this.__tasks.push(bound); 2103 return bound; 2104 }, 2105 2106 add_setup(aFunction) { 2107 if (!this.__setups.length) { 2108 this.waitForExplicitFinish(); 2109 } 2110 let bound = aFunction.bind(this); 2111 this.__setups.push(bound); 2112 return bound; 2113 }, 2114 2115 destroy: function test_destroy() { 2116 for (let prop in this) { 2117 delete this[prop]; 2118 } 2119 }, 2120 2121 get testSignal() { 2122 return this.__signal; 2123 }, 2124 };