diff options
Diffstat (limited to '')
-rw-r--r-- | testing/mochitest/tests/SimpleTest/TestRunner.js | 1168 |
1 files changed, 1168 insertions, 0 deletions
diff --git a/testing/mochitest/tests/SimpleTest/TestRunner.js b/testing/mochitest/tests/SimpleTest/TestRunner.js new file mode 100644 index 0000000000..5f305a176b --- /dev/null +++ b/testing/mochitest/tests/SimpleTest/TestRunner.js @@ -0,0 +1,1168 @@ +/* -*- js-indent-level: 4; indent-tabs-mode: nil -*- */ +/* + * e10s event dispatcher from content->chrome + * + * type = eventName (QuitApplication) + * data = json object {"filename":filename} <- for LoggerInit + */ + +// This file expects the following files to be loaded. +/* import-globals-from LogController.js */ +/* import-globals-from MemoryStats.js */ +/* import-globals-from MozillaLogger.js */ + +/* eslint-disable no-unsanitized/property */ + +"use strict"; + +const { StructuredLogger, StructuredFormatter } = + SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/StructuredLog.sys.mjs" + ); + +function getElement(id) { + return typeof id == "string" ? document.getElementById(id) : id; +} + +this.$ = this.getElement; + +function contentDispatchEvent(type, data, sync) { + if (typeof data == "undefined") { + data = {}; + } + + var event = new CustomEvent("contentEvent", { + bubbles: true, + detail: { + sync, + type, + data: JSON.stringify(data), + }, + }); + document.dispatchEvent(event); +} + +function contentAsyncEvent(type, data) { + contentDispatchEvent(type, data, 0); +} + +/* Helper Function */ +function extend(obj, /* optional */ skip) { + // Extend an array with an array-like object starting + // from the skip index + if (!skip) { + skip = 0; + } + if (obj) { + var l = obj.length; + var ret = []; + for (var i = skip; i < l; i++) { + ret.push(obj[i]); + } + } + return ret; +} + +function flattenArguments(lst /* ...*/) { + var res = []; + var args = extend(arguments); + while (args.length) { + var o = args.shift(); + if (o && typeof o == "object" && typeof o.length == "number") { + for (var i = o.length - 1; i >= 0; i--) { + args.unshift(o[i]); + } + } else { + res.push(o); + } + } + return res; +} + +function testInXOriginFrame() { + // Check if the test running in an iframe is a cross origin test. + try { + $("testframe").contentWindow.origin; + return false; + } catch (e) { + return true; + } +} + +function testInDifferentProcess() { + // Check if the test running in an iframe that is loaded in a different process. + return SpecialPowers.Cu.isRemoteProxy($("testframe").contentWindow); +} + +/** + * TestRunner: A test runner for SimpleTest + * TODO: + * + * * Avoid moving iframes: That causes reloads on mozilla and opera. + * + * + **/ +var TestRunner = {}; +TestRunner.logEnabled = false; +TestRunner._currentTest = 0; +TestRunner._lastTestFinished = -1; +TestRunner._loopIsRestarting = false; +TestRunner.currentTestURL = ""; +TestRunner.originalTestURL = ""; +TestRunner._urls = []; +TestRunner._lastAssertionCount = 0; +TestRunner._expectedMinAsserts = 0; +TestRunner._expectedMaxAsserts = 0; + +TestRunner.timeout = 300 * 1000; // 5 minutes. +TestRunner.maxTimeouts = 4; // halt testing after too many timeouts +TestRunner.runSlower = false; +TestRunner.dumpOutputDirectory = ""; +TestRunner.dumpAboutMemoryAfterTest = false; +TestRunner.dumpDMDAfterTest = false; +TestRunner.slowestTestTime = 0; +TestRunner.slowestTestURL = ""; +TestRunner.interactiveDebugger = false; +TestRunner.cleanupCrashes = false; +TestRunner.timeoutAsPass = false; +TestRunner.conditionedProfile = false; +TestRunner.comparePrefs = false; + +TestRunner._expectingProcessCrash = false; +TestRunner._structuredFormatter = new StructuredFormatter(); + +/** + * Make sure the tests don't hang indefinitely. + **/ +TestRunner._numTimeouts = 0; +TestRunner._currentTestStartTime = new Date().valueOf(); +TestRunner._timeoutFactor = 1; + +/** + * Used to collect code coverage with the js debugger. + */ +TestRunner.jscovDirPrefix = ""; +var coverageCollector = {}; + +function record(succeeded, expectedFail, msg) { + let successInfo; + let failureInfo; + if (expectedFail) { + successInfo = { + status: "PASS", + expected: "FAIL", + message: "TEST-UNEXPECTED-PASS", + }; + failureInfo = { + status: "FAIL", + expected: "FAIL", + message: "TEST-KNOWN-FAIL", + }; + } else { + successInfo = { + status: "PASS", + expected: "PASS", + message: "TEST-PASS", + }; + failureInfo = { + status: "FAIL", + expected: "PASS", + message: "TEST-UNEXPECTED-FAIL", + }; + } + + let result = succeeded ? successInfo : failureInfo; + + TestRunner.structuredLogger.testStatus( + TestRunner.currentTestURL, + msg, + result.status, + result.expected, + "", + "" + ); +} + +TestRunner._checkForHangs = function () { + function reportError(win, msg) { + if (testInXOriginFrame() || "SimpleTest" in win) { + record(false, TestRunner.timeoutAsPass, msg); + } else if ("W3CTest" in win) { + win.W3CTest.logFailure(msg); + } + } + + async function killTest(win) { + if (testInXOriginFrame()) { + win.postMessage("SimpleTest:timeout", "*"); + } else if ("SimpleTest" in win) { + await win.SimpleTest.timeout(); + win.SimpleTest.finish(); + } else if ("W3CTest" in win) { + await win.W3CTest.timeout(); + } + } + + if (TestRunner._currentTest < TestRunner._urls.length) { + var runtime = new Date().valueOf() - TestRunner._currentTestStartTime; + if ( + !TestRunner.interactiveDebugger && + runtime >= TestRunner.timeout * TestRunner._timeoutFactor + ) { + let testIframe = $("testframe"); + var frameWindow = + (!testInXOriginFrame() && testIframe.contentWindow.wrappedJSObject) || + testIframe.contentWindow; + reportError(frameWindow, "Test timed out."); + TestRunner.updateUI([{ result: false }]); + + // If we have too many timeouts, give up. We don't want to wait hours + // for results if some bug causes lots of tests to time out. + if ( + ++TestRunner._numTimeouts >= TestRunner.maxTimeouts || + TestRunner.runUntilFailure + ) { + TestRunner._haltTests = true; + + TestRunner.currentTestURL = "(SimpleTest/TestRunner.js)"; + reportError( + frameWindow, + TestRunner.maxTimeouts + " test timeouts, giving up." + ); + var skippedTests = TestRunner._urls.length - TestRunner._currentTest; + reportError( + frameWindow, + "Skipping " + skippedTests + " remaining tests." + ); + } + + // Add a little (1 second) delay to ensure automation.py has time to notice + // "Test timed out" log and process it (= take a screenshot). + setTimeout(async function delayedKillTest() { + try { + await killTest(frameWindow); + } catch (e) { + reportError(frameWindow, "Test error: " + e); + TestRunner.updateUI([{ result: false }]); + } + }, 1000); + + if (TestRunner._haltTests) { + return; + } + } + + setTimeout(TestRunner._checkForHangs, 30000); + } +}; + +TestRunner.requestLongerTimeout = function (factor) { + TestRunner._timeoutFactor = factor; +}; + +/** + * This is used to loop tests + **/ +TestRunner.repeat = 0; +TestRunner._currentLoop = 1; + +TestRunner.expectAssertions = function (min, max) { + if (typeof max == "undefined") { + max = min; + } + if ( + typeof min != "number" || + typeof max != "number" || + min < 0 || + max < min + ) { + throw new Error("bad parameter to expectAssertions"); + } + TestRunner._expectedMinAsserts = min; + TestRunner._expectedMaxAsserts = max; +}; + +/** + * This function is called after generating the summary. + **/ +TestRunner.onComplete = null; + +/** + * Adds a failed test case to a list so we can rerun only the failed tests + **/ +TestRunner._failedTests = {}; +TestRunner._failureFile = ""; + +TestRunner.addFailedTest = function (testName) { + if (TestRunner._failedTests[testName] == undefined) { + TestRunner._failedTests[testName] = ""; + } +}; + +TestRunner.setFailureFile = function (fileName) { + TestRunner._failureFile = fileName; +}; + +TestRunner.generateFailureList = function () { + if (TestRunner._failureFile) { + var failures = new MozillaFileLogger(TestRunner._failureFile); + failures.log(JSON.stringify(TestRunner._failedTests)); + failures.close(); + } +}; + +/** + * If logEnabled is true, this is the logger that will be used. + **/ + +// This delimiter is used to avoid interleaving Mochitest/Gecko logs. +var LOG_DELIMITER = "\ue175\uee31\u2c32\uacbf"; + +// A log callback for StructuredLog.sys.mjs +TestRunner._dumpMessage = function (message) { + var str; + + // This is a directive to python to format these messages + // for compatibility with mozharness. This can be removed + // with the MochitestFormatter (see bug 1045525). + message.js_source = "TestRunner.js"; + if ( + TestRunner.interactiveDebugger && + message.action in TestRunner._structuredFormatter + ) { + str = TestRunner._structuredFormatter[message.action](message); + } else { + str = LOG_DELIMITER + JSON.stringify(message) + LOG_DELIMITER; + } + // BUGFIX: browser-chrome tests don't use LogController + if (Object.keys(LogController.listeners).length !== 0) { + LogController.log(str); + } else { + dump("\n" + str + "\n"); + } + // Checking for error messages + if (message.expected || message.level === "ERROR") { + TestRunner.failureHandler(); + } +}; + +// From https://searchfox.org/mozilla-central/source/testing/modules/StructuredLog.sys.mjs +TestRunner.structuredLogger = new StructuredLogger( + "mochitest", + TestRunner._dumpMessage, + [], + TestRunner +); +TestRunner.structuredLogger.deactivateBuffering = function () { + TestRunner.structuredLogger.logData("buffering_off"); +}; +TestRunner.structuredLogger.activateBuffering = function () { + TestRunner.structuredLogger.logData("buffering_on"); +}; + +TestRunner.log = function (msg) { + if (TestRunner.logEnabled) { + TestRunner.structuredLogger.info(msg); + } else { + dump(msg + "\n"); + } +}; + +TestRunner.error = function (msg) { + if (TestRunner.logEnabled) { + TestRunner.structuredLogger.error(msg); + } else { + dump(msg + "\n"); + TestRunner.failureHandler(); + } +}; + +TestRunner.failureHandler = function () { + if (TestRunner.runUntilFailure) { + TestRunner._haltTests = true; + } + + if (TestRunner.debugOnFailure) { + // You've hit this line because you requested to break into the + // debugger upon a testcase failure on your test run. + // eslint-disable-next-line no-debugger + debugger; + } +}; + +/** + * Toggle element visibility + **/ +TestRunner._toggle = function (el) { + if (el.className == "noshow") { + el.className = ""; + el.style.cssText = ""; + } else { + el.className = "noshow"; + el.style.cssText = "width:0px; height:0px; border:0px;"; + } +}; + +/** + * Creates the iframe that contains a test + **/ +TestRunner._makeIframe = function (url, retry) { + var iframe = $("testframe"); + if ( + url != "about:blank" && + (("hasFocus" in document && !document.hasFocus()) || + ("activeElement" in document && document.activeElement != iframe)) + ) { + contentAsyncEvent("Focus"); + window.focus(); + SpecialPowers.focus(); + iframe.focus(); + if (retry < 3) { + window.setTimeout(function () { + TestRunner._makeIframe(url, retry + 1); + }, 1000); + return; + } + + TestRunner.structuredLogger.info( + "Error: Unable to restore focus, expect failures and timeouts." + ); + } + window.scrollTo(0, $("indicator").offsetTop); + try { + let urlObj = new URL(url); + if (TestRunner.xOriginTests) { + // The test will run in a xorigin iframe, so we pass in additional test params in the + // URL since the content process won't be able to access them from the parentRunner + // directly. + let params = TestRunner.getParameterInfo(); + urlObj.searchParams.append( + "currentTestURL", + urlObj.pathname.replace("/tests/", "") + ); + urlObj.searchParams.append("closeWhenDone", params.closeWhenDone); + urlObj.searchParams.append("showTestReport", TestRunner.showTestReport); + urlObj.searchParams.append("expected", TestRunner.expected); + iframe.src = urlObj.href; + } else { + iframe.src = url; + } + } catch { + // If the provided `url` is not a valid URL (i.e. doesn't include a protocol) + // then the new URL() constructor will raise a TypeError. This is expected in the + // usual case (i.e. non-xorigin iFrame tests) so set the URL in the usual way. + iframe.src = url; + } + iframe.name = url; + iframe.width = "500"; +}; + +/** + * Returns the current test URL. + * We use this to tell whether the test has navigated to another test without + * being finished first. + */ +TestRunner.getLoadedTestURL = function () { + if (!testInXOriginFrame()) { + var prefix = ""; + // handle mochitest-chrome URIs + if ($("testframe").contentWindow.location.protocol == "chrome:") { + prefix = "chrome://mochitests"; + } + return prefix + $("testframe").contentWindow.location.pathname; + } + return TestRunner.currentTestURL; +}; + +TestRunner.setParameterInfo = function (params) { + this._params = params; +}; + +TestRunner.getParameterInfo = function () { + return this._params; +}; + +/** + * Print information about which prefs are set. + * This is used to help validate that the tests are actually + * running in the expected context. + */ +TestRunner.dumpPrefContext = function () { + let prefs = ["fission.autostart"]; + + let message = ["Dumping test context:"]; + prefs.forEach(function formatPref(pref) { + let val = SpecialPowers.getBoolPref(pref); + message.push(pref + "=" + val); + }); + TestRunner.structuredLogger.info(message.join("\n ")); +}; + +/** + * TestRunner entry point. + * + * The arguments are the URLs of the test to be ran. + * + **/ +TestRunner.runTests = function (/*url...*/) { + TestRunner.structuredLogger.info("SimpleTest START"); + TestRunner.dumpPrefContext(); + TestRunner.originalTestURL = $("current-test").innerHTML; + + SpecialPowers.registerProcessCrashObservers(); + + // Initialize code coverage + if (TestRunner.jscovDirPrefix != "") { + var { CoverageCollector } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/CoverageUtils.sys.mjs" + ); + coverageCollector = new CoverageCollector(TestRunner.jscovDirPrefix); + } + + SpecialPowers.requestResetCoverageCounters().then(() => { + TestRunner._urls = flattenArguments(arguments); + + var singleTestRun = this._urls.length <= 1 && TestRunner.repeat <= 1; + TestRunner.showTestReport = singleTestRun; + var frame = $("testframe"); + frame.src = ""; + if (singleTestRun) { + // Can't use document.body because this runs in a XUL doc as well... + var body = document.getElementsByTagName("body")[0]; + body.setAttribute("singletest", "true"); + frame.removeAttribute("scrolling"); + } + TestRunner._checkForHangs(); + TestRunner.runNextTest(); + }); +}; + +/** + * Used for running a set of tests in a loop for debugging purposes + * Takes an array of URLs + **/ +TestRunner.resetTests = function (listURLs) { + TestRunner._currentTest = 0; + // Reset our "Current-test" line - functionality depends on it + $("current-test").innerHTML = TestRunner.originalTestURL; + if (TestRunner.logEnabled) { + TestRunner.structuredLogger.info( + "SimpleTest START Loop " + TestRunner._currentLoop + ); + } + + TestRunner._urls = listURLs; + $("testframe").src = ""; + TestRunner._checkForHangs(); + TestRunner.runNextTest(); +}; + +TestRunner.getNextUrl = function () { + var url = ""; + // sometimes we have a subtest/harness which doesn't use a manifest + if ( + TestRunner._urls[TestRunner._currentTest] instanceof Object && + "test" in TestRunner._urls[TestRunner._currentTest] + ) { + url = TestRunner._urls[TestRunner._currentTest].test.url; + TestRunner.expected = + TestRunner._urls[TestRunner._currentTest].test.expected; + } else { + url = TestRunner._urls[TestRunner._currentTest]; + TestRunner.expected = "pass"; + } + return url; +}; + +/** + * Run the next test. If no test remains, calls onComplete(). + **/ +TestRunner._haltTests = false; +async function _runNextTest() { + if ( + TestRunner._currentTest < TestRunner._urls.length && + !TestRunner._haltTests + ) { + var url = TestRunner.getNextUrl(); + TestRunner.currentTestURL = url; + + $("current-test-path").innerHTML = url; + + TestRunner._currentTestStartTimestamp = SpecialPowers.Cu.now(); + TestRunner._currentTestStartTime = new Date().valueOf(); + TestRunner._timeoutFactor = 1; + TestRunner._expectedMinAsserts = 0; + TestRunner._expectedMaxAsserts = 0; + + TestRunner.structuredLogger.testStart(url); + + if (TestRunner._urls[TestRunner._currentTest].test.allow_xul_xbl) { + await SpecialPowers.pushPermissions([ + { type: "allowXULXBL", allow: true, context: "http://mochi.test:8888" }, + { type: "allowXULXBL", allow: true, context: "http://example.org" }, + ]); + } + TestRunner._makeIframe(url, 0); + } else { + $("current-test").innerHTML = "<b>Finished</b>"; + // Only unload the last test to run if we're running more than one test. + if (TestRunner._urls.length > 1) { + TestRunner._makeIframe("about:blank", 0); + } + + var passCount = parseInt($("pass-count").innerHTML, 10); + var failCount = parseInt($("fail-count").innerHTML, 10); + var todoCount = parseInt($("todo-count").innerHTML, 10); + + if (passCount === 0 && failCount === 0 && todoCount === 0) { + // No |$('testframe').contentWindow|, so manually update: ... + // ... the log, + TestRunner.structuredLogger.error( + "TEST-UNEXPECTED-FAIL | SimpleTest/TestRunner.js | No checks actually run" + ); + // ... the count, + $("fail-count").innerHTML = 1; + // ... the indicator. + var indicator = $("indicator"); + indicator.innerHTML = "Status: Fail (No checks actually run)"; + indicator.style.backgroundColor = "red"; + } + + let e10sMode = SpecialPowers.isMainProcess() ? "non-e10s" : "e10s"; + + TestRunner.structuredLogger.info("TEST-START | Shutdown"); + TestRunner.structuredLogger.info("Passed: " + passCount); + TestRunner.structuredLogger.info("Failed: " + failCount); + TestRunner.structuredLogger.info("Todo: " + todoCount); + TestRunner.structuredLogger.info("Mode: " + e10sMode); + TestRunner.structuredLogger.info( + "Slowest: " + + TestRunner.slowestTestTime + + "ms - " + + TestRunner.slowestTestURL + ); + + // If we are looping, don't send this cause it closes the log file, + // also don't unregister the crash observers until we're done. + if (TestRunner.repeat === 0) { + SpecialPowers.unregisterProcessCrashObservers(); + TestRunner.structuredLogger.info("SimpleTest FINISHED"); + } + + if (TestRunner.repeat === 0 && TestRunner.onComplete) { + TestRunner.onComplete(); + } + + if ( + TestRunner._currentLoop <= TestRunner.repeat && + !TestRunner._haltTests + ) { + TestRunner._currentLoop++; + TestRunner.resetTests(TestRunner._urls); + TestRunner._loopIsRestarting = true; + } else { + // Loops are finished + if (TestRunner.logEnabled) { + TestRunner.structuredLogger.info( + "TEST-INFO | Ran " + TestRunner._currentLoop + " Loops" + ); + TestRunner.structuredLogger.info("SimpleTest FINISHED"); + } + + if (TestRunner.onComplete) { + TestRunner.onComplete(); + } + } + TestRunner.generateFailureList(); + + if (TestRunner.jscovDirPrefix != "") { + coverageCollector.finalize(); + } + } +} +TestRunner.runNextTest = _runNextTest; + +TestRunner.expectChildProcessCrash = function () { + TestRunner._expectingProcessCrash = true; +}; + +/** + * This stub is called by SimpleTest when a test is finished. + **/ +TestRunner.testFinished = function (tests) { + // Need to track subtests recorded here separately or else they'll + // trigger the `result after SimpleTest.finish()` error. + var extraTests = []; + var result = "OK"; + + // Prevent a test from calling finish() multiple times before we + // have a chance to unload it. + if ( + TestRunner._currentTest == TestRunner._lastTestFinished && + !TestRunner._loopIsRestarting + ) { + TestRunner.structuredLogger.testEnd( + TestRunner.currentTestURL, + "ERROR", + "OK", + "called finish() multiple times" + ); + TestRunner.updateUI([{ result: false }]); + return; + } + + if (TestRunner.jscovDirPrefix != "") { + coverageCollector.recordTestCoverage(TestRunner.currentTestURL); + } + + SpecialPowers.requestDumpCoverageCounters().then(() => { + TestRunner._lastTestFinished = TestRunner._currentTest; + TestRunner._loopIsRestarting = false; + + // TODO : replace this by a function that returns the mem data as an object + // that's dumped later with the test_end message + MemoryStats.dump( + TestRunner._currentTest, + TestRunner.currentTestURL, + TestRunner.dumpOutputDirectory, + TestRunner.dumpAboutMemoryAfterTest, + TestRunner.dumpDMDAfterTest + ); + + async function cleanUpCrashDumpFiles() { + if ( + !(await SpecialPowers.removeExpectedCrashDumpFiles( + TestRunner._expectingProcessCrash + )) + ) { + let subtest = "expected-crash-dump-missing"; + TestRunner.structuredLogger.testStatus( + TestRunner.currentTestURL, + subtest, + "ERROR", + "PASS", + "This test did not leave any crash dumps behind, but we were expecting some!" + ); + extraTests.push({ name: subtest, result: false }); + result = "ERROR"; + } + + var unexpectedCrashDumpFiles = + await SpecialPowers.findUnexpectedCrashDumpFiles(); + TestRunner._expectingProcessCrash = false; + if (unexpectedCrashDumpFiles.length) { + let subtest = "unexpected-crash-dump-found"; + TestRunner.structuredLogger.testStatus( + TestRunner.currentTestURL, + subtest, + "ERROR", + "PASS", + "This test left crash dumps behind, but we " + + "weren't expecting it to!", + null, + { unexpected_crashdump_files: unexpectedCrashDumpFiles } + ); + extraTests.push({ name: subtest, result: false }); + result = "CRASH"; + unexpectedCrashDumpFiles.sort().forEach(function (aFilename) { + TestRunner.structuredLogger.info( + "Found unexpected crash dump file " + aFilename + "." + ); + }); + } + + if (TestRunner.cleanupCrashes) { + if (await SpecialPowers.removePendingCrashDumpFiles()) { + TestRunner.structuredLogger.info( + "This test left pending crash dumps" + ); + } + } + } + + function runNextTest() { + if (TestRunner.currentTestURL != TestRunner.getLoadedTestURL()) { + TestRunner.structuredLogger.testStatus( + TestRunner.currentTestURL, + TestRunner.getLoadedTestURL(), + "FAIL", + "PASS", + "finished in a non-clean fashion, probably" + + " because it didn't call SimpleTest.finish()", + { loaded_test_url: TestRunner.getLoadedTestURL() } + ); + extraTests.push({ name: "clean-finish", result: false }); + result = result != "CRASH" ? "ERROR" : result; + } + + SpecialPowers.addProfilerMarker( + "TestRunner", + { category: "Test", startTime: TestRunner._currentTestStartTimestamp }, + TestRunner.currentTestURL + ); + var runtime = new Date().valueOf() - TestRunner._currentTestStartTime; + + if ( + TestRunner.slowestTestTime < runtime && + TestRunner._timeoutFactor >= 1 + ) { + TestRunner.slowestTestTime = runtime; + TestRunner.slowestTestURL = TestRunner.currentTestURL; + } + + TestRunner.updateUI(tests.concat(extraTests)); + + // Don't show the interstitial if we just run one test with no repeats: + if (TestRunner._urls.length == 1 && TestRunner.repeat <= 1) { + TestRunner.testUnloaded(result, runtime); + return; + } + + var interstitialURL; + if ( + !testInXOriginFrame() && + $("testframe").contentWindow.location.protocol == "chrome:" + ) { + interstitialURL = + "tests/SimpleTest/iframe-between-tests.html?result=" + + result + + "&runtime=" + + runtime; + } else { + interstitialURL = + "/tests/SimpleTest/iframe-between-tests.html?result=" + + result + + "&runtime=" + + runtime; + } + // check if there were test run after SimpleTest.finish, which should never happen + if (!testInXOriginFrame()) { + $("testframe").contentWindow.addEventListener("unload", function () { + var testwin = $("testframe").contentWindow; + if (testwin.SimpleTest) { + if (typeof testwin.SimpleTest.testsLength === "undefined") { + TestRunner.structuredLogger.error( + "TEST-UNEXPECTED-FAIL | " + + TestRunner.currentTestURL + + " fired an unload callback with missing test data," + + " possibly due to the test navigating or reloading" + ); + TestRunner.updateUI([{ result: false }]); + } else if ( + testwin.SimpleTest._tests.length != testwin.SimpleTest.testsLength + ) { + var didReportError = false; + var wrongtestlength = + testwin.SimpleTest._tests.length - + testwin.SimpleTest.testsLength; + var wrongtestname = ""; + for (var i = 0; i < wrongtestlength; i++) { + wrongtestname = + testwin.SimpleTest._tests[testwin.SimpleTest.testsLength + i] + .name; + TestRunner.structuredLogger.error( + "TEST-UNEXPECTED-FAIL | " + + TestRunner.currentTestURL + + " logged result after SimpleTest.finish(): " + + wrongtestname + ); + didReportError = true; + } + if (!didReportError) { + // This clause shouldn't be reachable, but if we somehow get + // here (e.g. if wrongtestlength is somehow negative), it's + // important that we log *something* for the { result: false } + // test-failure that we're about to post. + TestRunner.structuredLogger.error( + "TEST-UNEXPECTED-FAIL | " + + TestRunner.currentTestURL + + " hit an unexpected condition when checking for" + + " logged results after SimpleTest.finish()" + ); + } + TestRunner.updateUI([{ result: false }]); + } + } + }); + } + TestRunner._makeIframe(interstitialURL, 0); + } + + SpecialPowers.executeAfterFlushingMessageQueue(async function () { + await SpecialPowers.waitForCrashes(TestRunner._expectingProcessCrash); + await cleanUpCrashDumpFiles(); + await SpecialPowers.flushPermissions(); + await SpecialPowers.flushPrefEnv(); + runNextTest(); + }); + }); +}; + +/** + * This stub is called by XOrigin Tests to report assertion count. + **/ +TestRunner._xoriginAssertionCount = 0; +TestRunner.addAssertionCount = function (count) { + if (!testInXOriginFrame()) { + TestRunner.error( + `addAssertionCount should only be called by a cross origin test` + ); + return; + } + + if (testInDifferentProcess()) { + TestRunner._xoriginAssertionCount += count; + } +}; + +TestRunner.testUnloaded = function (result, runtime) { + // If we're in a debug build, check assertion counts. This code is + // similar to the code in Tester_nextTest in browser-test.js used + // for browser-chrome mochitests. + if (SpecialPowers.isDebugBuild) { + var newAssertionCount = + SpecialPowers.assertionCount() + TestRunner._xoriginAssertionCount; + var numAsserts = newAssertionCount - TestRunner._lastAssertionCount; + TestRunner._lastAssertionCount = newAssertionCount; + + var max = TestRunner._expectedMaxAsserts; + var min = TestRunner._expectedMinAsserts; + if (Array.isArray(TestRunner.expected)) { + // Accumulate all assertion counts recorded in the failure pattern file. + let additionalAsserts = TestRunner.expected.reduce( + (acc, [pat, count]) => { + return pat == "ASSERTION" ? acc + count : acc; + }, + 0 + ); + min += additionalAsserts; + max += additionalAsserts; + } + + TestRunner.structuredLogger.assertionCount( + TestRunner.currentTestURL, + numAsserts, + min, + max + ); + + if (numAsserts < min || numAsserts > max) { + result = "ERROR"; + + var direction = "more"; + var target = max; + if (numAsserts < min) { + direction = "less"; + target = min; + } + TestRunner.structuredLogger.testStatus( + TestRunner.currentTestURL, + "Assertion Count", + "ERROR", + "PASS", + numAsserts + + " is " + + direction + + " than expected " + + target + + " assertions" + ); + + // reset result so we don't print a second error on test-end + result = "OK"; + } + } + + TestRunner.structuredLogger.testEnd( + TestRunner.currentTestURL, + result, + "OK", + "Finished in " + runtime + "ms", + { runtime } + ); + + // Always do this, so we can "reset" preferences between tests + SpecialPowers.comparePrefsToBaseline( + TestRunner.ignorePrefs, + TestRunner.verifyPrefsNextTest + ); +}; + +TestRunner.verifyPrefsNextTest = function (p) { + if (TestRunner.comparePrefs) { + let prefs = Array.from(SpecialPowers.Cu.waiveXrays(p), x => + SpecialPowers.unwrapIfWrapped(SpecialPowers.Cu.unwaiveXrays(x)) + ); + prefs.forEach(pr => + TestRunner.structuredLogger.error( + "TEST-UNEXPECTED-FAIL | " + + TestRunner.currentTestURL + + " | changed preference: " + + pr + ) + ); + } + TestRunner.doNextTest(); +}; + +TestRunner.doNextTest = function () { + TestRunner._currentTest++; + if (TestRunner.runSlower) { + setTimeout(TestRunner.runNextTest, 1000); + } else { + TestRunner.runNextTest(); + } +}; + +/** + * Get the results. + */ +TestRunner.countResults = function (tests) { + var nOK = 0; + var nNotOK = 0; + var nTodo = 0; + for (var i = 0; i < tests.length; ++i) { + var test = tests[i]; + if (test.todo && !test.result) { + nTodo++; + } else if (test.result && !test.todo) { + nOK++; + } else { + nNotOK++; + } + } + return { OK: nOK, notOK: nNotOK, todo: nTodo }; +}; + +/** + * Print out table of any error messages found during looped run + */ +TestRunner.displayLoopErrors = function (tableName, tests) { + if (TestRunner.countResults(tests).notOK > 0) { + var table = $(tableName); + var curtest; + if (!table.rows.length) { + //if table headers are not yet generated, make them + var row = table.insertRow(table.rows.length); + var cell = row.insertCell(0); + var textNode = document.createTextNode("Test File Name:"); + cell.appendChild(textNode); + cell = row.insertCell(1); + textNode = document.createTextNode("Test:"); + cell.appendChild(textNode); + cell = row.insertCell(2); + textNode = document.createTextNode("Error message:"); + cell.appendChild(textNode); + } + + //find the broken test + for (var testnum in tests) { + curtest = tests[testnum]; + if ( + !( + (curtest.todo && !curtest.result) || + (curtest.result && !curtest.todo) + ) + ) { + //this is a failed test or the result of todo test. Display the related message + row = table.insertRow(table.rows.length); + cell = row.insertCell(0); + textNode = document.createTextNode(TestRunner.currentTestURL); + cell.appendChild(textNode); + cell = row.insertCell(1); + textNode = document.createTextNode(curtest.name); + cell.appendChild(textNode); + cell = row.insertCell(2); + textNode = document.createTextNode(curtest.diag ? curtest.diag : ""); + cell.appendChild(textNode); + } + } + } +}; + +TestRunner.updateUI = function (tests) { + var results = TestRunner.countResults(tests); + var passCount = parseInt($("pass-count").innerHTML) + results.OK; + var failCount = parseInt($("fail-count").innerHTML) + results.notOK; + var todoCount = parseInt($("todo-count").innerHTML) + results.todo; + $("pass-count").innerHTML = passCount; + $("fail-count").innerHTML = failCount; + $("todo-count").innerHTML = todoCount; + + // Set the top Green/Red bar + var indicator = $("indicator"); + if (failCount > 0) { + indicator.innerHTML = "Status: Fail"; + indicator.style.backgroundColor = "red"; + } else if (passCount > 0) { + indicator.innerHTML = "Status: Pass"; + indicator.style.backgroundColor = "#0d0"; + } else { + indicator.innerHTML = "Status: ToDo"; + indicator.style.backgroundColor = "orange"; + } + + // Set the table values + var trID = "tr-" + $("current-test-path").innerHTML; + var row = $(trID); + + // Only update the row if it actually exists (autoUI) + if (row != null) { + var tds = row.getElementsByTagName("td"); + tds[0].style.backgroundColor = "#0d0"; + tds[0].innerHTML = parseInt(tds[0].innerHTML) + parseInt(results.OK); + tds[1].style.backgroundColor = results.notOK > 0 ? "red" : "#0d0"; + tds[1].innerHTML = parseInt(tds[1].innerHTML) + parseInt(results.notOK); + tds[2].style.backgroundColor = results.todo > 0 ? "orange" : "#0d0"; + tds[2].innerHTML = parseInt(tds[2].innerHTML) + parseInt(results.todo); + } + + //if we ran in a loop, display any found errors + if (TestRunner.repeat > 0) { + TestRunner.displayLoopErrors("fail-table", tests); + } +}; + +// XOrigin Tests +// If "--enable-xorigin-tests" is set, mochitests are run in a cross origin iframe. +// The parent process will run at http://mochi.xorigin-test:8888", and individual +// mochitests will be launched in a cross-origin iframe at http://mochi.test:8888. + +var xOriginDispatchMap = { + runner: TestRunner, + logger: TestRunner.structuredLogger, + addFailedTest: TestRunner.addFailedTest, + expectAssertions: TestRunner.expectAssertions, + expectChildProcessCrash: TestRunner.expectChildProcessCrash, + requestLongerTimeout: TestRunner.requestLongerTimeout, + "structuredLogger.deactivateBuffering": + TestRunner.structuredLogger.deactivateBuffering, + "structuredLogger.activateBuffering": + TestRunner.structuredLogger.activateBuffering, + "structuredLogger.testStatus": TestRunner.structuredLogger.testStatus, + "structuredLogger.info": TestRunner.structuredLogger.info, + "structuredLogger.warning": TestRunner.structuredLogger.warning, + "structuredLogger.error": TestRunner.structuredLogger.error, + testFinished: TestRunner.testFinished, + addAssertionCount: TestRunner.addAssertionCount, +}; + +function xOriginTestRunnerHandler(event) { + if (event.data.harnessType != "SimpleTest") { + return; + } + // Handles messages from xOriginRunner in SimpleTest.js. + if (event.data.command in xOriginDispatchMap) { + xOriginDispatchMap[event.data.command].apply( + xOriginDispatchMap[event.data.applyOn], + event.data.params + ); + } else { + TestRunner.error(`Command ${event.data.command} not found + in xOriginDispatchMap`); + } +} + +TestRunner.setXOriginEventHandler = function () { + window.addEventListener("message", xOriginTestRunnerHandler); +}; |