head.js (60143B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim:set ts=2 sw=2 sts=2 et: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 /* 8 * This file contains common code that is loaded before each test file(s). 9 * See https://developer.mozilla.org/en-US/docs/Mozilla/QA/Writing_xpcshell-based_unit_tests 10 * for more information. 11 */ 12 13 /* defined by the harness */ 14 /* globals _HEAD_FILES, _HEAD_JS_PATH, _JSDEBUGGER_PORT, _JSCOV_DIR, 15 _MOZINFO_JS_PATH, _TEST_FILE, _TEST_NAME, _TEST_CWD, _TESTING_MODULES_DIR:true, 16 _PREFS_FILE, _EXPECTED */ 17 18 /* defined by XPCShellImpl.cpp */ 19 /* globals load, sendCommand, changeTestShellDir */ 20 21 /* must be defined by tests using do_await_remote_message/do_send_remote_message */ 22 /* globals Cc, Ci */ 23 24 /* defined by this file but is defined as read-only for tests */ 25 // eslint-disable-next-line no-redeclare 26 /* globals runningInParent: true */ 27 28 /* may be defined in test files */ 29 /* globals run_test */ 30 31 var _quit = false; 32 var _passed = true; 33 var _tests_pending = 0; 34 var _cleanupFunctions = []; 35 var _pendingTimers = []; 36 var _profileInitialized = false; 37 38 // Assigned in do_load_child_test_harness. 39 var _XPCSHELL_PROCESS; 40 41 // Register the testing-common resource protocol early, to have access to its 42 // modules. 43 let _Services = Services; 44 _register_modules_protocol_handler(); 45 46 let { AppConstants: _AppConstants } = ChromeUtils.importESModule( 47 "resource://gre/modules/AppConstants.sys.mjs" 48 ); 49 50 let { PromiseTestUtils: _PromiseTestUtils } = ChromeUtils.importESModule( 51 "resource://testing-common/PromiseTestUtils.sys.mjs" 52 ); 53 54 let { NetUtil: _NetUtil } = ChromeUtils.importESModule( 55 "resource://gre/modules/NetUtil.sys.mjs" 56 ); 57 58 let { XPCOMUtils: _XPCOMUtils } = ChromeUtils.importESModule( 59 "resource://gre/modules/XPCOMUtils.sys.mjs" 60 ); 61 62 // Support a common assertion library, Assert.sys.mjs. 63 var { Assert: AssertCls } = ChromeUtils.importESModule( 64 "resource://testing-common/Assert.sys.mjs" 65 ); 66 67 // Pass a custom report function for xpcshell-test style reporting. 68 var Assert = new AssertCls(function (err, message, stack) { 69 if (err) { 70 do_report_result(false, err.message, err.stack); 71 } else { 72 do_report_result(true, message, stack); 73 } 74 }, true); 75 76 // Bug 1506134 for followup. Some xpcshell tests use ContentTask.sys.mjs, which 77 // expects browser-test.js to have set a testScope that includes record. 78 function record(condition, name, diag, stack) { 79 do_report_result(condition, name, stack); 80 } 81 82 var _add_params = function (params) { 83 if (typeof _XPCSHELL_PROCESS != "undefined") { 84 params.xpcshell_process = _XPCSHELL_PROCESS; 85 } 86 }; 87 88 var _dumpLog = function (raw_msg) { 89 dump(JSON.stringify(raw_msg) + "\n"); 90 }; 91 92 var { StructuredLogger: _LoggerClass } = ChromeUtils.importESModule( 93 "resource://testing-common/StructuredLog.sys.mjs" 94 ); 95 var _testLogger = new _LoggerClass("xpcshell/head.js", _dumpLog, [_add_params]); 96 97 // Disable automatic network detection, so tests work correctly when 98 // not connected to a network. 99 _Services.io.manageOfflineStatus = false; 100 _Services.io.offline = false; 101 102 // Determine if we're running on parent or child 103 var runningInParent = true; 104 try { 105 // Don't use Services.appinfo here as it disables replacing appinfo with stubs 106 // for test usage. 107 runningInParent = 108 // eslint-disable-next-line mozilla/use-services 109 Cc["@mozilla.org/xre/runtime;1"].getService(Ci.nsIXULRuntime).processType == 110 Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; 111 } catch (e) {} 112 113 // Only if building of places is enabled. 114 if (runningInParent && "mozIAsyncHistory" in Ci) { 115 // Ensure places history is enabled for xpcshell-tests as some non-FF 116 // apps disable it. 117 _Services.prefs.setBoolPref("places.history.enabled", true); 118 } 119 120 // Configure crash reporting, if possible 121 // We rely on the Python harness to set MOZ_CRASHREPORTER, 122 // MOZ_CRASHREPORTER_NO_REPORT, and handle checking for minidumps. 123 // Note that if we're in a child process, we don't want to init the 124 // crashreporter component. 125 try { 126 if (runningInParent && "@mozilla.org/toolkit/crash-reporter;1" in Cc) { 127 // Intentially access the crash reporter service directly for this. 128 // eslint-disable-next-line mozilla/use-services 129 let crashReporter = Cc["@mozilla.org/toolkit/crash-reporter;1"].getService( 130 Ci.nsICrashReporter 131 ); 132 crashReporter.UpdateCrashEventsDir(); 133 crashReporter.minidumpPath = do_get_minidumpdir(); 134 135 // Tell the test harness that the crash reporter is set up, and any crash 136 // after this point is supposed to be caught by the crash reporter. 137 // 138 // Due to the limitation on the remote xpcshell test, the process return 139 // code does not represent the process crash. Any crash before this point 140 // can be caught by the absence of this log. 141 _testLogger.logData("crash_reporter_init"); 142 } 143 } catch (e) {} 144 145 if (runningInParent) { 146 _Services.prefs.setBoolPref("dom.push.connection.enabled", false); 147 } 148 149 // Configure a console listener so messages sent to it are logged as part 150 // of the test. 151 try { 152 let levelNames = {}; 153 for (let level of ["debug", "info", "warn", "error"]) { 154 levelNames[Ci.nsIConsoleMessage[level]] = level; 155 } 156 157 let listener = { 158 QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener"]), 159 observe(msg) { 160 if (typeof info === "function") { 161 info( 162 "CONSOLE_MESSAGE: (" + 163 levelNames[msg.logLevel] + 164 ") " + 165 msg.toString() 166 ); 167 } 168 }, 169 }; 170 // Don't use _Services.console here as it causes one of the devtools tests 171 // to fail, probably due to initializing Services.console too early. 172 // eslint-disable-next-line mozilla/use-services 173 Cc["@mozilla.org/consoleservice;1"] 174 .getService(Ci.nsIConsoleService) 175 .registerListener(listener); 176 } catch (e) {} 177 /** 178 * Date.now() is not necessarily monotonically increasing (insert sob story 179 * about times not being the right tool to use for measuring intervals of time, 180 * robarnold can tell all), so be wary of error by erring by at least 181 * _timerFuzz ms. 182 */ 183 const _timerFuzz = 15; 184 185 function _Timer(func, delay) { 186 delay = Number(delay); 187 if (delay < 0) { 188 do_throw("do_timeout() delay must be nonnegative"); 189 } 190 191 if (typeof func !== "function") { 192 do_throw("string callbacks no longer accepted; use a function!"); 193 } 194 195 this._func = func; 196 this._start = Date.now(); 197 this._delay = delay; 198 199 var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 200 timer.initWithCallback(this, delay + _timerFuzz, timer.TYPE_ONE_SHOT); 201 202 // Keep timer alive until it fires 203 _pendingTimers.push(timer); 204 } 205 _Timer.prototype = { 206 QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]), 207 208 notify(timer) { 209 _pendingTimers.splice(_pendingTimers.indexOf(timer), 1); 210 211 // The current nsITimer implementation can undershoot, but even if it 212 // couldn't, paranoia is probably a virtue here given the potential for 213 // random orange on tinderboxen. 214 var end = Date.now(); 215 var elapsed = end - this._start; 216 if (elapsed >= this._delay) { 217 try { 218 this._func.call(null); 219 } catch (e) { 220 do_throw("exception thrown from do_timeout callback: " + e); 221 } 222 return; 223 } 224 225 // Timer undershot, retry with a little overshoot to try to avoid more 226 // undershoots. 227 var newDelay = this._delay - elapsed; 228 do_timeout(newDelay, this._func); 229 }, 230 }; 231 232 function _isGenerator(val) { 233 return typeof val === "object" && val && typeof val.next === "function"; 234 } 235 236 function _do_main() { 237 if (_quit) { 238 return; 239 } 240 241 _testLogger.info("running event loop"); 242 243 var tm = Cc["@mozilla.org/thread-manager;1"].getService(); 244 245 tm.spinEventLoopUntil("Test(xpcshell/head.js:_do_main)", () => _quit); 246 247 tm.spinEventLoopUntilEmpty(); 248 } 249 250 function _do_quit() { 251 _testLogger.info("exiting test"); 252 _quit = true; 253 } 254 255 // This is useless, except to the extent that it has the side-effect of 256 // initializing the widget module, which some tests unfortunately 257 // accidentally rely on. 258 void Cc["@mozilla.org/widget/transferable;1"].createInstance(); 259 260 /** 261 * Overrides idleService with a mock. Idle is commonly used for maintenance 262 * tasks, thus if a test uses a service that requires the idle service, it will 263 * start handling them. 264 * This behaviour would cause random failures and slowdown tests execution, 265 * for example by running database vacuum or cleanups for each test. 266 * 267 * Note: Idle service is overridden by default. If a test requires it, it will 268 * have to call do_get_idle() function at least once before use. 269 */ 270 var _fakeIdleService = { 271 get registrar() { 272 delete this.registrar; 273 return (this.registrar = Components.manager.QueryInterface( 274 Ci.nsIComponentRegistrar 275 )); 276 }, 277 contractID: "@mozilla.org/widget/useridleservice;1", 278 CID: Components.ID("{9163a4ae-70c2-446c-9ac1-bbe4ab93004e}"), 279 280 activate: function FIS_activate() { 281 if (!this.originalCID) { 282 this.originalCID = this.registrar.contractIDToCID(this.contractID); 283 // Replace with the mock. 284 this.registrar.registerFactory( 285 this.CID, 286 "Fake Idle Service", 287 this.contractID, 288 this.factory 289 ); 290 } 291 }, 292 293 deactivate: function FIS_deactivate() { 294 if (this.originalCID) { 295 // Unregister the mock. 296 this.registrar.unregisterFactory(this.CID, this.factory); 297 // Restore original factory. 298 this.registrar.registerFactory( 299 this.originalCID, 300 "Idle Service", 301 this.contractID, 302 null 303 ); 304 delete this.originalCID; 305 } 306 }, 307 308 factory: { 309 // nsIFactory 310 createInstance(aIID) { 311 return _fakeIdleService.QueryInterface(aIID); 312 }, 313 QueryInterface: ChromeUtils.generateQI(["nsIFactory"]), 314 }, 315 316 // nsIUserIdleService 317 get idleTime() { 318 return 0; 319 }, 320 addIdleObserver() {}, 321 removeIdleObserver() {}, 322 323 // eslint-disable-next-line mozilla/use-chromeutils-generateqi 324 QueryInterface(aIID) { 325 // Useful for testing purposes, see test_get_idle.js. 326 if (aIID.equals(Ci.nsIFactory)) { 327 return this.factory; 328 } 329 if (aIID.equals(Ci.nsIUserIdleService) || aIID.equals(Ci.nsISupports)) { 330 return this; 331 } 332 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); 333 }, 334 }; 335 336 /** 337 * Restores the idle service factory if needed and returns the service's handle. 338 * 339 * @return A handle to the idle service. 340 */ 341 function do_get_idle() { 342 _fakeIdleService.deactivate(); 343 return Cc[_fakeIdleService.contractID].getService(Ci.nsIUserIdleService); 344 } 345 346 // Map resource://test/ to current working directory and 347 // resource://testing-common/ to the shared test modules directory. 348 function _register_protocol_handlers() { 349 let protocolHandler = _Services.io 350 .getProtocolHandler("resource") 351 .QueryInterface(Ci.nsIResProtocolHandler); 352 353 let curDirURI = _Services.io.newFileURI(do_get_cwd()); 354 protocolHandler.setSubstitution("test", curDirURI); 355 356 _register_modules_protocol_handler(); 357 } 358 359 function _register_modules_protocol_handler() { 360 if (!_TESTING_MODULES_DIR) { 361 throw new Error( 362 "Please define a path where the testing modules can be " + 363 "found in a variable called '_TESTING_MODULES_DIR' before " + 364 "head.js is included." 365 ); 366 } 367 368 let protocolHandler = _Services.io 369 .getProtocolHandler("resource") 370 .QueryInterface(Ci.nsIResProtocolHandler); 371 372 let modulesFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 373 modulesFile.initWithPath(_TESTING_MODULES_DIR); 374 375 if (!modulesFile.exists()) { 376 throw new Error( 377 "Specified modules directory does not exist: " + _TESTING_MODULES_DIR 378 ); 379 } 380 381 if (!modulesFile.isDirectory()) { 382 throw new Error( 383 "Specified modules directory is not a directory: " + _TESTING_MODULES_DIR 384 ); 385 } 386 387 let modulesURI = _Services.io.newFileURI(modulesFile); 388 389 protocolHandler.setSubstitution("testing-common", modulesURI); 390 } 391 392 /* Debugging support */ 393 // Used locally and by our self-tests. 394 function _setupDevToolsServer(breakpointFiles, callback) { 395 // Always allow remote debugging. 396 _Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true); 397 398 // for debugging-the-debugging, let an env var cause log spew. 399 if (_Services.env.get("DEVTOOLS_DEBUGGER_LOG")) { 400 _Services.prefs.setBoolPref("devtools.debugger.log", true); 401 } 402 if (_Services.env.get("DEVTOOLS_DEBUGGER_LOG_VERBOSE")) { 403 _Services.prefs.setBoolPref("devtools.debugger.log.verbose", true); 404 } 405 406 let require; 407 try { 408 const { 409 useDistinctSystemPrincipalLoader, 410 releaseDistinctSystemPrincipalLoader, 411 } = ChromeUtils.importESModule( 412 "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", 413 { global: "shared" } 414 ); 415 const requester = {}; 416 const distinctLoader = useDistinctSystemPrincipalLoader(requester); 417 registerCleanupFunction(() => { 418 releaseDistinctSystemPrincipalLoader(requester); 419 }); 420 421 require = distinctLoader.require; 422 } catch (e) { 423 throw new Error( 424 "resource://devtools appears to be inaccessible from the " + 425 "xpcshell environment.\n" + 426 "This can usually be resolved by adding:\n" + 427 " firefox-appdir = browser\n" + 428 "to the xpcshell.toml manifest.\n" + 429 "It is possible for this to alter test behevior by " + 430 "triggering additional browser code to run, so check " + 431 "test behavior after making this change.\n" + 432 "See also https://bugzil.la/1215378." 433 ); 434 } 435 let { DevToolsServer } = require("devtools/server/devtools-server"); 436 DevToolsServer.init(); 437 DevToolsServer.registerAllActors(); 438 let { createRootActor } = require("resource://testing-common/dbg-actors.js"); 439 DevToolsServer.setRootActor(createRootActor); 440 DevToolsServer.allowChromeProcess = true; 441 442 const TOPICS = [ 443 // An observer notification that tells us when the thread actor is ready 444 // and can accept breakpoints. 445 "devtools-thread-ready", 446 // Or when devtools are destroyed and we should stop observing. 447 "xpcshell-test-devtools-shutdown", 448 ]; 449 let observe = function (subject, topic) { 450 if (topic === "devtools-thread-ready") { 451 const threadActor = subject.wrappedJSObject; 452 threadActor.setBreakpointOnLoad(breakpointFiles); 453 } 454 455 for (let topicToRemove of TOPICS) { 456 _Services.obs.removeObserver(observe, topicToRemove); 457 } 458 callback(); 459 }; 460 461 for (let topic of TOPICS) { 462 _Services.obs.addObserver(observe, topic); 463 } 464 465 const { SocketListener } = require("devtools/shared/security/socket"); 466 467 return { DevToolsServer, SocketListener }; 468 } 469 470 function _initDebugging(port) { 471 let initialized = false; 472 const { DevToolsServer, SocketListener } = _setupDevToolsServer( 473 _TEST_FILE, 474 () => { 475 initialized = true; 476 } 477 ); 478 479 info(""); 480 info("*******************************************************************"); 481 info("Waiting for the debugger to connect on port " + port); 482 info(""); 483 info("To connect the debugger, open a Firefox instance, select 'Connect'"); 484 info("from the Developer menu and specify the port as " + port); 485 info("*******************************************************************"); 486 info(""); 487 488 const AuthenticatorType = DevToolsServer.Authenticators.get("PROMPT"); 489 const authenticator = new AuthenticatorType.Server(); 490 authenticator.allowConnection = () => { 491 return DevToolsServer.AuthenticationResult.ALLOW; 492 }; 493 const socketOptions = { 494 authenticator, 495 portOrPath: port, 496 }; 497 498 const listener = new SocketListener(DevToolsServer, socketOptions); 499 listener.open(); 500 501 // spin an event loop until the debugger connects. 502 const tm = Cc["@mozilla.org/thread-manager;1"].getService(); 503 let lastUpdate = Date.now(); 504 tm.spinEventLoopUntil("Test(xpcshell/head.js:_initDebugging)", () => { 505 if (initialized) { 506 return true; 507 } 508 if (Date.now() - lastUpdate > 5000) { 509 info("Still waiting for debugger to connect..."); 510 lastUpdate = Date.now(); 511 } 512 return false; 513 }); 514 // NOTE: if you want to debug the harness itself, you can now add a 'debugger' 515 // statement anywhere and it will stop - but we've already added a breakpoint 516 // for the first line of the test scripts, so we just continue... 517 info("Debugger connected, starting test execution"); 518 } 519 520 function _do_upload_profile() { 521 let name = _TEST_NAME.replace(/.*\//, ""); 522 let filename = `profile_${name}.json`; 523 let path = Services.env.get("MOZ_UPLOAD_DIR"); 524 let profilePath = PathUtils.join(path, filename); 525 let done = false; 526 (async function _save_profile() { 527 const { profile } = 528 await Services.profiler.getProfileDataAsGzippedArrayBuffer(); 529 await IOUtils.write(profilePath, new Uint8Array(profile)); 530 _testLogger.testStatus( 531 _TEST_NAME, 532 "Found unexpected failures during the test; profile uploaded in " + 533 filename, 534 "FAIL" 535 ); 536 })() 537 .catch(e => { 538 // If the profile is large, we may encounter out of memory errors. 539 _testLogger.error( 540 "Found unexpected failures during the test; failed to upload profile: " + 541 e 542 ); 543 }) 544 .then(() => (done = true)); 545 _Services.tm.spinEventLoopUntil( 546 "Test(xpcshell/head.js:_save_profile)", 547 () => done 548 ); 549 } 550 551 // eslint-disable-next-line complexity 552 function _execute_test() { 553 if (typeof _TEST_CWD != "undefined") { 554 try { 555 changeTestShellDir(_TEST_CWD); 556 } catch (e) { 557 _testLogger.error(_exception_message(e)); 558 } 559 } 560 if (runningInParent && _AppConstants.platform == "android") { 561 try { 562 // GeckoView initialization needs the profile 563 do_get_profile(true); 564 // Wake up GeckoViewStartup 565 let geckoViewStartup = Cc["@mozilla.org/geckoview/startup;1"].getService( 566 Ci.nsIObserver 567 ); 568 geckoViewStartup.observe(null, "profile-after-change", null); 569 geckoViewStartup.observe(null, "app-startup", null); 570 571 // Glean needs to be initialized for metric recording & tests to work. 572 // Usually this happens through Glean Kotlin, 573 // but for xpcshell tests we initialize it from here. 574 _Services.fog.initializeFOG(); 575 } catch (ex) { 576 do_throw(`Failed to initialize GeckoView: ${ex}`, ex.stack); 577 } 578 } 579 580 // _JSDEBUGGER_PORT is dynamically defined by <runxpcshelltests.py>. 581 if (_JSDEBUGGER_PORT) { 582 try { 583 _initDebugging(_JSDEBUGGER_PORT); 584 } catch (ex) { 585 // Fail the test run immediately if debugging is requested but fails, so 586 // that the failure state is more obvious. 587 do_throw(`Failed to initialize debugging: ${ex}`, ex.stack); 588 } 589 } 590 591 _register_protocol_handlers(); 592 593 // Override idle service by default. 594 // Call do_get_idle() to restore the factory and get the service. 595 _fakeIdleService.activate(); 596 597 _PromiseTestUtils.init(); 598 599 let coverageCollector = null; 600 if (typeof _JSCOV_DIR === "string") { 601 let _CoverageCollector = ChromeUtils.importESModule( 602 "resource://testing-common/CoverageUtils.sys.mjs" 603 ).CoverageCollector; 604 coverageCollector = new _CoverageCollector(_JSCOV_DIR); 605 } 606 607 let startTime = ChromeUtils.now(); 608 609 // _HEAD_FILES is dynamically defined by <runxpcshelltests.py>. 610 _load_files(_HEAD_FILES); 611 // _TEST_FILE is dynamically defined by <runxpcshelltests.py>. 612 _load_files(_TEST_FILE); 613 614 // Tack Assert.sys.mjs methods to the current scope. 615 this.Assert = Assert; 616 for (let func in Assert) { 617 this[func] = Assert[func].bind(Assert); 618 } 619 620 const { PerTestCoverageUtils } = ChromeUtils.importESModule( 621 "resource://testing-common/PerTestCoverageUtils.sys.mjs" 622 ); 623 624 if (runningInParent) { 625 PerTestCoverageUtils.beforeTestSync(); 626 } 627 628 let timer; 629 if ( 630 // Services.profiler is missing on some tier3 platforms where 631 // MOZ_GECKO_PROFILER is not set. 632 Services.profiler?.IsActive() && 633 !Services.env.exists("MOZ_PROFILER_SHUTDOWN") && 634 Services.env.exists("MOZ_UPLOAD_DIR") && 635 Services.env.exists("MOZ_TEST_TIMEOUT_INTERVAL") 636 ) { 637 timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 638 timer.initWithCallback( 639 function upload_test_timeout_profile() { 640 ChromeUtils.addProfilerMarker( 641 "xpcshell-test", 642 { category: "Test", startTime }, 643 _TEST_NAME 644 ); 645 _do_upload_profile(); 646 }, 647 parseInt(Services.env.get("MOZ_TEST_TIMEOUT_INTERVAL")) * 1000 * 0.9, // Keep 10% of the time to gather the profile. 648 timer.TYPE_ONE_SHOT 649 ); 650 } 651 652 try { 653 do_test_pending("MAIN run_test"); 654 // Check if run_test() is defined. If defined, run it. 655 // Else, call run_next_test() directly to invoke tests 656 // added by add_test() and add_task(). 657 if (typeof run_test === "function") { 658 run_test(); 659 } else { 660 run_next_test(); 661 } 662 663 do_test_finished("MAIN run_test"); 664 _do_main(); 665 _PromiseTestUtils.assertNoUncaughtRejections(); 666 667 if (coverageCollector != null) { 668 coverageCollector.recordTestCoverage(_TEST_FILE[0]); 669 } 670 671 if (runningInParent) { 672 PerTestCoverageUtils.afterTestSync(); 673 } 674 } catch (e) { 675 _passed = false; 676 // do_check failures are already logged and set _quit to true and throw 677 // NS_ERROR_ABORT. If both of those are true it is likely this exception 678 // has already been logged so there is no need to log it again. It's 679 // possible that this will mask an NS_ERROR_ABORT that happens after a 680 // do_check failure though. 681 682 if (!_quit || e.result != Cr.NS_ERROR_ABORT) { 683 let extra = {}; 684 if (e.fileName) { 685 extra.source_file = e.fileName; 686 if (e.lineNumber) { 687 extra.line_number = e.lineNumber; 688 } 689 } else { 690 extra.source_file = "xpcshell/head.js"; 691 } 692 let message = _exception_message(e); 693 if (e.stack) { 694 extra.stack = _format_stack(e.stack); 695 } 696 _testLogger.error(message, extra); 697 } 698 } finally { 699 if (coverageCollector != null) { 700 coverageCollector.finalize(); 701 } 702 } 703 704 // Execute all of our cleanup functions. 705 let reportCleanupError = function (ex) { 706 let stack, filename; 707 if (ex && typeof ex == "object" && "stack" in ex) { 708 stack = ex.stack; 709 } else { 710 stack = Components.stack.caller; 711 } 712 if (stack instanceof Ci.nsIStackFrame) { 713 filename = stack.filename; 714 } else if (ex.fileName) { 715 filename = ex.fileName; 716 } 717 _testLogger.error(_exception_message(ex), { 718 stack: _format_stack(stack), 719 source_file: filename, 720 }); 721 }; 722 723 let complete = !_cleanupFunctions.length; 724 let cleanupStartTime = complete ? 0 : ChromeUtils.now(); 725 (async () => { 726 for (let func of _cleanupFunctions.reverse()) { 727 try { 728 let result = await func(); 729 if (_isGenerator(result)) { 730 Assert.ok(false, "Cleanup function returned a generator"); 731 } 732 } catch (ex) { 733 reportCleanupError(ex); 734 } 735 } 736 _cleanupFunctions = []; 737 })() 738 .catch(reportCleanupError) 739 .then(() => (complete = true)); 740 _Services.tm.spinEventLoopUntil( 741 "Test(xpcshell/head.js:_execute_test)", 742 () => complete 743 ); 744 if (cleanupStartTime) { 745 ChromeUtils.addProfilerMarker( 746 "xpcshell-test", 747 { category: "Test", startTime: cleanupStartTime }, 748 "Cleanup functions" 749 ); 750 } 751 752 ChromeUtils.addProfilerMarker( 753 "xpcshell-test", 754 { category: "Test", startTime }, 755 _TEST_NAME 756 ); 757 _Services.obs.notifyObservers(null, "test-complete"); 758 759 // Restore idle service to avoid leaks. 760 _fakeIdleService.deactivate(); 761 762 if ( 763 globalThis.hasOwnProperty("storage") && 764 StorageManager.isInstance(globalThis.storage) 765 ) { 766 globalThis.storage.shutdown(); 767 } 768 769 if (_profileInitialized) { 770 // Since we have a profile, we will notify profile shutdown topics at 771 // the end of the current test, to ensure correct cleanup on shutdown. 772 _Services.startup.advanceShutdownPhase( 773 _Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNNETTEARDOWN 774 ); 775 _Services.startup.advanceShutdownPhase( 776 _Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNTEARDOWN 777 ); 778 _Services.startup.advanceShutdownPhase( 779 _Services.startup.SHUTDOWN_PHASE_APPSHUTDOWN 780 ); 781 _Services.startup.advanceShutdownPhase( 782 _Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNQM 783 ); 784 785 _profileInitialized = false; 786 } 787 788 try { 789 _PromiseTestUtils.ensureDOMPromiseRejectionsProcessed(); 790 _PromiseTestUtils.assertNoUncaughtRejections(); 791 _PromiseTestUtils.assertNoMoreExpectedRejections(); 792 } finally { 793 // It's important to terminate the module to avoid crashes on shutdown. 794 _PromiseTestUtils.uninit(); 795 } 796 797 if (timer) { 798 timer.cancel(); 799 } 800 801 // If MOZ_PROFILER_SHUTDOWN is set, the profiler got started from --profiler 802 // and a profile will be shown even if there's no test failure. 803 if ( 804 !_passed && 805 runningInParent && 806 Services.env.exists("MOZ_UPLOAD_DIR") && 807 !Services.env.exists("MOZ_PROFILER_SHUTDOWN") && 808 Services.profiler.IsActive() 809 ) { 810 if (_EXPECTED != "pass") { 811 _testLogger.error( 812 "Not uploading the profile as the test is expected to fail." 813 ); 814 } else { 815 _do_upload_profile(); 816 } 817 } 818 819 // Skip the normal shutdown path for optimized builds that don't do leak checking. 820 if ( 821 runningInParent && 822 !_AppConstants.RELEASE_OR_BETA && 823 !_AppConstants.DEBUG && 824 !_AppConstants.MOZ_CODE_COVERAGE && 825 !_AppConstants.ASAN && 826 !_AppConstants.TSAN 827 ) { 828 Cu.exitIfInAutomation(); 829 } 830 } 831 832 /** 833 * Loads files. 834 * 835 * @param aFiles Array of files to load. 836 */ 837 function _load_files(aFiles) { 838 function load_file(element) { 839 try { 840 let startTime = ChromeUtils.now(); 841 load(element); 842 ChromeUtils.addProfilerMarker( 843 "load_file", 844 { category: "Test", startTime }, 845 element.replace(/.*\/_?tests\/xpcshell\//, "") 846 ); 847 } catch (e) { 848 let extra = { 849 source_file: element, 850 }; 851 if (e.stack) { 852 extra.stack = _format_stack(e.stack); 853 } 854 _testLogger.error(_exception_message(e), extra); 855 } 856 } 857 858 aFiles.forEach(load_file); 859 } 860 861 function _wrap_with_quotes_if_necessary(val) { 862 return typeof val == "string" ? '"' + val + '"' : val; 863 } 864 865 /* ************* Functions to be used from the tests ************* */ 866 867 /** 868 * Prints a message to the output log. 869 */ 870 function info(msg, data) { 871 ChromeUtils.addProfilerMarker("INFO", { category: "Test" }, msg); 872 msg = _wrap_with_quotes_if_necessary(msg); 873 data = data ? data : null; 874 _testLogger.info(msg, data); 875 } 876 877 /** 878 * Calls the given function at least the specified number of milliseconds later. 879 * The callback will not undershoot the given time, but it might overshoot -- 880 * don't expect precision! 881 * 882 * @param delay : uint 883 * the number of milliseconds to delay 884 * @param callback : function() : void 885 * the function to call 886 */ 887 function do_timeout(delay, func) { 888 new _Timer(func, Number(delay)); 889 } 890 891 function executeSoon(callback, aName) { 892 let funcName = aName ? aName : callback.name; 893 do_test_pending(funcName); 894 895 _Services.tm.dispatchToMainThread({ 896 run() { 897 try { 898 callback(); 899 } catch (e) { 900 // do_check failures are already logged and set _quit to true and throw 901 // NS_ERROR_ABORT. If both of those are true it is likely this exception 902 // has already been logged so there is no need to log it again. It's 903 // possible that this will mask an NS_ERROR_ABORT that happens after a 904 // do_check failure though. 905 if (!_quit || e.result != Cr.NS_ERROR_ABORT) { 906 let stack = e.stack ? _format_stack(e.stack) : null; 907 _testLogger.testStatus( 908 _TEST_NAME, 909 funcName, 910 "FAIL", 911 "PASS", 912 _exception_message(e), 913 stack 914 ); 915 _passed = false; 916 _do_quit(); 917 } 918 } finally { 919 do_test_finished(funcName); 920 } 921 }, 922 }); 923 } 924 925 /** 926 * Shows an error message and the current stack and aborts the test. 927 * 928 * @param error A message string or an Error object. 929 * @param stack null or nsIStackFrame object or a string containing 930 * \n separated stack lines (as in Error().stack). 931 */ 932 function do_throw(error, stack) { 933 let filename = ""; 934 // If we didn't get passed a stack, maybe the error has one 935 // otherwise get it from our call context 936 stack = stack || error.stack || Components.stack.caller; 937 938 if (stack instanceof Ci.nsIStackFrame) { 939 filename = stack.filename; 940 } else if (error.fileName) { 941 filename = error.fileName; 942 } 943 944 _testLogger.error(_exception_message(error), { 945 source_file: filename, 946 stack: _format_stack(stack), 947 }); 948 ChromeUtils.addProfilerMarker( 949 "ERROR", 950 { category: "Test", captureStack: true }, 951 _exception_message(error) 952 ); 953 _abort_failed_test(); 954 } 955 956 function _abort_failed_test() { 957 // Called to abort the test run after all failures are logged. 958 _passed = false; 959 _do_quit(); 960 throw Components.Exception("", Cr.NS_ERROR_ABORT); 961 } 962 963 function _format_stack(stack) { 964 let normalized; 965 if (stack instanceof Ci.nsIStackFrame) { 966 let frames = []; 967 for (let frame = stack; frame; frame = frame.caller) { 968 frames.push(frame.filename + ":" + frame.name + ":" + frame.lineNumber); 969 } 970 normalized = frames.join("\n"); 971 } else { 972 normalized = "" + stack; 973 } 974 return normalized; 975 } 976 977 // Make a nice display string from an object that behaves 978 // like Error 979 function _exception_message(ex) { 980 if (ex === undefined) { 981 return "`undefined` exception, maybe from an empty reject()?"; 982 } 983 let message = ""; 984 if (ex.name) { 985 message = ex.name + ": "; 986 } 987 if (ex.message) { 988 message += ex.message; 989 } 990 if (ex.fileName) { 991 message += " at " + ex.fileName; 992 if (ex.lineNumber) { 993 message += ":" + ex.lineNumber; 994 } 995 } 996 if (message !== "") { 997 return message; 998 } 999 // Force ex to be stringified 1000 return "" + ex; 1001 } 1002 1003 function do_report_unexpected_exception(ex, text) { 1004 let filename = Components.stack.caller.filename; 1005 text = text ? text + " - " : ""; 1006 1007 _passed = false; 1008 _testLogger.error(text + "Unexpected exception " + _exception_message(ex), { 1009 source_file: filename, 1010 stack: _format_stack(ex?.stack), 1011 }); 1012 _do_quit(); 1013 throw Components.Exception("", Cr.NS_ERROR_ABORT); 1014 } 1015 1016 function do_note_exception(ex, text) { 1017 let filename = Components.stack.caller.filename; 1018 _testLogger.info(text + "Swallowed exception " + _exception_message(ex), { 1019 source_file: filename, 1020 stack: _format_stack(ex?.stack), 1021 }); 1022 } 1023 1024 function do_report_result(passed, text, stack, todo) { 1025 // Match names like head.js, head_foo.js, and foo_head.js, but not 1026 // test_headache.js 1027 while (/(\/head(_.+)?|head)\.js$/.test(stack.filename) && stack.caller) { 1028 stack = stack.caller; 1029 } 1030 1031 let name = _gRunningTest ? _gRunningTest.name : stack.name; 1032 let message; 1033 if (name) { 1034 message = "[" + name + " : " + stack.lineNumber + "] " + text; 1035 } else { 1036 message = text; 1037 } 1038 1039 if (passed) { 1040 if (todo) { 1041 _testLogger.testStatus( 1042 _TEST_NAME, 1043 name, 1044 "PASS", 1045 "FAIL", 1046 message, 1047 _format_stack(stack) 1048 ); 1049 ChromeUtils.addProfilerMarker( 1050 "UNEXPECTED-PASS", 1051 { category: "Test" }, 1052 message 1053 ); 1054 _abort_failed_test(); 1055 } else { 1056 _testLogger.testStatus(_TEST_NAME, name, "PASS", "PASS", message); 1057 ChromeUtils.addProfilerMarker("PASS", { category: "Test" }, message); 1058 } 1059 } else if (todo) { 1060 _testLogger.testStatus(_TEST_NAME, name, "FAIL", "FAIL", message); 1061 ChromeUtils.addProfilerMarker("TODO", { category: "Test" }, message); 1062 } else { 1063 _testLogger.testStatus( 1064 _TEST_NAME, 1065 name, 1066 "FAIL", 1067 "PASS", 1068 message, 1069 _format_stack(stack) 1070 ); 1071 ChromeUtils.addProfilerMarker("FAIL", { category: "Test" }, message); 1072 _abort_failed_test(); 1073 } 1074 } 1075 1076 function _do_check_eq(left, right, stack, todo) { 1077 if (!stack) { 1078 stack = Components.stack.caller; 1079 } 1080 1081 var text = 1082 _wrap_with_quotes_if_necessary(left) + 1083 " == " + 1084 _wrap_with_quotes_if_necessary(right); 1085 do_report_result(left == right, text, stack, todo); 1086 } 1087 1088 function todo_check_eq(left, right, stack) { 1089 if (!stack) { 1090 stack = Components.stack.caller; 1091 } 1092 1093 _do_check_eq(left, right, stack, true); 1094 } 1095 1096 function todo_check_true(condition, stack) { 1097 if (!stack) { 1098 stack = Components.stack.caller; 1099 } 1100 1101 todo_check_eq(condition, true, stack); 1102 } 1103 1104 function todo_check_false(condition, stack) { 1105 if (!stack) { 1106 stack = Components.stack.caller; 1107 } 1108 1109 todo_check_eq(condition, false, stack); 1110 } 1111 1112 function todo_check_null(condition, stack = Components.stack.caller) { 1113 todo_check_eq(condition, null, stack); 1114 } 1115 1116 // Check that |func| throws an nsIException that has 1117 // |Components.results[resultName]| as the value of its 'result' property. 1118 function do_check_throws_nsIException( 1119 func, 1120 resultName, 1121 stack = Components.stack.caller, 1122 todo = false 1123 ) { 1124 let expected = Cr[resultName]; 1125 if (typeof expected !== "number") { 1126 do_throw( 1127 "do_check_throws_nsIException requires a Components.results" + 1128 " property name, not " + 1129 uneval(resultName), 1130 stack 1131 ); 1132 } 1133 1134 let msg = 1135 "do_check_throws_nsIException: func should throw" + 1136 " an nsIException whose 'result' is Components.results." + 1137 resultName; 1138 1139 try { 1140 func(); 1141 } catch (ex) { 1142 if (!(ex instanceof Ci.nsIException) || ex.result !== expected) { 1143 do_report_result( 1144 false, 1145 msg + ", threw " + legible_exception(ex) + " instead", 1146 stack, 1147 todo 1148 ); 1149 } 1150 1151 do_report_result(true, msg, stack, todo); 1152 return; 1153 } 1154 1155 // Call this here, not in the 'try' clause, so do_report_result's own 1156 // throw doesn't get caught by our 'catch' clause. 1157 do_report_result(false, msg + ", but returned normally", stack, todo); 1158 } 1159 1160 // Produce a human-readable form of |exception|. This looks up 1161 // Components.results values, tries toString methods, and so on. 1162 function legible_exception(exception) { 1163 switch (typeof exception) { 1164 case "object": 1165 if (exception instanceof Ci.nsIException) { 1166 return "nsIException instance: " + uneval(exception.toString()); 1167 } 1168 return exception.toString(); 1169 1170 case "number": 1171 for (let name in Cr) { 1172 if (exception === Cr[name]) { 1173 return "Components.results." + name; 1174 } 1175 } 1176 1177 // Fall through. 1178 default: 1179 return uneval(exception); 1180 } 1181 } 1182 1183 function do_check_instanceof( 1184 value, 1185 constructor, 1186 stack = Components.stack.caller, 1187 todo = false 1188 ) { 1189 do_report_result( 1190 value instanceof constructor, 1191 "value should be an instance of " + constructor.name, 1192 stack, 1193 todo 1194 ); 1195 } 1196 1197 function todo_check_instanceof( 1198 value, 1199 constructor, 1200 stack = Components.stack.caller 1201 ) { 1202 do_check_instanceof(value, constructor, stack, true); 1203 } 1204 1205 function do_test_pending(aName) { 1206 ++_tests_pending; 1207 1208 _testLogger.info( 1209 "(xpcshell/head.js) | test" + 1210 (aName ? " " + aName : "") + 1211 " pending (" + 1212 _tests_pending + 1213 ")" 1214 ); 1215 } 1216 1217 function do_test_finished(aName) { 1218 _testLogger.info( 1219 "(xpcshell/head.js) | test" + 1220 (aName ? " " + aName : "") + 1221 " finished (" + 1222 _tests_pending + 1223 ")" 1224 ); 1225 if (--_tests_pending == 0) { 1226 _do_quit(); 1227 } 1228 } 1229 1230 function do_get_file(path, allowNonexistent) { 1231 try { 1232 let lf = _Services.dirsvc.get("CurWorkD", Ci.nsIFile); 1233 1234 let bits = path.split("/"); 1235 for (let i = 0; i < bits.length; i++) { 1236 if (bits[i]) { 1237 if (bits[i] == "..") { 1238 lf = lf.parent; 1239 } else { 1240 lf.append(bits[i]); 1241 } 1242 } 1243 } 1244 1245 if (!allowNonexistent && !lf.exists()) { 1246 // Not using do_throw(): caller will continue. 1247 _passed = false; 1248 var stack = Components.stack.caller; 1249 _testLogger.error( 1250 "[" + 1251 stack.name + 1252 " : " + 1253 stack.lineNumber + 1254 "] " + 1255 lf.path + 1256 " does not exist" 1257 ); 1258 } 1259 1260 return lf; 1261 } catch (ex) { 1262 do_throw(ex.toString(), Components.stack.caller); 1263 } 1264 1265 return null; 1266 } 1267 1268 // do_get_cwd() isn't exactly self-explanatory, so provide a helper 1269 function do_get_cwd() { 1270 return do_get_file(""); 1271 } 1272 1273 function do_load_manifest(path) { 1274 var lf = do_get_file(path); 1275 const nsIComponentRegistrar = Ci.nsIComponentRegistrar; 1276 Assert.ok(Components.manager instanceof nsIComponentRegistrar); 1277 // Previous do_check_true() is not a test check. 1278 Components.manager.autoRegister(lf); 1279 } 1280 1281 /** 1282 * Parse a DOM document. 1283 * 1284 * @param aPath File path to the document. 1285 * @param aType Content type to use in DOMParser. 1286 * 1287 * @return Document from the file. 1288 */ 1289 function do_parse_document(aPath, aType) { 1290 switch (aType) { 1291 case "application/xhtml+xml": 1292 case "application/xml": 1293 case "text/xml": 1294 break; 1295 1296 default: 1297 do_throw( 1298 "type: expected application/xhtml+xml, application/xml or text/xml," + 1299 " got '" + 1300 aType + 1301 "'", 1302 Components.stack.caller 1303 ); 1304 } 1305 1306 let file = do_get_file(aPath); 1307 let url = _Services.io.newFileURI(file).spec; 1308 file = null; 1309 return new Promise((resolve, reject) => { 1310 let xhr = new XMLHttpRequest(); 1311 xhr.open("GET", url); 1312 xhr.responseType = "document"; 1313 xhr.onerror = reject; 1314 xhr.onload = () => { 1315 resolve(xhr.response); 1316 }; 1317 xhr.send(); 1318 }); 1319 } 1320 1321 /** 1322 * Registers a function that will run when the test harness is done running all 1323 * tests. 1324 * 1325 * @param aFunction 1326 * The function to be called when the test harness has finished running. 1327 */ 1328 function registerCleanupFunction(aFunction) { 1329 _cleanupFunctions.push(aFunction); 1330 } 1331 1332 /** 1333 * Returns the directory for a temp dir, which is created by the 1334 * test harness. Every test gets its own temp dir. 1335 * 1336 * @return nsIFile of the temporary directory 1337 */ 1338 function do_get_tempdir() { 1339 // the python harness sets this in the environment for us 1340 let path = _Services.env.get("XPCSHELL_TEST_TEMP_DIR"); 1341 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 1342 file.initWithPath(path); 1343 return file; 1344 } 1345 1346 /** 1347 * Returns the directory for crashreporter minidumps. 1348 * 1349 * @return nsIFile of the minidump directory 1350 */ 1351 function do_get_minidumpdir() { 1352 // the python harness may set this in the environment for us 1353 let path = _Services.env.get("XPCSHELL_MINIDUMP_DIR"); 1354 if (path) { 1355 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 1356 file.initWithPath(path); 1357 return file; 1358 } 1359 return do_get_tempdir(); 1360 } 1361 1362 /** 1363 * Registers a directory with the profile service, 1364 * and return the directory as an nsIFile. 1365 * 1366 * @param notifyProfileAfterChange Whether to notify for "profile-after-change". 1367 * @return nsIFile of the profile directory. 1368 */ 1369 function do_get_profile(notifyProfileAfterChange = false) { 1370 if (!runningInParent) { 1371 _testLogger.info("Ignoring profile creation from child process."); 1372 return null; 1373 } 1374 1375 // the python harness sets this in the environment for us 1376 let profd = Services.env.get("XPCSHELL_TEST_PROFILE_DIR"); 1377 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 1378 file.initWithPath(profd); 1379 1380 let provider = { 1381 getFile(prop, persistent) { 1382 persistent.value = true; 1383 if ( 1384 prop == "ProfD" || 1385 prop == "ProfLD" || 1386 prop == "ProfDS" || 1387 prop == "ProfLDS" || 1388 prop == "TmpD" 1389 ) { 1390 return file.clone(); 1391 } 1392 return null; 1393 }, 1394 QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]), 1395 }; 1396 _Services.dirsvc 1397 .QueryInterface(Ci.nsIDirectoryService) 1398 .registerProvider(provider); 1399 1400 try { 1401 _Services.dirsvc.undefine("TmpD"); 1402 } catch (e) { 1403 // This throws if the key is not already registered, but that 1404 // doesn't matter. 1405 if (e.result != Cr.NS_ERROR_FAILURE) { 1406 throw e; 1407 } 1408 } 1409 1410 // We need to update the crash events directory when the profile changes. 1411 if (runningInParent && "@mozilla.org/toolkit/crash-reporter;1" in Cc) { 1412 // Intentially access the crash reporter service directly for this. 1413 // eslint-disable-next-line mozilla/use-services 1414 let crashReporter = Cc["@mozilla.org/toolkit/crash-reporter;1"].getService( 1415 Ci.nsICrashReporter 1416 ); 1417 crashReporter.UpdateCrashEventsDir(); 1418 } 1419 1420 if (!_profileInitialized) { 1421 _Services.obs.notifyObservers( 1422 null, 1423 "profile-do-change", 1424 "xpcshell-do-get-profile" 1425 ); 1426 _profileInitialized = true; 1427 if (notifyProfileAfterChange) { 1428 _Services.obs.notifyObservers( 1429 null, 1430 "profile-after-change", 1431 "xpcshell-do-get-profile" 1432 ); 1433 1434 // The "profile-after-change" dispatch above does not reach services that 1435 // listen for the message via nsICategoryManager. These services register 1436 // their observer via `components.conf`. The message for these services 1437 // gets dispatched separately. So we have to do the same in testing. 1438 1439 // TODO: Bug 1995259: Not all tests / services expect this behavior yet, 1440 // use an allow-list to determine which services to initialize on 1441 // synthetic PAC. 1442 let filterServices = new Set([ 1443 "@mozilla.org/profile-after-change-gate;1", 1444 ]); 1445 1446 for (let entry of Services.catMan.enumerateCategory( 1447 "profile-after-change" 1448 )) { 1449 if (!filterServices.has(entry.value)) { 1450 continue; 1451 } 1452 try { 1453 Cc[entry.value] 1454 ?.getService(Ci.nsIObserver) 1455 ?.observe(null, "profile-after-change", ""); 1456 } catch (e) {} 1457 } 1458 } 1459 } 1460 1461 // The methods of 'provider' will retain this scope so null out everything 1462 // to avoid spurious leak reports. 1463 profd = null; 1464 provider = null; 1465 return file.clone(); 1466 } 1467 1468 /** 1469 * This function loads head.js (this file) in the child process, so that all 1470 * functions defined in this file (do_throw, etc) are available to subsequent 1471 * sendCommand calls. It also sets various constants used by these functions. 1472 * 1473 * (Note that you may use sendCommand without calling this function first; you 1474 * simply won't have any of the functions in this file available.) 1475 */ 1476 function do_load_child_test_harness() { 1477 // Make sure this isn't called from child process 1478 if (!runningInParent) { 1479 do_throw("run_test_in_child cannot be called from child!"); 1480 } 1481 1482 // Allow to be called multiple times, but only run once 1483 if (typeof do_load_child_test_harness.alreadyRun != "undefined") { 1484 return; 1485 } 1486 do_load_child_test_harness.alreadyRun = 1; 1487 1488 _XPCSHELL_PROCESS = "parent"; 1489 1490 let command = 1491 "const _HEAD_JS_PATH=" + 1492 uneval(_HEAD_JS_PATH) + 1493 "; " + 1494 "const _HEAD_FILES=" + 1495 uneval(_HEAD_FILES) + 1496 "; " + 1497 "const _MOZINFO_JS_PATH=" + 1498 uneval(_MOZINFO_JS_PATH) + 1499 "; " + 1500 "const _TEST_NAME=" + 1501 uneval(_TEST_NAME) + 1502 "; " + 1503 // We'll need more magic to get the debugger working in the child 1504 "const _JSDEBUGGER_PORT=0; " + 1505 "_XPCSHELL_PROCESS='child';"; 1506 1507 if (typeof _JSCOV_DIR === "string") { 1508 command += " const _JSCOV_DIR=" + uneval(_JSCOV_DIR) + ";"; 1509 } 1510 1511 if (typeof _TEST_CWD != "undefined") { 1512 command += " const _TEST_CWD=" + uneval(_TEST_CWD) + ";"; 1513 } 1514 1515 if (_TESTING_MODULES_DIR) { 1516 command += 1517 " const _TESTING_MODULES_DIR=" + uneval(_TESTING_MODULES_DIR) + ";"; 1518 } 1519 1520 command += " load(_HEAD_JS_PATH);"; 1521 sendCommand(command); 1522 } 1523 1524 /** 1525 * Runs an entire xpcshell unit test in a child process (rather than in chrome, 1526 * which is the default). 1527 * 1528 * This function returns immediately, before the test has completed. 1529 * 1530 * @param testFile 1531 * The name of the script to run. Path format same as load(). 1532 * @param optionalCallback. 1533 * Optional function to be called (in parent) when test on child is 1534 * complete. If provided, the function must call do_test_finished(); 1535 * @return Promise Resolved when the test in the child is complete. 1536 */ 1537 function run_test_in_child(testFile, optionalCallback) { 1538 return new Promise(resolve => { 1539 var callback = () => { 1540 resolve(); 1541 if (typeof optionalCallback == "undefined") { 1542 do_test_finished(); 1543 } else { 1544 optionalCallback(); 1545 } 1546 }; 1547 1548 do_load_child_test_harness(); 1549 1550 var testPath = do_get_file(testFile).path.replace(/\\/g, "/"); 1551 do_test_pending("run in child"); 1552 sendCommand( 1553 "_testLogger.info('CHILD-TEST-STARTED'); " + 1554 "const _TEST_FILE=['" + 1555 testPath + 1556 "']; " + 1557 "_execute_test(); " + 1558 "_testLogger.info('CHILD-TEST-COMPLETED');", 1559 callback 1560 ); 1561 }); 1562 } 1563 1564 /** 1565 * Execute a given function as soon as a particular cross-process message is received. 1566 * Must be paired with do_send_remote_message or equivalent ProcessMessageManager calls. 1567 * 1568 * @param optionalCallback 1569 * Optional callback that is invoked when the message is received. If provided, 1570 * the function must call do_test_finished(). 1571 * @return Promise Promise that is resolved when the message is received. 1572 */ 1573 function do_await_remote_message(name, optionalCallback) { 1574 return new Promise(resolve => { 1575 var listener = { 1576 receiveMessage(message) { 1577 if (message.name == name) { 1578 mm.removeMessageListener(name, listener); 1579 resolve(message.data); 1580 if (optionalCallback) { 1581 optionalCallback(message.data); 1582 } else { 1583 do_test_finished(); 1584 } 1585 } 1586 }, 1587 }; 1588 1589 var mm; 1590 if (runningInParent) { 1591 mm = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(); 1592 } else { 1593 mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(); 1594 } 1595 do_test_pending(); 1596 mm.addMessageListener(name, listener); 1597 }); 1598 } 1599 1600 /** 1601 * Asynchronously send a message to all remote processes. Pairs with do_await_remote_message 1602 * or equivalent ProcessMessageManager listeners. 1603 */ 1604 function do_send_remote_message(name, data) { 1605 var mm; 1606 var sender; 1607 if (runningInParent) { 1608 mm = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(); 1609 sender = "broadcastAsyncMessage"; 1610 } else { 1611 mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(); 1612 sender = "sendAsyncMessage"; 1613 } 1614 mm[sender](name, data); 1615 } 1616 1617 /** 1618 * Schedules and awaits a precise GC, and forces CC, `maxCount` number of times. 1619 * 1620 * @param maxCount 1621 * How many times GC and CC should be scheduled. 1622 */ 1623 async function schedulePreciseGCAndForceCC(maxCount) { 1624 for (let count = 0; count < maxCount; count++) { 1625 await new Promise(resolve => Cu.schedulePreciseGC(resolve)); 1626 Cu.forceCC(); 1627 } 1628 } 1629 1630 /** 1631 * Add a test function to the list of tests that are to be run asynchronously. 1632 * 1633 * @param funcOrProperties 1634 * A function to be run or an object represents test properties. 1635 * Supported properties: 1636 * skip_if : An arrow function which has an expression to be 1637 * evaluated whether the test is skipped or not. 1638 * pref_set: An array of preferences to set for the test, reset at end of test. 1639 * @param func 1640 * A function to be run only if the funcOrProperies is not a function. 1641 * @param isTask 1642 * Optional flag that indicates whether `func` is a task. Defaults to `false`. 1643 * @param isSetup 1644 * Optional flag that indicates whether `func` is a setup task. Defaults to `false`. 1645 * Implies isTask. 1646 * 1647 * Each test function must call run_next_test() when it's done. Test files 1648 * should call run_next_test() in their run_test function to execute all 1649 * async tests. 1650 * 1651 * @return the test function that was passed in. 1652 */ 1653 var _gSupportedProperties = ["skip_if", "pref_set"]; 1654 var _gTests = []; 1655 var _gRunOnlyThisTest = null; 1656 function add_test( 1657 properties, 1658 func = properties, 1659 isTask = false, 1660 isSetup = false 1661 ) { 1662 if (isSetup) { 1663 isTask = true; 1664 } 1665 if (typeof properties == "function") { 1666 properties = { isTask, isSetup }; 1667 _gTests.push([properties, func]); 1668 } else if (typeof properties == "object") { 1669 // Ensure only documented properties are in the object. 1670 for (let prop of Object.keys(properties)) { 1671 if (!_gSupportedProperties.includes(prop)) { 1672 do_throw(`Task property is not supported: ${prop}`); 1673 } 1674 } 1675 properties.isTask = isTask; 1676 properties.isSetup = isSetup; 1677 _gTests.push([properties, func]); 1678 } else { 1679 do_throw("add_test() should take a function or an object and a function"); 1680 } 1681 func.skip = () => (properties.skip_if = () => true); 1682 func.only = () => (_gRunOnlyThisTest = func); 1683 return func; 1684 } 1685 1686 /** 1687 * Add a test function which is an asynchronous function. 1688 * 1689 * @param funcOrProperties 1690 * An async function to be run or an object represents test properties. 1691 * Supported properties: 1692 * skip_if : An arrow function which has an expression to be 1693 * evaluated whether the test is skipped or not. 1694 * pref_set: An array of preferences to set for the test, reset at end of test. 1695 * @param func 1696 * An async function to be run only if the funcOrProperies is not a function. 1697 * 1698 * If an exception is thrown, a do_check_* comparison fails, or if a rejected 1699 * promise is yielded, the test function aborts immediately and the test is 1700 * reported as a failure. 1701 * 1702 * Unlike add_test(), there is no need to call run_next_test(). The next test 1703 * will run automatically as soon the task function is exhausted. To trigger 1704 * premature (but successful) termination of the function or simply return. 1705 * 1706 * Example usage: 1707 * 1708 * add_task(async function test() { 1709 * let result = await Promise.resolve(true); 1710 * 1711 * do_check_true(result); 1712 * 1713 * let secondary = await someFunctionThatReturnsAPromise(result); 1714 * do_check_eq(secondary, "expected value"); 1715 * }); 1716 * 1717 * add_task(async function test_early_return() { 1718 * let result = await somethingThatReturnsAPromise(); 1719 * 1720 * if (!result) { 1721 * // Test is ended immediately, with success. 1722 * return; 1723 * } 1724 * 1725 * do_check_eq(result, "foo"); 1726 * }); 1727 * 1728 * add_task({ 1729 * skip_if: () => !("@mozilla.org/telephony/volume-service;1" in Components.classes), 1730 * pref_set: [["some.pref", "value"], ["another.pref", true]], 1731 * }, async function test_volume_service() { 1732 * let volumeService = Cc["@mozilla.org/telephony/volume-service;1"] 1733 * .getService(Ci.nsIVolumeService); 1734 * ... 1735 * }); 1736 */ 1737 function add_task(properties, func = properties) { 1738 return add_test(properties, func, true); 1739 } 1740 1741 /** 1742 * add_setup is like add_task, but creates setup tasks. 1743 */ 1744 function add_setup(properties, func = properties) { 1745 return add_test(properties, func, true, true); 1746 } 1747 1748 const _setTaskPrefs = prefs => { 1749 for (let [pref, value] of prefs) { 1750 if (value === undefined) { 1751 // Clear any pref that didn't have a user value. 1752 info(`Clearing pref "${pref}"`); 1753 _Services.prefs.clearUserPref(pref); 1754 continue; 1755 } 1756 1757 info(`Setting pref "${pref}": ${value}`); 1758 switch (typeof value) { 1759 case "boolean": 1760 _Services.prefs.setBoolPref(pref, value); 1761 break; 1762 case "number": 1763 _Services.prefs.setIntPref(pref, value); 1764 break; 1765 case "string": 1766 _Services.prefs.setStringPref(pref, value); 1767 break; 1768 default: 1769 throw new Error("runWithPrefs doesn't support this pref type yet"); 1770 } 1771 } 1772 }; 1773 1774 const _getTaskPrefs = prefs => { 1775 return prefs.map(([pref, value]) => { 1776 info(`Getting initial pref value for "${pref}"`); 1777 if (!_Services.prefs.prefHasUserValue(pref)) { 1778 // Check if the pref doesn't have a user value. 1779 return [pref, undefined]; 1780 } 1781 switch (typeof value) { 1782 case "boolean": 1783 return [pref, _Services.prefs.getBoolPref(pref)]; 1784 case "number": 1785 return [pref, _Services.prefs.getIntPref(pref)]; 1786 case "string": 1787 return [pref, _Services.prefs.getStringPref(pref)]; 1788 default: 1789 throw new Error("runWithPrefs doesn't support this pref type yet"); 1790 } 1791 }); 1792 }; 1793 1794 /** 1795 * Runs the next test function from the list of async tests. 1796 */ 1797 var _gRunningTest = null; 1798 var _gTestIndex = 0; // The index of the currently running test. 1799 var _gTaskRunning = false; 1800 var _gSetupRunning = false; 1801 function run_next_test() { 1802 if (_gTaskRunning) { 1803 throw new Error( 1804 "run_next_test() called from an add_task() test function. " + 1805 "run_next_test() should not be called from inside add_setup() or add_task() " + 1806 "under any circumstances!" 1807 ); 1808 } 1809 1810 if (_gSetupRunning) { 1811 throw new Error( 1812 "run_next_test() called from an add_setup() test function. " + 1813 "run_next_test() should not be called from inside add_setup() or add_task() " + 1814 "under any circumstances!" 1815 ); 1816 } 1817 1818 function _run_next_test() { 1819 if (_gTestIndex < _gTests.length) { 1820 // Check for uncaught rejections as early and often as possible. 1821 _PromiseTestUtils.assertNoUncaughtRejections(); 1822 let _properties; 1823 [_properties, _gRunningTest] = _gTests[_gTestIndex++]; 1824 1825 // Must set to pending before we check for skip, so that we keep the 1826 // running counts correct. 1827 _testLogger.info( 1828 `${_TEST_NAME} | Starting ${_properties.isSetup ? "setup " : ""}${ 1829 _gRunningTest.name 1830 }` 1831 ); 1832 do_test_pending(_gRunningTest.name); 1833 1834 if ( 1835 (typeof _properties.skip_if == "function" && _properties.skip_if()) || 1836 (_gRunOnlyThisTest && 1837 _gRunningTest != _gRunOnlyThisTest && 1838 !_properties.isSetup) 1839 ) { 1840 let _condition = _gRunOnlyThisTest 1841 ? "only one task may run." 1842 : _properties.skip_if.toSource().replace(/\(\)\s*=>\s*/, ""); 1843 if (_condition == "true") { 1844 _condition = "explicitly skipped."; 1845 } 1846 let _message = 1847 _gRunningTest.name + 1848 " skipped because the following conditions were" + 1849 " met: (" + 1850 _condition + 1851 ")"; 1852 _testLogger.testStatus( 1853 _TEST_NAME, 1854 _gRunningTest.name, 1855 "SKIP", 1856 "SKIP", 1857 _message 1858 ); 1859 executeSoon(run_next_test); 1860 return; 1861 } 1862 1863 let initialPrefsValues = []; 1864 if (_properties.pref_set) { 1865 initialPrefsValues = _getTaskPrefs(_properties.pref_set); 1866 _setTaskPrefs(_properties.pref_set); 1867 } 1868 1869 if (_properties.isTask) { 1870 if (_properties.isSetup) { 1871 _gSetupRunning = true; 1872 } else { 1873 _gTaskRunning = true; 1874 } 1875 let startTime = ChromeUtils.now(); 1876 (async () => _gRunningTest())().then( 1877 result => { 1878 _gTaskRunning = _gSetupRunning = false; 1879 ChromeUtils.addProfilerMarker( 1880 "task", 1881 { category: "Test", startTime }, 1882 _gRunningTest.name || undefined 1883 ); 1884 if (_isGenerator(result)) { 1885 Assert.ok(false, "Task returned a generator"); 1886 } 1887 _setTaskPrefs(initialPrefsValues); 1888 run_next_test(); 1889 }, 1890 ex => { 1891 _gTaskRunning = _gSetupRunning = false; 1892 ChromeUtils.addProfilerMarker( 1893 "task", 1894 { category: "Test", startTime }, 1895 _gRunningTest.name || undefined 1896 ); 1897 _setTaskPrefs(initialPrefsValues); 1898 try { 1899 // Note `ex` at this point could be undefined, for example as 1900 // result of a bare call to reject(). 1901 do_report_unexpected_exception(ex); 1902 } catch (error) { 1903 // The above throws NS_ERROR_ABORT and we don't want this to show 1904 // up as an unhandled rejection later. If any other exception 1905 // happened, something went wrong, so we abort. 1906 if (error.result != Cr.NS_ERROR_ABORT) { 1907 let extra = {}; 1908 if (error.fileName) { 1909 extra.source_file = error.fileName; 1910 if (error.lineNumber) { 1911 extra.line_number = error.lineNumber; 1912 } 1913 } else { 1914 extra.source_file = "xpcshell/head.js"; 1915 } 1916 if (error.stack) { 1917 extra.stack = _format_stack(error.stack); 1918 } 1919 _testLogger.error(_exception_message(error), extra); 1920 _do_quit(); 1921 throw Components.Exception("", Cr.NS_ERROR_ABORT); 1922 } 1923 } 1924 } 1925 ); 1926 } else { 1927 // Exceptions do not kill asynchronous tests, so they'll time out. 1928 let startTime = ChromeUtils.now(); 1929 try { 1930 _gRunningTest(); 1931 } catch (e) { 1932 do_throw(e); 1933 } finally { 1934 ChromeUtils.addProfilerMarker( 1935 "xpcshell-test", 1936 { category: "Test", startTime }, 1937 _gRunningTest.name || undefined 1938 ); 1939 _setTaskPrefs(initialPrefsValues); 1940 } 1941 } 1942 } 1943 } 1944 1945 function frontLoadSetups() { 1946 _gTests.sort(([propsA], [propsB]) => { 1947 if (propsB.isSetup === propsA.isSetup) { 1948 return 0; 1949 } 1950 return propsB.isSetup ? 1 : -1; 1951 }); 1952 } 1953 1954 if (!_gTestIndex) { 1955 frontLoadSetups(); 1956 } 1957 1958 // For sane stacks during failures, we execute this code soon, but not now. 1959 // We do this now, before we call do_test_finished(), to ensure the pending 1960 // counter (_tests_pending) never reaches 0 while we still have tests to run 1961 // (executeSoon bumps that counter). 1962 executeSoon(_run_next_test, "run_next_test " + _gTestIndex); 1963 1964 if (_gRunningTest !== null) { 1965 // Close the previous test do_test_pending call. 1966 do_test_finished(_gRunningTest.name); 1967 } 1968 } 1969 1970 try { 1971 // Set global preferences 1972 if (runningInParent) { 1973 let prefsFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 1974 prefsFile.initWithPath(_PREFS_FILE); 1975 _Services.prefs.readUserPrefsFromFile(prefsFile); 1976 } 1977 } catch (e) { 1978 do_throw(e); 1979 } 1980 1981 /** 1982 * Changing/Adding scalars or events to Telemetry is supported in build-faster/artifacts builds. 1983 * These need to be loaded explicitly at start. 1984 * It usually happens once all of Telemetry is initialized and set up. 1985 * However in xpcshell tests Telemetry is not necessarily fully loaded, 1986 * so we help out users by loading at least the dynamic-builtin probes. 1987 */ 1988 try { 1989 // We only need to run this in the parent process. 1990 // We only want to run this for local developer builds (which should have a "default" update channel). 1991 if (runningInParent && _AppConstants.MOZ_UPDATE_CHANNEL == "default") { 1992 let startTime = ChromeUtils.now(); 1993 let { TelemetryController: _TelemetryController } = 1994 ChromeUtils.importESModule( 1995 "resource://gre/modules/TelemetryController.sys.mjs" 1996 ); 1997 1998 let complete = false; 1999 _TelemetryController.testRegisterJsProbes().finally(() => { 2000 ChromeUtils.addProfilerMarker( 2001 "xpcshell-test", 2002 { category: "Test", startTime }, 2003 "TelemetryController.testRegisterJsProbes" 2004 ); 2005 complete = true; 2006 }); 2007 _Services.tm.spinEventLoopUntil( 2008 "Test(xpcshell/head.js:run_next-Test)", 2009 () => complete 2010 ); 2011 } 2012 } catch (e) { 2013 do_throw(e); 2014 } 2015 2016 function _load_mozinfo() { 2017 let mozinfoFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 2018 mozinfoFile.initWithPath(_MOZINFO_JS_PATH); 2019 let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( 2020 Ci.nsIFileInputStream 2021 ); 2022 stream.init(mozinfoFile, -1, 0, 0); 2023 let bytes = _NetUtil.readInputStream(stream, stream.available()); 2024 let decoded = JSON.parse(new TextDecoder().decode(bytes)); 2025 stream.close(); 2026 return decoded; 2027 } 2028 2029 Object.defineProperty(this, "mozinfo", { 2030 configurable: true, 2031 get() { 2032 let _mozinfo = _load_mozinfo(); 2033 Object.defineProperty(this, "mozinfo", { 2034 configurable: false, 2035 value: _mozinfo, 2036 }); 2037 return _mozinfo; 2038 }, 2039 });