michael@0: /* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim:set ts=2 sw=2 sts=2 et: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /* michael@0: * This file contains common code that is loaded before each test file(s). michael@0: * See http://developer.mozilla.org/en/docs/Writing_xpcshell-based_unit_tests michael@0: * for more information. michael@0: */ michael@0: michael@0: var _quit = false; michael@0: var _passed = true; michael@0: var _tests_pending = 0; michael@0: var _passedChecks = 0, _falsePassedChecks = 0; michael@0: var _todoChecks = 0; michael@0: var _cleanupFunctions = []; michael@0: var _pendingTimers = []; michael@0: var _profileInitialized = false; michael@0: michael@0: let _Promise = Components.utils.import("resource://gre/modules/Promise.jsm", this).Promise; michael@0: michael@0: let _log = function (action, params) { michael@0: if (typeof _XPCSHELL_PROCESS != "undefined") { michael@0: params.process = _XPCSHELL_PROCESS; michael@0: } michael@0: params.action = action; michael@0: params._time = Date.now(); michael@0: dump("\n" + JSON.stringify(params) + "\n"); michael@0: } michael@0: michael@0: function _dump(str) { michael@0: let start = /^TEST-/.test(str) ? "\n" : ""; michael@0: if (typeof _XPCSHELL_PROCESS == "undefined") { michael@0: dump(start + str); michael@0: } else { michael@0: dump(start + _XPCSHELL_PROCESS + ": " + str); michael@0: } michael@0: } michael@0: michael@0: // Disable automatic network detection, so tests work correctly when michael@0: // not connected to a network. michael@0: let (ios = Components.classes["@mozilla.org/network/io-service;1"] michael@0: .getService(Components.interfaces.nsIIOService2)) { michael@0: ios.manageOfflineStatus = false; michael@0: ios.offline = false; michael@0: } michael@0: michael@0: // Determine if we're running on parent or child michael@0: let runningInParent = true; michael@0: try { michael@0: runningInParent = Components.classes["@mozilla.org/xre/runtime;1"]. michael@0: getService(Components.interfaces.nsIXULRuntime).processType michael@0: == Components.interfaces.nsIXULRuntime.PROCESS_TYPE_DEFAULT; michael@0: } michael@0: catch (e) { } michael@0: michael@0: // Only if building of places is enabled. michael@0: if (runningInParent && michael@0: "mozIAsyncHistory" in Components.interfaces) { michael@0: // Ensure places history is enabled for xpcshell-tests as some non-FF michael@0: // apps disable it. michael@0: let (prefs = Components.classes["@mozilla.org/preferences-service;1"] michael@0: .getService(Components.interfaces.nsIPrefBranch)) { michael@0: prefs.setBoolPref("places.history.enabled", true); michael@0: }; michael@0: } michael@0: michael@0: try { michael@0: if (runningInParent) { michael@0: let prefs = Components.classes["@mozilla.org/preferences-service;1"] michael@0: .getService(Components.interfaces.nsIPrefBranch); michael@0: michael@0: // disable necko IPC security checks for xpcshell, as they lack the michael@0: // docshells needed to pass them michael@0: prefs.setBoolPref("network.disable.ipc.security", true); michael@0: michael@0: // Disable IPv6 lookups for 'localhost' on windows. michael@0: if ("@mozilla.org/windows-registry-key;1" in Components.classes) { michael@0: prefs.setCharPref("network.dns.ipv4OnlyDomains", "localhost"); michael@0: } michael@0: } michael@0: } michael@0: catch (e) { } michael@0: michael@0: // Configure crash reporting, if possible michael@0: // We rely on the Python harness to set MOZ_CRASHREPORTER, michael@0: // MOZ_CRASHREPORTER_NO_REPORT, and handle checking for minidumps. michael@0: // Note that if we're in a child process, we don't want to init the michael@0: // crashreporter component. michael@0: try { michael@0: if (runningInParent && michael@0: "@mozilla.org/toolkit/crash-reporter;1" in Components.classes) { michael@0: let (crashReporter = michael@0: Components.classes["@mozilla.org/toolkit/crash-reporter;1"] michael@0: .getService(Components.interfaces.nsICrashReporter)) { michael@0: crashReporter.UpdateCrashEventsDir(); michael@0: crashReporter.minidumpPath = do_get_minidumpdir(); michael@0: } michael@0: } michael@0: } michael@0: catch (e) { } michael@0: michael@0: /** michael@0: * Date.now() is not necessarily monotonically increasing (insert sob story michael@0: * about times not being the right tool to use for measuring intervals of time, michael@0: * robarnold can tell all), so be wary of error by erring by at least michael@0: * _timerFuzz ms. michael@0: */ michael@0: const _timerFuzz = 15; michael@0: michael@0: function _Timer(func, delay) { michael@0: delay = Number(delay); michael@0: if (delay < 0) michael@0: do_throw("do_timeout() delay must be nonnegative"); michael@0: michael@0: if (typeof func !== "function") michael@0: do_throw("string callbacks no longer accepted; use a function!"); michael@0: michael@0: this._func = func; michael@0: this._start = Date.now(); michael@0: this._delay = delay; michael@0: michael@0: var timer = Components.classes["@mozilla.org/timer;1"] michael@0: .createInstance(Components.interfaces.nsITimer); michael@0: timer.initWithCallback(this, delay + _timerFuzz, timer.TYPE_ONE_SHOT); michael@0: michael@0: // Keep timer alive until it fires michael@0: _pendingTimers.push(timer); michael@0: } michael@0: _Timer.prototype = { michael@0: QueryInterface: function(iid) { michael@0: if (iid.equals(Components.interfaces.nsITimerCallback) || michael@0: iid.equals(Components.interfaces.nsISupports)) michael@0: return this; michael@0: michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: michael@0: notify: function(timer) { michael@0: _pendingTimers.splice(_pendingTimers.indexOf(timer), 1); michael@0: michael@0: // The current nsITimer implementation can undershoot, but even if it michael@0: // couldn't, paranoia is probably a virtue here given the potential for michael@0: // random orange on tinderboxen. michael@0: var end = Date.now(); michael@0: var elapsed = end - this._start; michael@0: if (elapsed >= this._delay) { michael@0: try { michael@0: this._func.call(null); michael@0: } catch (e) { michael@0: do_throw("exception thrown from do_timeout callback: " + e); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: // Timer undershot, retry with a little overshoot to try to avoid more michael@0: // undershoots. michael@0: var newDelay = this._delay - elapsed; michael@0: do_timeout(newDelay, this._func); michael@0: } michael@0: }; michael@0: michael@0: function _do_main() { michael@0: if (_quit) michael@0: return; michael@0: michael@0: _log("test_info", michael@0: {_message: "TEST-INFO | (xpcshell/head.js) | running event loop\n"}); michael@0: michael@0: var thr = Components.classes["@mozilla.org/thread-manager;1"] michael@0: .getService().currentThread; michael@0: michael@0: while (!_quit) michael@0: thr.processNextEvent(true); michael@0: michael@0: while (thr.hasPendingEvents()) michael@0: thr.processNextEvent(true); michael@0: } michael@0: michael@0: function _do_quit() { michael@0: _log("test_info", michael@0: {_message: "TEST-INFO | (xpcshell/head.js) | exiting test\n"}); michael@0: _Promise.Debugging.flushUncaughtErrors(); michael@0: _quit = true; michael@0: } michael@0: michael@0: function _format_exception_stack(stack) { michael@0: if (typeof stack == "object" && stack.caller) { michael@0: let frame = stack; michael@0: let strStack = ""; michael@0: while (frame != null) { michael@0: strStack += frame + "\n"; michael@0: frame = frame.caller; michael@0: } michael@0: stack = strStack; michael@0: } michael@0: // frame is of the form "fname@file:line" michael@0: let frame_regexp = new RegExp("(.*)@(.*):(\\d*)", "g"); michael@0: return stack.split("\n").reduce(function(stack_msg, frame) { michael@0: if (frame) { michael@0: let parts = frame_regexp.exec(frame); michael@0: if (parts) { michael@0: let [ _, func, file, line ] = parts; michael@0: return stack_msg + "JS frame :: " + file + " :: " + michael@0: (func || "anonymous") + " :: line " + line + "\n"; michael@0: } michael@0: else { /* Could be a -e (command line string) style location. */ michael@0: return stack_msg + "JS frame :: " + frame + "\n"; michael@0: } michael@0: } michael@0: return stack_msg; michael@0: }, ""); michael@0: } michael@0: michael@0: /** michael@0: * Overrides idleService with a mock. Idle is commonly used for maintenance michael@0: * tasks, thus if a test uses a service that requires the idle service, it will michael@0: * start handling them. michael@0: * This behaviour would cause random failures and slowdown tests execution, michael@0: * for example by running database vacuum or cleanups for each test. michael@0: * michael@0: * @note Idle service is overridden by default. If a test requires it, it will michael@0: * have to call do_get_idle() function at least once before use. michael@0: */ michael@0: var _fakeIdleService = { michael@0: get registrar() { michael@0: delete this.registrar; michael@0: return this.registrar = michael@0: Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar); michael@0: }, michael@0: contractID: "@mozilla.org/widget/idleservice;1", michael@0: get CID() this.registrar.contractIDToCID(this.contractID), michael@0: michael@0: activate: function FIS_activate() michael@0: { michael@0: if (!this.originalFactory) { michael@0: // Save original factory. michael@0: this.originalFactory = michael@0: Components.manager.getClassObject(Components.classes[this.contractID], michael@0: Components.interfaces.nsIFactory); michael@0: // Unregister original factory. michael@0: this.registrar.unregisterFactory(this.CID, this.originalFactory); michael@0: // Replace with the mock. michael@0: this.registrar.registerFactory(this.CID, "Fake Idle Service", michael@0: this.contractID, this.factory michael@0: ); michael@0: } michael@0: }, michael@0: michael@0: deactivate: function FIS_deactivate() michael@0: { michael@0: if (this.originalFactory) { michael@0: // Unregister the mock. michael@0: this.registrar.unregisterFactory(this.CID, this.factory); michael@0: // Restore original factory. michael@0: this.registrar.registerFactory(this.CID, "Idle Service", michael@0: this.contractID, this.originalFactory); michael@0: delete this.originalFactory; michael@0: } michael@0: }, michael@0: michael@0: factory: { michael@0: // nsIFactory michael@0: createInstance: function (aOuter, aIID) michael@0: { michael@0: if (aOuter) { michael@0: throw Components.results.NS_ERROR_NO_AGGREGATION; michael@0: } michael@0: return _fakeIdleService.QueryInterface(aIID); michael@0: }, michael@0: lockFactory: function (aLock) { michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: }, michael@0: QueryInterface: function(aIID) { michael@0: if (aIID.equals(Components.interfaces.nsIFactory) || michael@0: aIID.equals(Components.interfaces.nsISupports)) { michael@0: return this; michael@0: } michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: }, michael@0: michael@0: // nsIIdleService michael@0: get idleTime() 0, michael@0: addIdleObserver: function () {}, michael@0: removeIdleObserver: function () {}, michael@0: michael@0: QueryInterface: function(aIID) { michael@0: // Useful for testing purposes, see test_get_idle.js. michael@0: if (aIID.equals(Components.interfaces.nsIFactory)) { michael@0: return this.factory; michael@0: } michael@0: if (aIID.equals(Components.interfaces.nsIIdleService) || michael@0: aIID.equals(Components.interfaces.nsISupports)) { michael@0: return this; michael@0: } michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Restores the idle service factory if needed and returns the service's handle. michael@0: * @return A handle to the idle service. michael@0: */ michael@0: function do_get_idle() { michael@0: _fakeIdleService.deactivate(); michael@0: return Components.classes[_fakeIdleService.contractID] michael@0: .getService(Components.interfaces.nsIIdleService); michael@0: } michael@0: michael@0: // Map resource://test/ to current working directory and michael@0: // resource://testing-common/ to the shared test modules directory. michael@0: function _register_protocol_handlers() { michael@0: let (ios = Components.classes["@mozilla.org/network/io-service;1"] michael@0: .getService(Components.interfaces.nsIIOService)) { michael@0: let protocolHandler = michael@0: ios.getProtocolHandler("resource") michael@0: .QueryInterface(Components.interfaces.nsIResProtocolHandler); michael@0: let curDirURI = ios.newFileURI(do_get_cwd()); michael@0: protocolHandler.setSubstitution("test", curDirURI); michael@0: michael@0: if (this._TESTING_MODULES_DIR) { michael@0: let modulesFile = Components.classes["@mozilla.org/file/local;1"]. michael@0: createInstance(Components.interfaces.nsILocalFile); michael@0: modulesFile.initWithPath(_TESTING_MODULES_DIR); michael@0: michael@0: if (!modulesFile.exists()) { michael@0: throw new Error("Specified modules directory does not exist: " + michael@0: _TESTING_MODULES_DIR); michael@0: } michael@0: michael@0: if (!modulesFile.isDirectory()) { michael@0: throw new Error("Specified modules directory is not a directory: " + michael@0: _TESTING_MODULES_DIR); michael@0: } michael@0: michael@0: let modulesURI = ios.newFileURI(modulesFile); michael@0: michael@0: protocolHandler.setSubstitution("testing-common", modulesURI); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function _execute_test() { michael@0: _register_protocol_handlers(); michael@0: michael@0: // Override idle service by default. michael@0: // Call do_get_idle() to restore the factory and get the service. michael@0: _fakeIdleService.activate(); michael@0: michael@0: _Promise.Debugging.clearUncaughtErrorObservers(); michael@0: _Promise.Debugging.addUncaughtErrorObserver(function observer({message, date, fileName, stack, lineNumber}) { michael@0: let text = "Once bug 976205 has landed, THIS ERROR WILL CAUSE A TEST FAILURE.\n" + michael@0: " A promise chain failed to handle a rejection: " + michael@0: message + " - rejection date: " + date; michael@0: _log_message_with_stack("test_known_fail", michael@0: text, stack, fileName); michael@0: }); michael@0: michael@0: // _HEAD_FILES is dynamically defined by . michael@0: _load_files(_HEAD_FILES); michael@0: // _TEST_FILE is dynamically defined by . michael@0: _load_files(_TEST_FILE); michael@0: michael@0: // Support a common assertion library, Assert.jsm. michael@0: let Assert = Components.utils.import("resource://testing-common/Assert.jsm", null).Assert; michael@0: // Pass a custom report function for xpcshell-test style reporting. michael@0: let assertImpl = new Assert(function(err, message, stack) { michael@0: if (err) { michael@0: do_report_result(false, err.message, err.stack); michael@0: } else { michael@0: do_report_result(true, message, stack); michael@0: } michael@0: }); michael@0: // Allow Assert.jsm methods to be tacked to the current scope. michael@0: this.export_assertions = function() { michael@0: for (let func in assertImpl) { michael@0: this[func] = assertImpl[func].bind(assertImpl); michael@0: } michael@0: }; michael@0: this.Assert = assertImpl; michael@0: michael@0: try { michael@0: do_test_pending("MAIN run_test"); michael@0: run_test(); michael@0: do_test_finished("MAIN run_test"); michael@0: _do_main(); michael@0: } catch (e) { michael@0: _passed = false; michael@0: // do_check failures are already logged and set _quit to true and throw michael@0: // NS_ERROR_ABORT. If both of those are true it is likely this exception michael@0: // has already been logged so there is no need to log it again. It's michael@0: // possible that this will mask an NS_ERROR_ABORT that happens after a michael@0: // do_check failure though. michael@0: if (!_quit || e != Components.results.NS_ERROR_ABORT) { michael@0: let msgObject = {}; michael@0: if (e.fileName) { michael@0: msgObject.source_file = e.fileName; michael@0: if (e.lineNumber) { michael@0: msgObject.line_number = e.lineNumber; michael@0: } michael@0: } else { michael@0: msgObject.source_file = "xpcshell/head.js"; michael@0: } michael@0: msgObject.diagnostic = _exception_message(e); michael@0: if (e.stack) { michael@0: msgObject.diagnostic += " - See following stack:\n"; michael@0: msgObject.stack = _format_exception_stack(e.stack); michael@0: } michael@0: _log("test_unexpected_fail", msgObject); michael@0: } michael@0: } michael@0: michael@0: // _TAIL_FILES is dynamically defined by . michael@0: _load_files(_TAIL_FILES); michael@0: michael@0: // Execute all of our cleanup functions. michael@0: let reportCleanupError = function(ex) { michael@0: let stack, filename; michael@0: if (ex && typeof ex == "object" && "stack" in ex) { michael@0: stack = ex.stack; michael@0: } else { michael@0: stack = Components.stack.caller; michael@0: } michael@0: if (stack instanceof Components.interfaces.nsIStackFrame) { michael@0: filename = stack.filename; michael@0: } else if (ex.fileName) { michael@0: filename = ex.fileName; michael@0: } michael@0: _log_message_with_stack("test_unexpected_fail", michael@0: ex, stack, filename); michael@0: }; michael@0: michael@0: let func; michael@0: while ((func = _cleanupFunctions.pop())) { michael@0: let result; michael@0: try { michael@0: result = func(); michael@0: } catch (ex) { michael@0: reportCleanupError(ex); michael@0: continue; michael@0: } michael@0: if (result && typeof result == "object" michael@0: && "then" in result && typeof result.then == "function") { michael@0: // This is a promise, wait until it is satisfied before proceeding michael@0: let complete = false; michael@0: let promise = result.then(null, reportCleanupError); michael@0: promise = promise.then(() => complete = true); michael@0: let thr = Components.classes["@mozilla.org/thread-manager;1"] michael@0: .getService().currentThread; michael@0: while (!complete) { michael@0: thr.processNextEvent(true); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Restore idle service to avoid leaks. michael@0: _fakeIdleService.deactivate(); michael@0: michael@0: if (!_passed) michael@0: return; michael@0: michael@0: var truePassedChecks = _passedChecks - _falsePassedChecks; michael@0: if (truePassedChecks > 0) { michael@0: _log("test_pass", michael@0: {_message: "TEST-PASS | (xpcshell/head.js) | " + truePassedChecks + " (+ " + michael@0: _falsePassedChecks + ") check(s) passed\n", michael@0: source_file: _TEST_FILE, michael@0: passed_checks: truePassedChecks}); michael@0: _log("test_info", michael@0: {_message: "TEST-INFO | (xpcshell/head.js) | " + _todoChecks + michael@0: " check(s) todo\n", michael@0: source_file: _TEST_FILE, michael@0: todo_checks: _todoChecks}); michael@0: } else { michael@0: // ToDo: switch to TEST-UNEXPECTED-FAIL when all tests have been updated. (Bug 496443) michael@0: _log("test_info", michael@0: {_message: "TEST-INFO | (xpcshell/head.js) | No (+ " + _falsePassedChecks + michael@0: ") checks actually run\n", michael@0: source_file: _TEST_FILE}); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Loads files. michael@0: * michael@0: * @param aFiles Array of files to load. michael@0: */ michael@0: function _load_files(aFiles) { michael@0: function loadTailFile(element, index, array) { michael@0: try { michael@0: load(element); michael@0: } catch (e if e instanceof SyntaxError) { michael@0: _log("javascript_error", michael@0: {_message: "TEST-UNEXPECTED-FAIL | (xpcshell/head.js) | Source file " + element + " contains SyntaxError", michael@0: diagnostic: _exception_message(e), michael@0: source_file: element, michael@0: stack: _format_exception_stack(e.stack)}); michael@0: } catch (e) { michael@0: _log("javascript_error", michael@0: {_message: "TEST-UNEXPECTED-FAIL | (xpcshell/head.js) | Source file " + element + " contains an error", michael@0: diagnostic: _exception_message(e), michael@0: source_file: element, michael@0: stack: _format_exception_stack(e.stack)}); michael@0: } michael@0: } michael@0: michael@0: aFiles.forEach(loadTailFile); michael@0: } michael@0: michael@0: function _wrap_with_quotes_if_necessary(val) { michael@0: return typeof val == "string" ? '"' + val + '"' : val; michael@0: } michael@0: michael@0: /************** Functions to be used from the tests **************/ michael@0: michael@0: /** michael@0: * Prints a message to the output log. michael@0: */ michael@0: function do_print(msg) { michael@0: var caller_stack = Components.stack.caller; michael@0: msg = _wrap_with_quotes_if_necessary(msg); michael@0: _log("test_info", michael@0: {source_file: caller_stack.filename, michael@0: diagnostic: msg}); michael@0: michael@0: } michael@0: michael@0: /** michael@0: * Calls the given function at least the specified number of milliseconds later. michael@0: * The callback will not undershoot the given time, but it might overshoot -- michael@0: * don't expect precision! michael@0: * michael@0: * @param delay : uint michael@0: * the number of milliseconds to delay michael@0: * @param callback : function() : void michael@0: * the function to call michael@0: */ michael@0: function do_timeout(delay, func) { michael@0: new _Timer(func, Number(delay)); michael@0: } michael@0: michael@0: function do_execute_soon(callback, aName) { michael@0: let funcName = (aName ? aName : callback.name); michael@0: do_test_pending(funcName); michael@0: var tm = Components.classes["@mozilla.org/thread-manager;1"] michael@0: .getService(Components.interfaces.nsIThreadManager); michael@0: michael@0: tm.mainThread.dispatch({ michael@0: run: function() { michael@0: try { michael@0: callback(); michael@0: } catch (e) { michael@0: // do_check failures are already logged and set _quit to true and throw michael@0: // NS_ERROR_ABORT. If both of those are true it is likely this exception michael@0: // has already been logged so there is no need to log it again. It's michael@0: // possible that this will mask an NS_ERROR_ABORT that happens after a michael@0: // do_check failure though. michael@0: if (!_quit || e != Components.results.NS_ERROR_ABORT) { michael@0: if (e.stack) { michael@0: _log("javascript_error", michael@0: {source_file: "xpcshell/head.js", michael@0: diagnostic: _exception_message(e) + " - See following stack:", michael@0: stack: _format_exception_stack(e.stack)}); michael@0: } else { michael@0: _log("javascript_error", michael@0: {source_file: "xpcshell/head.js", michael@0: diagnostic: _exception_message(e)}); michael@0: } michael@0: _do_quit(); michael@0: } michael@0: } michael@0: finally { michael@0: do_test_finished(funcName); michael@0: } michael@0: } michael@0: }, Components.interfaces.nsIThread.DISPATCH_NORMAL); michael@0: } michael@0: michael@0: /** michael@0: * Shows an error message and the current stack and aborts the test. michael@0: * michael@0: * @param error A message string or an Error object. michael@0: * @param stack null or nsIStackFrame object or a string containing michael@0: * \n separated stack lines (as in Error().stack). michael@0: */ michael@0: function do_throw(error, stack) { michael@0: let filename = ""; michael@0: // If we didn't get passed a stack, maybe the error has one michael@0: // otherwise get it from our call context michael@0: stack = stack || error.stack || Components.stack.caller; michael@0: michael@0: if (stack instanceof Components.interfaces.nsIStackFrame) michael@0: filename = stack.filename; michael@0: else if (error.fileName) michael@0: filename = error.fileName; michael@0: michael@0: _log_message_with_stack("test_unexpected_fail", michael@0: error, stack, filename); michael@0: michael@0: _passed = false; michael@0: _do_quit(); michael@0: throw Components.results.NS_ERROR_ABORT; michael@0: } michael@0: michael@0: function _format_stack(stack) { michael@0: if (stack instanceof Components.interfaces.nsIStackFrame) { michael@0: let stack_msg = ""; michael@0: let frame = stack; michael@0: while (frame != null) { michael@0: stack_msg += frame + "\n"; michael@0: frame = frame.caller; michael@0: } michael@0: return stack_msg; michael@0: } michael@0: return "" + stack; michael@0: } michael@0: michael@0: function do_throw_todo(text, stack) { michael@0: if (!stack) michael@0: stack = Components.stack.caller; michael@0: michael@0: _passed = false; michael@0: _log_message_with_stack("test_unexpected_pass", michael@0: text, stack, stack.filename); michael@0: _do_quit(); michael@0: throw Components.results.NS_ERROR_ABORT; michael@0: } michael@0: michael@0: // Make a nice display string from an object that behaves michael@0: // like Error michael@0: function _exception_message(ex) { michael@0: let message = ""; michael@0: if (ex.name) { michael@0: message = ex.name + ": "; michael@0: } michael@0: if (ex.message) { michael@0: message += ex.message; michael@0: } michael@0: if (ex.fileName) { michael@0: message += (" at " + ex.fileName); michael@0: if (ex.lineNumber) { michael@0: message += (":" + ex.lineNumber); michael@0: } michael@0: } michael@0: if (message !== "") { michael@0: return message; michael@0: } michael@0: // Force ex to be stringified michael@0: return "" + ex; michael@0: } michael@0: michael@0: function _log_message_with_stack(action, ex, stack, filename, text) { michael@0: if (stack) { michael@0: _log(action, michael@0: {diagnostic: (text ? text : "") + michael@0: _exception_message(ex) + michael@0: " - See following stack:", michael@0: source_file: filename, michael@0: stack: _format_stack(stack)}); michael@0: } else { michael@0: _log(action, michael@0: {diagnostic: (text ? text : "") + michael@0: _exception_message(ex), michael@0: source_file: filename}); michael@0: } michael@0: } michael@0: michael@0: function do_report_unexpected_exception(ex, text) { michael@0: var caller_stack = Components.stack.caller; michael@0: text = text ? text + " - " : ""; michael@0: michael@0: _passed = false; michael@0: _log_message_with_stack("test_unexpected_fail", ex, ex.stack, michael@0: caller_stack.filename, text + "Unexpected exception "); michael@0: _do_quit(); michael@0: throw Components.results.NS_ERROR_ABORT; michael@0: } michael@0: michael@0: function do_note_exception(ex, text) { michael@0: var caller_stack = Components.stack.caller; michael@0: text = text ? text + " - " : ""; michael@0: michael@0: _log_message_with_stack("test_info", ex, ex.stack, michael@0: caller_stack.filename, text + "Swallowed exception "); michael@0: } michael@0: michael@0: function _do_check_neq(left, right, stack, todo) { michael@0: if (!stack) michael@0: stack = Components.stack.caller; michael@0: michael@0: var text = _wrap_with_quotes_if_necessary(left) + " != " + michael@0: _wrap_with_quotes_if_necessary(right); michael@0: do_report_result(left != right, text, stack, todo); michael@0: } michael@0: michael@0: function do_check_neq(left, right, stack) { michael@0: if (!stack) michael@0: stack = Components.stack.caller; michael@0: michael@0: _do_check_neq(left, right, stack, false); michael@0: } michael@0: michael@0: function todo_check_neq(left, right, stack) { michael@0: if (!stack) michael@0: stack = Components.stack.caller; michael@0: michael@0: _do_check_neq(left, right, stack, true); michael@0: } michael@0: michael@0: function do_report_result(passed, text, stack, todo) { michael@0: if (passed) { michael@0: if (todo) { michael@0: do_throw_todo(text, stack); michael@0: } else { michael@0: ++_passedChecks; michael@0: _log("test_pass", michael@0: {source_file: stack.filename, michael@0: test_name: stack.name, michael@0: line_number: stack.lineNumber, michael@0: diagnostic: "[" + stack.name + " : " + stack.lineNumber + "] " + michael@0: text + "\n"}); michael@0: } michael@0: } else { michael@0: if (todo) { michael@0: ++_todoChecks; michael@0: _log("test_known_fail", michael@0: {source_file: stack.filename, michael@0: test_name: stack.name, michael@0: line_number: stack.lineNumber, michael@0: diagnostic: "[" + stack.name + " : " + stack.lineNumber + "] " + michael@0: text + "\n"}); michael@0: } else { michael@0: do_throw(text, stack); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function _do_check_eq(left, right, stack, todo) { michael@0: if (!stack) michael@0: stack = Components.stack.caller; michael@0: michael@0: var text = _wrap_with_quotes_if_necessary(left) + " == " + michael@0: _wrap_with_quotes_if_necessary(right); michael@0: do_report_result(left == right, text, stack, todo); michael@0: } michael@0: michael@0: function do_check_eq(left, right, stack) { michael@0: if (!stack) michael@0: stack = Components.stack.caller; michael@0: michael@0: _do_check_eq(left, right, stack, false); michael@0: } michael@0: michael@0: function todo_check_eq(left, right, stack) { michael@0: if (!stack) michael@0: stack = Components.stack.caller; michael@0: michael@0: _do_check_eq(left, right, stack, true); michael@0: } michael@0: michael@0: function do_check_true(condition, stack) { michael@0: if (!stack) michael@0: stack = Components.stack.caller; michael@0: michael@0: do_check_eq(condition, true, stack); michael@0: } michael@0: michael@0: function todo_check_true(condition, stack) { michael@0: if (!stack) michael@0: stack = Components.stack.caller; michael@0: michael@0: todo_check_eq(condition, true, stack); michael@0: } michael@0: michael@0: function do_check_false(condition, stack) { michael@0: if (!stack) michael@0: stack = Components.stack.caller; michael@0: michael@0: do_check_eq(condition, false, stack); michael@0: } michael@0: michael@0: function todo_check_false(condition, stack) { michael@0: if (!stack) michael@0: stack = Components.stack.caller; michael@0: michael@0: todo_check_eq(condition, false, stack); michael@0: } michael@0: michael@0: function do_check_null(condition, stack=Components.stack.caller) { michael@0: do_check_eq(condition, null, stack); michael@0: } michael@0: michael@0: function todo_check_null(condition, stack=Components.stack.caller) { michael@0: todo_check_eq(condition, null, stack); michael@0: } michael@0: michael@0: /** michael@0: * Check that |value| matches |pattern|. michael@0: * michael@0: * A |value| matches a pattern |pattern| if any one of the following is true: michael@0: * michael@0: * - |value| and |pattern| are both objects; |pattern|'s enumerable michael@0: * properties' values are valid patterns; and for each enumerable michael@0: * property |p| of |pattern|, plus 'length' if present at all, |value| michael@0: * has a property |p| whose value matches |pattern.p|. Note that if |j| michael@0: * has other properties not present in |p|, |j| may still match |p|. michael@0: * michael@0: * - |value| and |pattern| are equal string, numeric, or boolean literals michael@0: * michael@0: * - |pattern| is |undefined| (this is a wildcard pattern) michael@0: * michael@0: * - typeof |pattern| == "function", and |pattern(value)| is true. michael@0: * michael@0: * For example: michael@0: * michael@0: * do_check_matches({x:1}, {x:1}) // pass michael@0: * do_check_matches({x:1}, {}) // fail: all pattern props required michael@0: * do_check_matches({x:1}, {x:2}) // fail: values must match michael@0: * do_check_matches({x:1}, {x:1, y:2}) // pass: extra props tolerated michael@0: * michael@0: * // Property order is irrelevant. michael@0: * do_check_matches({x:"foo", y:"bar"}, {y:"bar", x:"foo"}) // pass michael@0: * michael@0: * do_check_matches({x:undefined}, {x:1}) // pass: 'undefined' is wildcard michael@0: * do_check_matches({x:undefined}, {x:2}) michael@0: * do_check_matches({x:undefined}, {y:2}) // fail: 'x' must still be there michael@0: * michael@0: * // Patterns nest. michael@0: * do_check_matches({a:1, b:{c:2,d:undefined}}, {a:1, b:{c:2,d:3}}) michael@0: * michael@0: * // 'length' property counts, even if non-enumerable. michael@0: * do_check_matches([3,4,5], [3,4,5]) // pass michael@0: * do_check_matches([3,4,5], [3,5,5]) // fail; value doesn't match michael@0: * do_check_matches([3,4,5], [3,4,5,6]) // fail; length doesn't match michael@0: * michael@0: * // functions in patterns get applied. michael@0: * do_check_matches({foo:function (v) v.length == 2}, {foo:"hi"}) // pass michael@0: * do_check_matches({foo:function (v) v.length == 2}, {bar:"hi"}) // fail michael@0: * do_check_matches({foo:function (v) v.length == 2}, {foo:"hello"}) // fail michael@0: * michael@0: * // We don't check constructors, prototypes, or classes. However, if michael@0: * // pattern has a 'length' property, we require values to match that as michael@0: * // well, even if 'length' is non-enumerable in the pattern. So arrays michael@0: * // are useful as patterns. michael@0: * do_check_matches({0:0, 1:1, length:2}, [0,1]) // pass michael@0: * do_check_matches({0:1}, [1,2]) // pass michael@0: * do_check_matches([0], {0:0, length:1}) // pass michael@0: * michael@0: * Notes: michael@0: * michael@0: * The 'length' hack gives us reasonably intuitive handling of arrays. michael@0: * michael@0: * This is not a tight pattern-matcher; it's only good for checking data michael@0: * from well-behaved sources. For example: michael@0: * - By default, we don't mind values having extra properties. michael@0: * - We don't check for proxies or getters. michael@0: * - We don't check the prototype chain. michael@0: * However, if you know the values are, say, JSON, which is pretty michael@0: * well-behaved, and if you want to tolerate additional properties michael@0: * appearing on the JSON for backward-compatibility, then do_check_matches michael@0: * is ideal. If you do want to be more careful, you can use function michael@0: * patterns to implement more stringent checks. michael@0: */ michael@0: function do_check_matches(pattern, value, stack=Components.stack.caller, todo=false) { michael@0: var matcher = pattern_matcher(pattern); michael@0: var text = "VALUE: " + uneval(value) + "\nPATTERN: " + uneval(pattern) + "\n"; michael@0: var diagnosis = [] michael@0: if (matcher(value, diagnosis)) { michael@0: do_report_result(true, "value matches pattern:\n" + text, stack, todo); michael@0: } else { michael@0: text = ("value doesn't match pattern:\n" + michael@0: text + michael@0: "DIAGNOSIS: " + michael@0: format_pattern_match_failure(diagnosis[0]) + "\n"); michael@0: do_report_result(false, text, stack, todo); michael@0: } michael@0: } michael@0: michael@0: function todo_check_matches(pattern, value, stack=Components.stack.caller) { michael@0: do_check_matches(pattern, value, stack, true); michael@0: } michael@0: michael@0: // Return a pattern-matching function of one argument, |value|, that michael@0: // returns true if |value| matches |pattern|. michael@0: // michael@0: // If the pattern doesn't match, and the pattern-matching function was michael@0: // passed its optional |diagnosis| argument, the pattern-matching function michael@0: // sets |diagnosis|'s '0' property to a JSON-ish description of the portion michael@0: // of the pattern that didn't match, which can be formatted legibly by michael@0: // format_pattern_match_failure. michael@0: function pattern_matcher(pattern) { michael@0: function explain(diagnosis, reason) { michael@0: if (diagnosis) { michael@0: diagnosis[0] = reason; michael@0: } michael@0: return false; michael@0: } michael@0: if (typeof pattern == "function") { michael@0: return pattern; michael@0: } else if (typeof pattern == "object" && pattern) { michael@0: var matchers = [[p, pattern_matcher(pattern[p])] for (p in pattern)]; michael@0: // Kludge: include 'length', if not enumerable. (If it is enumerable, michael@0: // we picked it up in the array comprehension, above. michael@0: ld = Object.getOwnPropertyDescriptor(pattern, 'length'); michael@0: if (ld && !ld.enumerable) { michael@0: matchers.push(['length', pattern_matcher(pattern.length)]) michael@0: } michael@0: return function (value, diagnosis) { michael@0: if (!(value && typeof value == "object")) { michael@0: return explain(diagnosis, "value not object"); michael@0: } michael@0: for (let [p, m] of matchers) { michael@0: var element_diagnosis = []; michael@0: if (!(p in value && m(value[p], element_diagnosis))) { michael@0: return explain(diagnosis, { property:p, michael@0: diagnosis:element_diagnosis[0] }); michael@0: } michael@0: } michael@0: return true; michael@0: }; michael@0: } else if (pattern === undefined) { michael@0: return function(value) { return true; }; michael@0: } else { michael@0: return function (value, diagnosis) { michael@0: if (value !== pattern) { michael@0: return explain(diagnosis, "pattern " + uneval(pattern) + " not === to value " + uneval(value)); michael@0: } michael@0: return true; michael@0: }; michael@0: } michael@0: } michael@0: michael@0: // Format an explanation for a pattern match failure, as stored in the michael@0: // second argument to a matching function. michael@0: function format_pattern_match_failure(diagnosis, indent="") { michael@0: var a; michael@0: if (!diagnosis) { michael@0: a = "Matcher did not explain reason for mismatch."; michael@0: } else if (typeof diagnosis == "string") { michael@0: a = diagnosis; michael@0: } else if (diagnosis.property) { michael@0: a = "Property " + uneval(diagnosis.property) + " of object didn't match:\n"; michael@0: a += format_pattern_match_failure(diagnosis.diagnosis, indent + " "); michael@0: } michael@0: return indent + a; michael@0: } michael@0: michael@0: // Check that |func| throws an nsIException that has michael@0: // |Components.results[resultName]| as the value of its 'result' property. michael@0: function do_check_throws_nsIException(func, resultName, michael@0: stack=Components.stack.caller, todo=false) michael@0: { michael@0: let expected = Components.results[resultName]; michael@0: if (typeof expected !== 'number') { michael@0: do_throw("do_check_throws_nsIException requires a Components.results" + michael@0: " property name, not " + uneval(resultName), stack); michael@0: } michael@0: michael@0: let msg = ("do_check_throws_nsIException: func should throw" + michael@0: " an nsIException whose 'result' is Components.results." + michael@0: resultName); michael@0: michael@0: try { michael@0: func(); michael@0: } catch (ex) { michael@0: if (!(ex instanceof Components.interfaces.nsIException) || michael@0: ex.result !== expected) { michael@0: do_report_result(false, msg + ", threw " + legible_exception(ex) + michael@0: " instead", stack, todo); michael@0: } michael@0: michael@0: do_report_result(true, msg, stack, todo); michael@0: return; michael@0: } michael@0: michael@0: // Call this here, not in the 'try' clause, so do_report_result's own michael@0: // throw doesn't get caught by our 'catch' clause. michael@0: do_report_result(false, msg + ", but returned normally", stack, todo); michael@0: } michael@0: michael@0: // Produce a human-readable form of |exception|. This looks up michael@0: // Components.results values, tries toString methods, and so on. michael@0: function legible_exception(exception) michael@0: { michael@0: switch (typeof exception) { michael@0: case 'object': michael@0: if (exception instanceof Components.interfaces.nsIException) { michael@0: return "nsIException instance: " + uneval(exception.toString()); michael@0: } michael@0: return exception.toString(); michael@0: michael@0: case 'number': michael@0: for (let name in Components.results) { michael@0: if (exception === Components.results[name]) { michael@0: return "Components.results." + name; michael@0: } michael@0: } michael@0: michael@0: // Fall through. michael@0: default: michael@0: return uneval(exception); michael@0: } michael@0: } michael@0: michael@0: function do_check_instanceof(value, constructor, michael@0: stack=Components.stack.caller, todo=false) { michael@0: do_report_result(value instanceof constructor, michael@0: "value should be an instance of " + constructor.name, michael@0: stack, todo); michael@0: } michael@0: michael@0: function todo_check_instanceof(value, constructor, michael@0: stack=Components.stack.caller) { michael@0: do_check_instanceof(value, constructor, stack, true); michael@0: } michael@0: michael@0: function do_test_pending(aName) { michael@0: ++_tests_pending; michael@0: michael@0: _log("test_pending", michael@0: {_message: "TEST-INFO | (xpcshell/head.js) | test" + michael@0: (aName ? " " + aName : "") + michael@0: " pending (" + _tests_pending + ")\n"}); michael@0: } michael@0: michael@0: function do_test_finished(aName) { michael@0: _log("test_finish", michael@0: {_message: "TEST-INFO | (xpcshell/head.js) | test" + michael@0: (aName ? " " + aName : "") + michael@0: " finished (" + _tests_pending + ")\n"}); michael@0: if (--_tests_pending == 0) michael@0: _do_quit(); michael@0: } michael@0: michael@0: function do_get_file(path, allowNonexistent) { michael@0: try { michael@0: let lf = Components.classes["@mozilla.org/file/directory_service;1"] michael@0: .getService(Components.interfaces.nsIProperties) michael@0: .get("CurWorkD", Components.interfaces.nsILocalFile); michael@0: michael@0: let bits = path.split("/"); michael@0: for (let i = 0; i < bits.length; i++) { michael@0: if (bits[i]) { michael@0: if (bits[i] == "..") michael@0: lf = lf.parent; michael@0: else michael@0: lf.append(bits[i]); michael@0: } michael@0: } michael@0: michael@0: if (!allowNonexistent && !lf.exists()) { michael@0: // Not using do_throw(): caller will continue. michael@0: _passed = false; michael@0: var stack = Components.stack.caller; michael@0: _log("test_unexpected_fail", michael@0: {source_file: stack.filename, michael@0: test_name: stack.name, michael@0: line_number: stack.lineNumber, michael@0: diagnostic: "[" + stack.name + " : " + stack.lineNumber + "] " + michael@0: lf.path + " does not exist\n"}); michael@0: } michael@0: michael@0: return lf; michael@0: } michael@0: catch (ex) { michael@0: do_throw(ex.toString(), Components.stack.caller); michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: // do_get_cwd() isn't exactly self-explanatory, so provide a helper michael@0: function do_get_cwd() { michael@0: return do_get_file(""); michael@0: } michael@0: michael@0: function do_load_manifest(path) { michael@0: var lf = do_get_file(path); michael@0: const nsIComponentRegistrar = Components.interfaces.nsIComponentRegistrar; michael@0: do_check_true(Components.manager instanceof nsIComponentRegistrar); michael@0: // Previous do_check_true() is not a test check. michael@0: ++_falsePassedChecks; michael@0: Components.manager.autoRegister(lf); michael@0: } michael@0: michael@0: /** michael@0: * Parse a DOM document. michael@0: * michael@0: * @param aPath File path to the document. michael@0: * @param aType Content type to use in DOMParser. michael@0: * michael@0: * @return nsIDOMDocument from the file. michael@0: */ michael@0: function do_parse_document(aPath, aType) { michael@0: switch (aType) { michael@0: case "application/xhtml+xml": michael@0: case "application/xml": michael@0: case "text/xml": michael@0: break; michael@0: michael@0: default: michael@0: do_throw("type: expected application/xhtml+xml, application/xml or text/xml," + michael@0: " got '" + aType + "'", michael@0: Components.stack.caller); michael@0: } michael@0: michael@0: var lf = do_get_file(aPath); michael@0: const C_i = Components.interfaces; michael@0: const parserClass = "@mozilla.org/xmlextras/domparser;1"; michael@0: const streamClass = "@mozilla.org/network/file-input-stream;1"; michael@0: var stream = Components.classes[streamClass] michael@0: .createInstance(C_i.nsIFileInputStream); michael@0: stream.init(lf, -1, -1, C_i.nsIFileInputStream.CLOSE_ON_EOF); michael@0: var parser = Components.classes[parserClass] michael@0: .createInstance(C_i.nsIDOMParser); michael@0: var doc = parser.parseFromStream(stream, null, lf.fileSize, aType); michael@0: parser = null; michael@0: stream = null; michael@0: lf = null; michael@0: return doc; michael@0: } michael@0: michael@0: /** michael@0: * Registers a function that will run when the test harness is done running all michael@0: * tests. michael@0: * michael@0: * @param aFunction michael@0: * The function to be called when the test harness has finished running. michael@0: */ michael@0: function do_register_cleanup(aFunction) michael@0: { michael@0: _cleanupFunctions.push(aFunction); michael@0: } michael@0: michael@0: /** michael@0: * Returns the directory for a temp dir, which is created by the michael@0: * test harness. Every test gets its own temp dir. michael@0: * michael@0: * @return nsILocalFile of the temporary directory michael@0: */ michael@0: function do_get_tempdir() { michael@0: let env = Components.classes["@mozilla.org/process/environment;1"] michael@0: .getService(Components.interfaces.nsIEnvironment); michael@0: // the python harness sets this in the environment for us michael@0: let path = env.get("XPCSHELL_TEST_TEMP_DIR"); michael@0: let file = Components.classes["@mozilla.org/file/local;1"] michael@0: .createInstance(Components.interfaces.nsILocalFile); michael@0: file.initWithPath(path); michael@0: return file; michael@0: } michael@0: michael@0: /** michael@0: * Returns the directory for crashreporter minidumps. michael@0: * michael@0: * @return nsILocalFile of the minidump directory michael@0: */ michael@0: function do_get_minidumpdir() { michael@0: let env = Components.classes["@mozilla.org/process/environment;1"] michael@0: .getService(Components.interfaces.nsIEnvironment); michael@0: // the python harness may set this in the environment for us michael@0: let path = env.get("XPCSHELL_MINIDUMP_DIR"); michael@0: if (path) { michael@0: let file = Components.classes["@mozilla.org/file/local;1"] michael@0: .createInstance(Components.interfaces.nsILocalFile); michael@0: file.initWithPath(path); michael@0: return file; michael@0: } else { michael@0: return do_get_tempdir(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Registers a directory with the profile service, michael@0: * and return the directory as an nsILocalFile. michael@0: * michael@0: * @return nsILocalFile of the profile directory. michael@0: */ michael@0: function do_get_profile() { michael@0: if (!runningInParent) { michael@0: _log("test_info", michael@0: {_message: "TEST-INFO | (xpcshell/head.js) | Ignoring profile creation from child process.\n"}); michael@0: michael@0: return null; michael@0: } michael@0: michael@0: if (!_profileInitialized) { michael@0: // Since we have a profile, we will notify profile shutdown topics at michael@0: // the end of the current test, to ensure correct cleanup on shutdown. michael@0: do_register_cleanup(function() { michael@0: let obsSvc = Components.classes["@mozilla.org/observer-service;1"]. michael@0: getService(Components.interfaces.nsIObserverService); michael@0: obsSvc.notifyObservers(null, "profile-change-net-teardown", null); michael@0: obsSvc.notifyObservers(null, "profile-change-teardown", null); michael@0: obsSvc.notifyObservers(null, "profile-before-change", null); michael@0: }); michael@0: } michael@0: michael@0: let env = Components.classes["@mozilla.org/process/environment;1"] michael@0: .getService(Components.interfaces.nsIEnvironment); michael@0: // the python harness sets this in the environment for us michael@0: let profd = env.get("XPCSHELL_TEST_PROFILE_DIR"); michael@0: let file = Components.classes["@mozilla.org/file/local;1"] michael@0: .createInstance(Components.interfaces.nsILocalFile); michael@0: file.initWithPath(profd); michael@0: michael@0: let dirSvc = Components.classes["@mozilla.org/file/directory_service;1"] michael@0: .getService(Components.interfaces.nsIProperties); michael@0: let provider = { michael@0: getFile: function(prop, persistent) { michael@0: persistent.value = true; michael@0: if (prop == "ProfD" || prop == "ProfLD" || prop == "ProfDS" || michael@0: prop == "ProfLDS" || prop == "TmpD") { michael@0: return file.clone(); michael@0: } michael@0: throw Components.results.NS_ERROR_FAILURE; michael@0: }, michael@0: QueryInterface: function(iid) { michael@0: if (iid.equals(Components.interfaces.nsIDirectoryServiceProvider) || michael@0: iid.equals(Components.interfaces.nsISupports)) { michael@0: return this; michael@0: } michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: }; michael@0: dirSvc.QueryInterface(Components.interfaces.nsIDirectoryService) michael@0: .registerProvider(provider); michael@0: michael@0: let obsSvc = Components.classes["@mozilla.org/observer-service;1"]. michael@0: getService(Components.interfaces.nsIObserverService); michael@0: michael@0: // We need to update the crash events directory when the profile changes. michael@0: if (runningInParent && michael@0: "@mozilla.org/toolkit/crash-reporter;1" in Components.classes) { michael@0: let crashReporter = michael@0: Components.classes["@mozilla.org/toolkit/crash-reporter;1"] michael@0: .getService(Components.interfaces.nsICrashReporter); michael@0: crashReporter.UpdateCrashEventsDir(); michael@0: } michael@0: michael@0: if (!_profileInitialized) { michael@0: obsSvc.notifyObservers(null, "profile-do-change", "xpcshell-do-get-profile"); michael@0: _profileInitialized = true; michael@0: } michael@0: michael@0: // The methods of 'provider' will retain this scope so null out everything michael@0: // to avoid spurious leak reports. michael@0: env = null; michael@0: profd = null; michael@0: dirSvc = null; michael@0: provider = null; michael@0: obsSvc = null; michael@0: return file.clone(); michael@0: } michael@0: michael@0: /** michael@0: * This function loads head.js (this file) in the child process, so that all michael@0: * functions defined in this file (do_throw, etc) are available to subsequent michael@0: * sendCommand calls. It also sets various constants used by these functions. michael@0: * michael@0: * (Note that you may use sendCommand without calling this function first; you michael@0: * simply won't have any of the functions in this file available.) michael@0: */ michael@0: function do_load_child_test_harness() michael@0: { michael@0: // Make sure this isn't called from child process michael@0: if (!runningInParent) { michael@0: do_throw("run_test_in_child cannot be called from child!"); michael@0: } michael@0: michael@0: // Allow to be called multiple times, but only run once michael@0: if (typeof do_load_child_test_harness.alreadyRun != "undefined") michael@0: return; michael@0: do_load_child_test_harness.alreadyRun = 1; michael@0: michael@0: _XPCSHELL_PROCESS = "parent"; michael@0: michael@0: let command = michael@0: "const _HEAD_JS_PATH=" + uneval(_HEAD_JS_PATH) + "; " michael@0: + "const _HTTPD_JS_PATH=" + uneval(_HTTPD_JS_PATH) + "; " michael@0: + "const _HEAD_FILES=" + uneval(_HEAD_FILES) + "; " michael@0: + "const _TAIL_FILES=" + uneval(_TAIL_FILES) + "; " michael@0: + "const _XPCSHELL_PROCESS='child';"; michael@0: michael@0: if (this._TESTING_MODULES_DIR) { michael@0: command += " const _TESTING_MODULES_DIR=" + uneval(_TESTING_MODULES_DIR) + ";"; michael@0: } michael@0: michael@0: command += " load(_HEAD_JS_PATH);"; michael@0: sendCommand(command); michael@0: } michael@0: michael@0: /** michael@0: * Runs an entire xpcshell unit test in a child process (rather than in chrome, michael@0: * which is the default). michael@0: * michael@0: * This function returns immediately, before the test has completed. michael@0: * michael@0: * @param testFile michael@0: * The name of the script to run. Path format same as load(). michael@0: * @param optionalCallback. michael@0: * Optional function to be called (in parent) when test on child is michael@0: * complete. If provided, the function must call do_test_finished(); michael@0: */ michael@0: function run_test_in_child(testFile, optionalCallback) michael@0: { michael@0: var callback = (typeof optionalCallback == 'undefined') ? michael@0: do_test_finished : optionalCallback; michael@0: michael@0: do_load_child_test_harness(); michael@0: michael@0: var testPath = do_get_file(testFile).path.replace(/\\/g, "/"); michael@0: do_test_pending("run in child"); michael@0: sendCommand("_log('child_test_start', {_message: 'CHILD-TEST-STARTED'}); " michael@0: + "const _TEST_FILE=['" + testPath + "']; _execute_test(); " michael@0: + "_log('child_test_end', {_message: 'CHILD-TEST-COMPLETED'});", michael@0: callback); michael@0: } michael@0: michael@0: /** michael@0: * Execute a given function as soon as a particular cross-process message is received. michael@0: * Must be paired with do_send_remote_message or equivalent ProcessMessageManager calls. michael@0: */ michael@0: function do_await_remote_message(name, callback) michael@0: { michael@0: var listener = { michael@0: receiveMessage: function(message) { michael@0: if (message.name == name) { michael@0: mm.removeMessageListener(name, listener); michael@0: callback(); michael@0: do_test_finished(); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: var mm; michael@0: if (runningInParent) { michael@0: mm = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIMessageBroadcaster); michael@0: } else { michael@0: mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender); michael@0: } michael@0: do_test_pending(); michael@0: mm.addMessageListener(name, listener); michael@0: } michael@0: michael@0: /** michael@0: * Asynchronously send a message to all remote processes. Pairs with do_await_remote_message michael@0: * or equivalent ProcessMessageManager listeners. michael@0: */ michael@0: function do_send_remote_message(name) { michael@0: var mm; michael@0: var sender; michael@0: if (runningInParent) { michael@0: mm = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIMessageBroadcaster); michael@0: sender = 'broadcastAsyncMessage'; michael@0: } else { michael@0: mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender); michael@0: sender = 'sendAsyncMessage'; michael@0: } michael@0: mm[sender](name); michael@0: } michael@0: michael@0: /** michael@0: * Add a test function to the list of tests that are to be run asynchronously. michael@0: * michael@0: * Each test function must call run_next_test() when it's done. Test files michael@0: * should call run_next_test() in their run_test function to execute all michael@0: * async tests. michael@0: * michael@0: * @return the test function that was passed in. michael@0: */ michael@0: let _gTests = []; michael@0: function add_test(func) { michael@0: _gTests.push([false, func]); michael@0: return func; michael@0: } michael@0: michael@0: // We lazy import Task.jsm so we don't incur a run-time penalty for all tests. michael@0: let _Task; michael@0: michael@0: /** michael@0: * Add a test function which is a Task function. michael@0: * michael@0: * Task functions are functions fed into Task.jsm's Task.spawn(). They are michael@0: * generators that emit promises. michael@0: * michael@0: * If an exception is thrown, a do_check_* comparison fails, or if a rejected michael@0: * promise is yielded, the test function aborts immediately and the test is michael@0: * reported as a failure. michael@0: * michael@0: * Unlike add_test(), there is no need to call run_next_test(). The next test michael@0: * will run automatically as soon the task function is exhausted. To trigger michael@0: * premature (but successful) termination of the function, simply return or michael@0: * throw a Task.Result instance. michael@0: * michael@0: * Example usage: michael@0: * michael@0: * add_task(function test() { michael@0: * let result = yield Promise.resolve(true); michael@0: * michael@0: * do_check_true(result); michael@0: * michael@0: * let secondary = yield someFunctionThatReturnsAPromise(result); michael@0: * do_check_eq(secondary, "expected value"); michael@0: * }); michael@0: * michael@0: * add_task(function test_early_return() { michael@0: * let result = yield somethingThatReturnsAPromise(); michael@0: * michael@0: * if (!result) { michael@0: * // Test is ended immediately, with success. michael@0: * return; michael@0: * } michael@0: * michael@0: * do_check_eq(result, "foo"); michael@0: * }); michael@0: */ michael@0: function add_task(func) { michael@0: if (!_Task) { michael@0: let ns = {}; michael@0: _Task = Components.utils.import("resource://gre/modules/Task.jsm", ns).Task; michael@0: } michael@0: michael@0: _gTests.push([true, func]); michael@0: } michael@0: michael@0: /** michael@0: * Runs the next test function from the list of async tests. michael@0: */ michael@0: let _gRunningTest = null; michael@0: let _gTestIndex = 0; // The index of the currently running test. michael@0: let _gTaskRunning = false; michael@0: function run_next_test() michael@0: { michael@0: if (_gTaskRunning) { michael@0: throw new Error("run_next_test() called from an add_task() test function. " + michael@0: "run_next_test() should not be called from inside add_task() " + michael@0: "under any circumstances!"); michael@0: } michael@0: michael@0: function _run_next_test() michael@0: { michael@0: if (_gTestIndex < _gTests.length) { michael@0: // Flush uncaught errors as early and often as possible. michael@0: _Promise.Debugging.flushUncaughtErrors(); michael@0: let _isTask; michael@0: [_isTask, _gRunningTest] = _gTests[_gTestIndex++]; michael@0: print("TEST-INFO | " + _TEST_FILE + " | Starting " + _gRunningTest.name); michael@0: do_test_pending(_gRunningTest.name); michael@0: michael@0: if (_isTask) { michael@0: _gTaskRunning = true; michael@0: _Task.spawn(_gRunningTest).then( michael@0: () => { _gTaskRunning = false; run_next_test(); }, michael@0: (ex) => { _gTaskRunning = false; do_report_unexpected_exception(ex); } michael@0: ); michael@0: } else { michael@0: // Exceptions do not kill asynchronous tests, so they'll time out. michael@0: try { michael@0: _gRunningTest(); michael@0: } catch (e) { michael@0: do_throw(e); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: // For sane stacks during failures, we execute this code soon, but not now. michael@0: // We do this now, before we call do_test_finished(), to ensure the pending michael@0: // counter (_tests_pending) never reaches 0 while we still have tests to run michael@0: // (do_execute_soon bumps that counter). michael@0: do_execute_soon(_run_next_test, "run_next_test " + _gTestIndex); michael@0: michael@0: if (_gRunningTest !== null) { michael@0: // Close the previous test do_test_pending call. michael@0: do_test_finished(_gRunningTest.name); michael@0: } michael@0: } michael@0: michael@0: try { michael@0: if (runningInParent) { michael@0: // Always use network provider for geolocation tests michael@0: // so we bypass the OSX dialog raised by the corelocation provider michael@0: let prefs = Components.classes["@mozilla.org/preferences-service;1"] michael@0: .getService(Components.interfaces.nsIPrefBranch); michael@0: michael@0: prefs.setBoolPref("geo.provider.testing", true); michael@0: } michael@0: } catch (e) { }