tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

TestRunner.js (35642B)


      1 /* -*- js-indent-level: 4; indent-tabs-mode: nil -*- */
      2 /*
      3 * e10s event dispatcher from content->chrome
      4 *
      5 * type = eventName (QuitApplication)
      6 * data = json object {"filename":filename} <- for LoggerInit
      7 */
      8 
      9 // This file expects the following files to be loaded.
     10 /* import-globals-from LogController.js */
     11 /* import-globals-from MemoryStats.js */
     12 /* import-globals-from MozillaLogger.js */
     13 
     14 /* eslint-disable no-unsanitized/property */
     15 
     16 "use strict";
     17 
     18 const { StructuredLogger, StructuredFormatter } =
     19  SpecialPowers.ChromeUtils.importESModule(
     20    "resource://testing-common/StructuredLog.sys.mjs"
     21  );
     22 
     23 function getElement(id) {
     24  return typeof id == "string" ? document.getElementById(id) : id;
     25 }
     26 
     27 this.$ = this.getElement;
     28 
     29 function contentDispatchEvent(type, data, sync) {
     30  if (typeof data == "undefined") {
     31    data = {};
     32  }
     33 
     34  var event = new CustomEvent("contentEvent", {
     35    bubbles: true,
     36    detail: {
     37      sync,
     38      type,
     39      data: JSON.stringify(data),
     40    },
     41  });
     42  document.dispatchEvent(event);
     43 }
     44 
     45 function contentAsyncEvent(type, data) {
     46  contentDispatchEvent(type, data, 0);
     47 }
     48 
     49 /* Helper Function */
     50 function extend(obj, /* optional */ skip) {
     51  // Extend an array with an array-like object starting
     52  // from the skip index
     53  if (!skip) {
     54    skip = 0;
     55  }
     56  if (obj) {
     57    var l = obj.length;
     58    var ret = [];
     59    for (var i = skip; i < l; i++) {
     60      ret.push(obj[i]);
     61    }
     62  }
     63  return ret;
     64 }
     65 
     66 function flattenArguments(/* ...*/) {
     67  var res = [];
     68  var args = extend(arguments);
     69  while (args.length) {
     70    var o = args.shift();
     71    if (o && typeof o == "object" && typeof o.length == "number") {
     72      for (var i = o.length - 1; i >= 0; i--) {
     73        args.unshift(o[i]);
     74      }
     75    } else {
     76      res.push(o);
     77    }
     78  }
     79  return res;
     80 }
     81 
     82 function testInXOriginFrame() {
     83  // Check if the test running in an iframe is a cross origin test.
     84  try {
     85    $("testframe").contentWindow.origin;
     86    return false;
     87  } catch (e) {
     88    return true;
     89  }
     90 }
     91 
     92 function testInDifferentProcess() {
     93  // Check if the test running in an iframe that is loaded in a different process.
     94  return SpecialPowers.Cu.isRemoteProxy($("testframe").contentWindow);
     95 }
     96 
     97 /**
     98 * TestRunner: A test runner for SimpleTest
     99 * TODO:
    100 *
    101 *  * Avoid moving iframes: That causes reloads on mozilla and opera.
    102 *
    103 *
    104 */
    105 var TestRunner = {};
    106 TestRunner.logEnabled = false;
    107 TestRunner._currentTest = 0;
    108 TestRunner._lastTestFinished = -1;
    109 TestRunner._loopIsRestarting = false;
    110 TestRunner.currentTestURL = "";
    111 TestRunner.originalTestURL = "";
    112 TestRunner._urls = [];
    113 TestRunner._lastAssertionCount = 0;
    114 TestRunner._expectedMinAsserts = 0;
    115 TestRunner._expectedMaxAsserts = 0;
    116 
    117 TestRunner.timeout = 300 * 1000; // 5 minutes.
    118 TestRunner.maxTimeouts = 4; // halt testing after too many timeouts
    119 TestRunner.runSlower = false;
    120 TestRunner.dumpOutputDirectory = "";
    121 TestRunner.dumpAboutMemoryAfterTest = false;
    122 TestRunner.dumpDMDAfterTest = false;
    123 TestRunner.slowestTestTime = 0;
    124 TestRunner.slowestTestURL = "";
    125 TestRunner.interactiveDebugger = false;
    126 TestRunner.cleanupCrashes = false;
    127 TestRunner.timeoutAsPass = false;
    128 TestRunner.conditionedProfile = false;
    129 TestRunner.comparePrefs = false;
    130 
    131 TestRunner._expectingProcessCrash = false;
    132 TestRunner._structuredFormatter = new StructuredFormatter();
    133 
    134 /**
    135 * Make sure the tests don't hang indefinitely.
    136 */
    137 TestRunner._numTimeouts = 0;
    138 TestRunner._currentTestStartTime = new Date().valueOf();
    139 TestRunner._timeoutFactor = 1;
    140 
    141 /**
    142 * Used to collect code coverage with the js debugger.
    143 */
    144 TestRunner.jscovDirPrefix = "";
    145 var coverageCollector = {};
    146 
    147 function record(succeeded, expectedFail, msg) {
    148  let successInfo;
    149  let failureInfo;
    150  if (expectedFail) {
    151    successInfo = {
    152      status: "PASS",
    153      expected: "FAIL",
    154      message: "TEST-UNEXPECTED-PASS",
    155    };
    156    failureInfo = {
    157      status: "FAIL",
    158      expected: "FAIL",
    159      message: "TEST-KNOWN-FAIL",
    160    };
    161  } else {
    162    successInfo = {
    163      status: "PASS",
    164      expected: "PASS",
    165      message: "TEST-PASS",
    166    };
    167    failureInfo = {
    168      status: "FAIL",
    169      expected: "PASS",
    170      message: "TEST-UNEXPECTED-FAIL",
    171    };
    172  }
    173 
    174  let result = succeeded ? successInfo : failureInfo;
    175 
    176  TestRunner.structuredLogger.testStatus(
    177    TestRunner.currentTestURL,
    178    msg,
    179    result.status,
    180    result.expected,
    181    "",
    182    ""
    183  );
    184 }
    185 
    186 TestRunner._checkForHangs = function () {
    187  function reportError(win, msg) {
    188    if (testInXOriginFrame() || "SimpleTest" in win) {
    189      record(false, TestRunner.timeoutAsPass, msg);
    190    } else if ("W3CTest" in win) {
    191      win.W3CTest.logFailure(msg);
    192    }
    193  }
    194 
    195  async function killTest(win) {
    196    if (testInXOriginFrame()) {
    197      win.postMessage("SimpleTest:timeout", "*");
    198    } else if ("SimpleTest" in win) {
    199      await win.SimpleTest.timeout();
    200      win.SimpleTest.finish();
    201    } else if ("W3CTest" in win) {
    202      await win.W3CTest.timeout();
    203    }
    204  }
    205 
    206  if (TestRunner._currentTest < TestRunner._urls.length) {
    207    var runtime = new Date().valueOf() - TestRunner._currentTestStartTime;
    208    if (
    209      !TestRunner.interactiveDebugger &&
    210      runtime >= TestRunner.timeout * TestRunner._timeoutFactor
    211    ) {
    212      let testIframe = $("testframe");
    213      var frameWindow =
    214        (!testInXOriginFrame() && testIframe.contentWindow.wrappedJSObject) ||
    215        testIframe.contentWindow;
    216      reportError(frameWindow, "Test timed out.");
    217      TestRunner.updateUI([{ result: false }]);
    218 
    219      // If we have too many timeouts, give up. We don't want to wait hours
    220      // for results if some bug causes lots of tests to time out.
    221      if (
    222        ++TestRunner._numTimeouts >= TestRunner.maxTimeouts ||
    223        TestRunner.runUntilFailure
    224      ) {
    225        TestRunner._haltTests = true;
    226 
    227        TestRunner.currentTestURL = "(SimpleTest/TestRunner.js)";
    228        reportError(
    229          frameWindow,
    230          TestRunner.maxTimeouts + " test timeouts, giving up."
    231        );
    232        var skippedTests = TestRunner._urls.length - TestRunner._currentTest;
    233        reportError(
    234          frameWindow,
    235          "Skipping " + skippedTests + " remaining tests."
    236        );
    237      }
    238 
    239      // Add a little (1 second) delay to ensure automation.py has time to notice
    240      // "Test timed out" log and process it (= take a screenshot).
    241      setTimeout(async function delayedKillTest() {
    242        try {
    243          await killTest(frameWindow);
    244        } catch (e) {
    245          reportError(frameWindow, "Test error: " + e);
    246          TestRunner.updateUI([{ result: false }]);
    247        }
    248      }, 1000);
    249 
    250      if (TestRunner._haltTests) {
    251        return;
    252      }
    253    }
    254 
    255    setTimeout(TestRunner._checkForHangs, 30000);
    256  }
    257 };
    258 
    259 TestRunner.requestLongerTimeout = function (factor) {
    260  TestRunner._timeoutFactor = factor;
    261 };
    262 
    263 /**
    264 * This is used to loop tests
    265 */
    266 TestRunner.repeat = 0;
    267 TestRunner._currentLoop = 1;
    268 
    269 TestRunner.expectAssertions = function (min, max) {
    270  if (typeof max == "undefined") {
    271    max = min;
    272  }
    273  if (
    274    typeof min != "number" ||
    275    typeof max != "number" ||
    276    min < 0 ||
    277    max < min
    278  ) {
    279    throw new Error("bad parameter to expectAssertions");
    280  }
    281  TestRunner._expectedMinAsserts = min;
    282  TestRunner._expectedMaxAsserts = max;
    283 };
    284 
    285 /**
    286 * This function is called after generating the summary.
    287 */
    288 TestRunner.onComplete = null;
    289 
    290 /**
    291 * Adds a failed test case to a list so we can rerun only the failed tests
    292 */
    293 TestRunner._failedTests = {};
    294 TestRunner._failureFile = "";
    295 
    296 TestRunner.addFailedTest = function (testName) {
    297  if (TestRunner._failedTests[testName] == undefined) {
    298    TestRunner._failedTests[testName] = "";
    299  }
    300 };
    301 
    302 TestRunner.setFailureFile = function (fileName) {
    303  TestRunner._failureFile = fileName;
    304 };
    305 
    306 TestRunner.generateFailureList = function () {
    307  if (TestRunner._failureFile) {
    308    var failures = new MozillaFileLogger(TestRunner._failureFile);
    309    failures.log(JSON.stringify(TestRunner._failedTests));
    310    failures.close();
    311  }
    312 };
    313 
    314 /**
    315 * If logEnabled is true, this is the logger that will be used.
    316 */
    317 
    318 // This delimiter is used to avoid interleaving Mochitest/Gecko logs.
    319 var LOG_DELIMITER = "\ue175\uee31\u2c32\uacbf";
    320 
    321 // A log callback for StructuredLog.sys.mjs
    322 TestRunner._dumpMessage = function (message) {
    323  var str;
    324 
    325  // This is a directive to python to format these messages
    326  // for compatibility with mozharness. This can be removed
    327  // with the MochitestFormatter (see bug 1045525).
    328  message.js_source = "TestRunner.js";
    329  if (
    330    TestRunner.interactiveDebugger &&
    331    message.action in TestRunner._structuredFormatter
    332  ) {
    333    str = TestRunner._structuredFormatter[message.action](message);
    334  } else {
    335    str = LOG_DELIMITER + JSON.stringify(message) + LOG_DELIMITER;
    336  }
    337  // BUGFIX: browser-chrome tests don't use LogController
    338  if (Object.keys(LogController.listeners).length !== 0) {
    339    LogController.log(str);
    340  } else {
    341    dump("\n" + str + "\n");
    342  }
    343  // Checking for error messages
    344  if (message.expected || message.level === "ERROR") {
    345    TestRunner.failureHandler();
    346  }
    347 };
    348 
    349 // From https://searchfox.org/mozilla-central/source/testing/modules/StructuredLog.sys.mjs
    350 TestRunner.structuredLogger = new StructuredLogger(
    351  "mochitest",
    352  TestRunner._dumpMessage,
    353  [],
    354  TestRunner
    355 );
    356 TestRunner.structuredLogger.deactivateBuffering = function () {
    357  TestRunner.structuredLogger.logData("buffering_off");
    358 };
    359 TestRunner.structuredLogger.activateBuffering = function () {
    360  TestRunner.structuredLogger.logData("buffering_on");
    361 };
    362 
    363 TestRunner.log = function (msg) {
    364  if (TestRunner.logEnabled) {
    365    TestRunner.structuredLogger.info(msg);
    366  } else {
    367    dump(msg + "\n");
    368  }
    369 };
    370 
    371 TestRunner.error = function (msg) {
    372  if (TestRunner.logEnabled) {
    373    TestRunner.structuredLogger.error(msg);
    374  } else {
    375    dump(msg + "\n");
    376    TestRunner.failureHandler();
    377  }
    378 };
    379 
    380 TestRunner.failureHandler = function () {
    381  if (TestRunner.runUntilFailure) {
    382    TestRunner._haltTests = true;
    383  }
    384 
    385  if (TestRunner.debugOnFailure) {
    386    // You've hit this line because you requested to break into the
    387    // debugger upon a testcase failure on your test run.
    388    // eslint-disable-next-line no-debugger
    389    debugger;
    390  }
    391 };
    392 
    393 /**
    394 * Toggle element visibility
    395 */
    396 TestRunner._toggle = function (el) {
    397  if (el.className == "noshow") {
    398    el.className = "";
    399    el.style.cssText = "";
    400  } else {
    401    el.className = "noshow";
    402    el.style.cssText = "width:0px; height:0px; border:0px;";
    403  }
    404 };
    405 
    406 /**
    407 * Creates the iframe that contains a test
    408 */
    409 TestRunner._makeIframe = function (url, retry) {
    410  var iframe = $("testframe");
    411  if (
    412    url != "about:blank" &&
    413    (("hasFocus" in document && !document.hasFocus()) ||
    414      ("activeElement" in document && document.activeElement != iframe))
    415  ) {
    416    contentAsyncEvent("Focus");
    417    window.focus();
    418    SpecialPowers.focus();
    419    iframe.focus();
    420    if (retry < 3) {
    421      window.setTimeout(function () {
    422        TestRunner._makeIframe(url, retry + 1);
    423      });
    424      return;
    425    }
    426 
    427    TestRunner.structuredLogger.info(
    428      "Error: Unable to restore focus, expect failures and timeouts."
    429    );
    430  }
    431  window.scrollTo(0, $("indicator").offsetTop);
    432  try {
    433    let urlObj = new URL(url);
    434    if (TestRunner.xOriginTests) {
    435      // The test will run in a xorigin iframe, so we pass in additional test params in the
    436      // URL since the content process won't be able to access them from the parentRunner
    437      // directly.
    438      let params = TestRunner.getParameterInfo();
    439      urlObj.searchParams.append(
    440        "currentTestURL",
    441        urlObj.pathname.replace("/tests/", "")
    442      );
    443      urlObj.searchParams.append("closeWhenDone", params.closeWhenDone);
    444      urlObj.searchParams.append("showTestReport", TestRunner.showTestReport);
    445      urlObj.searchParams.append("expected", TestRunner.expected);
    446      iframe.src = urlObj.href;
    447    } else {
    448      iframe.src = url;
    449    }
    450  } catch {
    451    // If the provided `url` is not a valid URL (i.e. doesn't include a protocol)
    452    // then the new URL() constructor will raise a TypeError. This is expected in the
    453    // usual case (i.e. non-xorigin iFrame tests) so set the URL in the usual way.
    454    iframe.src = url;
    455  }
    456  iframe.name = url;
    457  iframe.width = "500";
    458 };
    459 
    460 /**
    461 * Returns the current test URL.
    462 * We use this to tell whether the test has navigated to another test without
    463 * being finished first.
    464 */
    465 TestRunner.getLoadedTestURL = function () {
    466  if (!testInXOriginFrame()) {
    467    var prefix = "";
    468    // handle mochitest-chrome URIs
    469    if ($("testframe").contentWindow.location.protocol == "chrome:") {
    470      prefix = "chrome://mochitests";
    471    }
    472    return prefix + $("testframe").contentWindow.location.pathname;
    473  }
    474  return TestRunner.currentTestURL;
    475 };
    476 
    477 TestRunner.setParameterInfo = function (params) {
    478  this._params = params;
    479 };
    480 
    481 TestRunner.getParameterInfo = function () {
    482  return this._params;
    483 };
    484 
    485 /**
    486 * Print information about which prefs are set.
    487 * This is used to help validate that the tests are actually
    488 * running in the expected context.
    489 */
    490 TestRunner.dumpPrefContext = function () {
    491  let prefs = ["fission.autostart"];
    492 
    493  let message = ["Dumping test context:"];
    494  prefs.forEach(function formatPref(pref) {
    495    let val = SpecialPowers.getBoolPref(pref);
    496    message.push(pref + "=" + val);
    497  });
    498  TestRunner.structuredLogger.info(message.join("\n  "));
    499 };
    500 
    501 /**
    502 * TestRunner entry point.
    503 *
    504 * The arguments are the URLs of the test to be ran.
    505 *
    506 */
    507 TestRunner.runTests = function (/*url...*/) {
    508  TestRunner.structuredLogger.info("SimpleTest START");
    509  TestRunner.dumpPrefContext();
    510  TestRunner.originalTestURL = $("current-test").innerHTML;
    511 
    512  SpecialPowers.registerProcessCrashObservers();
    513 
    514  // Initialize code coverage
    515  if (TestRunner.jscovDirPrefix != "") {
    516    var { CoverageCollector } = SpecialPowers.ChromeUtils.importESModule(
    517      "resource://testing-common/CoverageUtils.sys.mjs"
    518    );
    519    coverageCollector = new CoverageCollector(TestRunner.jscovDirPrefix);
    520  }
    521 
    522  SpecialPowers.requestResetCoverageCounters().then(() => {
    523    TestRunner._urls = flattenArguments(arguments);
    524 
    525    var singleTestRun = this._urls.length <= 1 && TestRunner.repeat <= 1;
    526    TestRunner.showTestReport = singleTestRun;
    527    var frame = $("testframe");
    528    frame.src = "";
    529    if (singleTestRun) {
    530      // Can't use document.body because this runs in a XUL doc as well...
    531      var body = document.getElementsByTagName("body")[0];
    532      body.setAttribute("singletest", "true");
    533      frame.removeAttribute("scrolling");
    534    }
    535    TestRunner._checkForHangs();
    536    TestRunner.runNextTest();
    537  });
    538 };
    539 
    540 /**
    541 * Used for running a set of tests in a loop for debugging purposes
    542 * Takes an array of URLs
    543 */
    544 TestRunner.resetTests = function (listURLs) {
    545  TestRunner._currentTest = 0;
    546  // Reset our "Current-test" line - functionality depends on it
    547  $("current-test").innerHTML = TestRunner.originalTestURL;
    548  if (TestRunner.logEnabled) {
    549    TestRunner.structuredLogger.info(
    550      "SimpleTest START Loop " + TestRunner._currentLoop
    551    );
    552  }
    553 
    554  TestRunner._urls = listURLs;
    555  $("testframe").src = "";
    556  TestRunner._checkForHangs();
    557  TestRunner.runNextTest();
    558 };
    559 
    560 TestRunner.getNextUrl = function () {
    561  var url = "";
    562  // sometimes we have a subtest/harness which doesn't use a manifest
    563  if (
    564    TestRunner._urls[TestRunner._currentTest] instanceof Object &&
    565    "test" in TestRunner._urls[TestRunner._currentTest]
    566  ) {
    567    url = TestRunner._urls[TestRunner._currentTest].test.url;
    568    TestRunner.expected =
    569      TestRunner._urls[TestRunner._currentTest].test.expected;
    570  } else {
    571    url = TestRunner._urls[TestRunner._currentTest];
    572    TestRunner.expected = "pass";
    573  }
    574  return url;
    575 };
    576 
    577 /**
    578 * Run the next test. If no test remains, calls onComplete().
    579 */
    580 TestRunner._haltTests = false;
    581 async function _runNextTest() {
    582  if (
    583    TestRunner._currentTest < TestRunner._urls.length &&
    584    !TestRunner._haltTests
    585  ) {
    586    var url = TestRunner.getNextUrl();
    587    TestRunner.currentTestURL = url;
    588 
    589    $("current-test-path").innerHTML = url;
    590 
    591    TestRunner._currentTestStartTimestamp = SpecialPowers.ChromeUtils.now();
    592    TestRunner._currentTestStartTime = new Date().valueOf();
    593    TestRunner._timeoutFactor = 1;
    594    TestRunner._expectedMinAsserts = 0;
    595    TestRunner._expectedMaxAsserts = 0;
    596 
    597    TestRunner.structuredLogger.testStart(url);
    598 
    599    if (TestRunner._urls[TestRunner._currentTest].test.allow_xul_xbl) {
    600      await SpecialPowers.pushPermissions([
    601        { type: "allowXULXBL", allow: true, context: "http://mochi.test:8888" },
    602        { type: "allowXULXBL", allow: true, context: "http://example.org" },
    603      ]);
    604    }
    605    if (TestRunner._urls[TestRunner._currentTest].test.https_first_disabled) {
    606      await SpecialPowers.pushPrefEnv({
    607        set: [["dom.security.https_first", false]],
    608      });
    609    }
    610    TestRunner._makeIframe(url, 0);
    611  } else {
    612    $("current-test").innerHTML = "<b>Finished</b>";
    613    // Only unload the last test to run if we're running more than one test.
    614    if (TestRunner._urls.length > 1) {
    615      TestRunner._makeIframe("about:blank", 0);
    616    }
    617 
    618    var passCount = parseInt($("pass-count").innerHTML, 10);
    619    var failCount = parseInt($("fail-count").innerHTML, 10);
    620    var todoCount = parseInt($("todo-count").innerHTML, 10);
    621 
    622    if (passCount === 0 && failCount === 0 && todoCount === 0) {
    623      // No |$('testframe').contentWindow|, so manually update: ...
    624      // ... the log,
    625      TestRunner.structuredLogger.error(
    626        "TEST-UNEXPECTED-FAIL | SimpleTest/TestRunner.js | No checks actually run"
    627      );
    628      // ... the count,
    629      $("fail-count").innerHTML = 1;
    630      // ... the indicator.
    631      var indicator = $("indicator");
    632      indicator.innerHTML = "Status: Fail (No checks actually run)";
    633      indicator.style.backgroundColor = "red";
    634    }
    635 
    636    let e10sMode = SpecialPowers.isMainProcess() ? "non-e10s" : "e10s";
    637 
    638    TestRunner.structuredLogger.info("TEST-START | Shutdown");
    639    TestRunner.structuredLogger.info("Passed:  " + passCount);
    640    TestRunner.structuredLogger.info("Failed:  " + failCount);
    641    TestRunner.structuredLogger.info("Todo:    " + todoCount);
    642    TestRunner.structuredLogger.info("Mode:    " + e10sMode);
    643    TestRunner.structuredLogger.info(
    644      "Slowest: " +
    645        TestRunner.slowestTestTime +
    646        "ms - " +
    647        TestRunner.slowestTestURL
    648    );
    649 
    650    // If we are looping, don't send this cause it closes the log file,
    651    // also don't unregister the crash observers until we're done.
    652    if (TestRunner.repeat === 0) {
    653      SpecialPowers.unregisterProcessCrashObservers();
    654      TestRunner.structuredLogger.info("SimpleTest FINISHED");
    655    }
    656 
    657    if (TestRunner.repeat === 0 && TestRunner.onComplete) {
    658      TestRunner.onComplete();
    659    }
    660 
    661    if (
    662      TestRunner._currentLoop <= TestRunner.repeat &&
    663      !TestRunner._haltTests
    664    ) {
    665      TestRunner._currentLoop++;
    666      TestRunner.resetTests(TestRunner._urls);
    667      TestRunner._loopIsRestarting = true;
    668    } else {
    669      // Loops are finished
    670      if (TestRunner.logEnabled) {
    671        TestRunner.structuredLogger.info(
    672          "TEST-INFO | Ran " + TestRunner._currentLoop + " Loops"
    673        );
    674        TestRunner.structuredLogger.info("SimpleTest FINISHED");
    675      }
    676 
    677      if (TestRunner.onComplete) {
    678        TestRunner.onComplete();
    679      }
    680    }
    681    TestRunner.generateFailureList();
    682 
    683    if (TestRunner.jscovDirPrefix != "") {
    684      coverageCollector.finalize();
    685    }
    686  }
    687 }
    688 TestRunner.runNextTest = _runNextTest;
    689 
    690 TestRunner.expectChildProcessCrash = function () {
    691  TestRunner._expectingProcessCrash = true;
    692 };
    693 
    694 /**
    695 * This stub is called by SimpleTest when a test is finished.
    696 */
    697 TestRunner.testFinished = function (tests) {
    698  // Need to track subtests recorded here separately or else they'll
    699  // trigger the `result after SimpleTest.finish()` error.
    700  var extraTests = [];
    701  var result = "OK";
    702 
    703  // Prevent a test from calling finish() multiple times before we
    704  // have a chance to unload it.
    705  if (
    706    TestRunner._currentTest == TestRunner._lastTestFinished &&
    707    !TestRunner._loopIsRestarting
    708  ) {
    709    TestRunner.structuredLogger.testEnd(
    710      TestRunner.currentTestURL,
    711      "ERROR",
    712      "OK",
    713      "called finish() multiple times"
    714    );
    715    TestRunner.updateUI([{ result: false }]);
    716    return;
    717  }
    718 
    719  if (TestRunner.jscovDirPrefix != "") {
    720    coverageCollector.recordTestCoverage(TestRunner.currentTestURL);
    721  }
    722 
    723  SpecialPowers.requestDumpCoverageCounters().then(() => {
    724    TestRunner._lastTestFinished = TestRunner._currentTest;
    725    TestRunner._loopIsRestarting = false;
    726 
    727    // TODO : replace this by a function that returns the mem data as an object
    728    // that's dumped later with the test_end message
    729    MemoryStats.dump(
    730      TestRunner._currentTest,
    731      TestRunner.currentTestURL,
    732      TestRunner.dumpOutputDirectory,
    733      TestRunner.dumpAboutMemoryAfterTest,
    734      TestRunner.dumpDMDAfterTest
    735    );
    736 
    737    async function cleanUpCrashDumpFiles() {
    738      if (
    739        !(await SpecialPowers.removeExpectedCrashDumpFiles(
    740          TestRunner._expectingProcessCrash
    741        ))
    742      ) {
    743        let subtest = "expected-crash-dump-missing";
    744        TestRunner.structuredLogger.testStatus(
    745          TestRunner.currentTestURL,
    746          subtest,
    747          "ERROR",
    748          "PASS",
    749          "This test did not leave any crash dumps behind, but we were expecting some!"
    750        );
    751        extraTests.push({ name: subtest, result: false });
    752        result = "ERROR";
    753      }
    754 
    755      var unexpectedCrashDumpFiles = SpecialPowers.unwrap(
    756        await SpecialPowers.findUnexpectedCrashDumpFiles()
    757      );
    758      TestRunner._expectingProcessCrash = false;
    759      if (unexpectedCrashDumpFiles.length) {
    760        let subtest = "unexpected-crash-dump-found";
    761        TestRunner.structuredLogger.testStatus(
    762          TestRunner.currentTestURL,
    763          subtest,
    764          "ERROR",
    765          "PASS",
    766          "This test left crash dumps behind, but we " +
    767            "weren't expecting it to!",
    768          null,
    769          { unexpected_crashdump_files: unexpectedCrashDumpFiles }
    770        );
    771        extraTests.push({ name: subtest, result: false });
    772        result = "CRASH";
    773        unexpectedCrashDumpFiles.sort().forEach(function (aFilename) {
    774          TestRunner.structuredLogger.info(
    775            "Found unexpected crash dump file " + aFilename + "."
    776          );
    777        });
    778      }
    779 
    780      if (TestRunner.cleanupCrashes) {
    781        if (await SpecialPowers.removePendingCrashDumpFiles()) {
    782          TestRunner.structuredLogger.info(
    783            "This test left pending crash dumps"
    784          );
    785        }
    786      }
    787    }
    788 
    789    function runNextTest() {
    790      if (TestRunner.currentTestURL != TestRunner.getLoadedTestURL()) {
    791        TestRunner.structuredLogger.testStatus(
    792          TestRunner.currentTestURL,
    793          TestRunner.getLoadedTestURL(),
    794          "FAIL",
    795          "PASS",
    796          "finished in a non-clean fashion, probably" +
    797            " because it didn't call SimpleTest.finish()",
    798          { loaded_test_url: TestRunner.getLoadedTestURL() }
    799        );
    800        extraTests.push({ name: "clean-finish", result: false });
    801        result = result != "CRASH" ? "ERROR" : result;
    802      }
    803 
    804      SpecialPowers.addProfilerMarker(
    805        "TestRunner",
    806        { category: "Test", startTime: TestRunner._currentTestStartTimestamp },
    807        TestRunner.currentTestURL
    808      );
    809      var runtime = new Date().valueOf() - TestRunner._currentTestStartTime;
    810 
    811      if (
    812        TestRunner.slowestTestTime < runtime &&
    813        TestRunner._timeoutFactor >= 1
    814      ) {
    815        TestRunner.slowestTestTime = runtime;
    816        TestRunner.slowestTestURL = TestRunner.currentTestURL;
    817      }
    818 
    819      TestRunner.updateUI(tests.concat(extraTests));
    820 
    821      // Don't show the interstitial if we just run one test with no repeats:
    822      if (TestRunner._urls.length == 1 && TestRunner.repeat <= 1) {
    823        TestRunner.testUnloaded(result, runtime);
    824        return;
    825      }
    826 
    827      var interstitialURL;
    828      if (
    829        !testInXOriginFrame() &&
    830        $("testframe").contentWindow.location.protocol == "chrome:"
    831      ) {
    832        interstitialURL =
    833          "tests/SimpleTest/iframe-between-tests.html?result=" +
    834          result +
    835          "&runtime=" +
    836          runtime;
    837      } else {
    838        interstitialURL =
    839          "/tests/SimpleTest/iframe-between-tests.html?result=" +
    840          result +
    841          "&runtime=" +
    842          runtime;
    843      }
    844      // check if there were test run after SimpleTest.finish, which should never happen
    845      if (!testInXOriginFrame()) {
    846        $("testframe").contentWindow.addEventListener("unload", function () {
    847          var testwin = $("testframe").contentWindow;
    848          if (testwin.SimpleTest) {
    849            if (typeof testwin.SimpleTest.testsLength === "undefined") {
    850              TestRunner.structuredLogger.error(
    851                "TEST-UNEXPECTED-FAIL | " +
    852                  TestRunner.currentTestURL +
    853                  " fired an unload callback with missing test data," +
    854                  " possibly due to the test navigating or reloading"
    855              );
    856              TestRunner.updateUI([{ result: false }]);
    857            } else if (
    858              testwin.SimpleTest._tests.length != testwin.SimpleTest.testsLength
    859            ) {
    860              var didReportError = false;
    861              var wrongtestlength =
    862                testwin.SimpleTest._tests.length -
    863                testwin.SimpleTest.testsLength;
    864              var wrongtestname = "";
    865              for (var i = 0; i < wrongtestlength; i++) {
    866                wrongtestname =
    867                  testwin.SimpleTest._tests[testwin.SimpleTest.testsLength + i]
    868                    .name;
    869                TestRunner.structuredLogger.error(
    870                  "TEST-UNEXPECTED-FAIL | " +
    871                    TestRunner.currentTestURL +
    872                    " logged result after SimpleTest.finish(): " +
    873                    wrongtestname
    874                );
    875                didReportError = true;
    876              }
    877              if (!didReportError) {
    878                // This clause shouldn't be reachable, but if we somehow get
    879                // here (e.g. if wrongtestlength is somehow negative), it's
    880                // important that we log *something* for the { result: false }
    881                // test-failure that we're about to post.
    882                TestRunner.structuredLogger.error(
    883                  "TEST-UNEXPECTED-FAIL | " +
    884                    TestRunner.currentTestURL +
    885                    " hit an unexpected condition when checking for" +
    886                    " logged results after SimpleTest.finish()"
    887                );
    888              }
    889              TestRunner.updateUI([{ result: false }]);
    890            }
    891          }
    892        });
    893      }
    894      TestRunner._makeIframe(interstitialURL, 0);
    895    }
    896 
    897    SpecialPowers.executeAfterFlushingMessageQueue(async function () {
    898      await SpecialPowers.waitForCrashes(TestRunner._expectingProcessCrash);
    899      await cleanUpCrashDumpFiles();
    900      await SpecialPowers.flushPermissions();
    901      await SpecialPowers.flushPrefEnv();
    902      SpecialPowers.cleanupAllClipboard(window);
    903      runNextTest();
    904    });
    905  });
    906 };
    907 
    908 /**
    909 * This stub is called by XOrigin Tests to report assertion count.
    910 */
    911 TestRunner._xoriginAssertionCount = 0;
    912 TestRunner.addAssertionCount = function (count) {
    913  if (!testInXOriginFrame()) {
    914    TestRunner.error(
    915      `addAssertionCount should only be called by a cross origin test`
    916    );
    917    return;
    918  }
    919 
    920  if (testInDifferentProcess()) {
    921    TestRunner._xoriginAssertionCount += count;
    922  }
    923 };
    924 
    925 TestRunner.testUnloaded = function (result, runtime) {
    926  // If we're in a debug build, check assertion counts.  This code is
    927  // similar to the code in Tester_nextTest in browser-test.js used
    928  // for browser-chrome mochitests.
    929  if (SpecialPowers.isDebugBuild) {
    930    var newAssertionCount =
    931      SpecialPowers.assertionCount() + TestRunner._xoriginAssertionCount;
    932    var numAsserts = newAssertionCount - TestRunner._lastAssertionCount;
    933    TestRunner._lastAssertionCount = newAssertionCount;
    934 
    935    var max = TestRunner._expectedMaxAsserts;
    936    var min = TestRunner._expectedMinAsserts;
    937    if (Array.isArray(TestRunner.expected)) {
    938      // Accumulate all assertion counts recorded in the failure pattern file.
    939      let additionalAsserts = TestRunner.expected.reduce(
    940        (acc, [pat, count]) => {
    941          return pat == "ASSERTION" ? acc + count : acc;
    942        },
    943        0
    944      );
    945      min += additionalAsserts;
    946      max += additionalAsserts;
    947    }
    948 
    949    TestRunner.structuredLogger.assertionCount(
    950      TestRunner.currentTestURL,
    951      numAsserts,
    952      min,
    953      max
    954    );
    955 
    956    if (numAsserts < min || numAsserts > max) {
    957      result = "ERROR";
    958 
    959      var direction = "more";
    960      var target = max;
    961      if (numAsserts < min) {
    962        direction = "less";
    963        target = min;
    964      }
    965      TestRunner.structuredLogger.testStatus(
    966        TestRunner.currentTestURL,
    967        "Assertion Count",
    968        "ERROR",
    969        "PASS",
    970        numAsserts +
    971          " is " +
    972          direction +
    973          " than expected " +
    974          target +
    975          " assertions"
    976      );
    977 
    978      // reset result so we don't print a second error on test-end
    979      result = "OK";
    980    }
    981  }
    982 
    983  TestRunner.structuredLogger.testEnd(
    984    TestRunner.currentTestURL,
    985    result,
    986    "OK",
    987    "Finished in " + runtime + "ms",
    988    { runtime }
    989  );
    990 
    991  // Always do this, so we can "reset" preferences between tests.
    992  // Note: this is for mochitest-plain only; browser tests do not
    993  // unconditionally reset between tests, see
    994  // checkPreferencesAfterTest in testing/mochitest/browser-test.js
    995  SpecialPowers.comparePrefsToBaseline(
    996    TestRunner.ignorePrefs,
    997    TestRunner.verifyPrefsNextTest
    998  );
    999 };
   1000 
   1001 TestRunner.verifyPrefsNextTest = function (p) {
   1002  if (TestRunner.comparePrefs) {
   1003    let prefs = Array.from(SpecialPowers.Cu.waiveXrays(p), x =>
   1004      SpecialPowers.unwrapIfWrapped(SpecialPowers.Cu.unwaiveXrays(x))
   1005    );
   1006    prefs.forEach(pr =>
   1007      TestRunner.structuredLogger.error(
   1008        "TEST-UNEXPECTED-FAIL | " +
   1009          TestRunner.currentTestURL +
   1010          " | changed preference: " +
   1011          pr
   1012      )
   1013    );
   1014  }
   1015  TestRunner.doNextTest();
   1016 };
   1017 
   1018 TestRunner.doNextTest = function () {
   1019  TestRunner._currentTest++;
   1020  if (TestRunner.runSlower) {
   1021    setTimeout(TestRunner.runNextTest, 1000);
   1022  } else {
   1023    TestRunner.runNextTest();
   1024  }
   1025 };
   1026 
   1027 /**
   1028 * Get the results.
   1029 */
   1030 TestRunner.countResults = function (tests) {
   1031  var nOK = 0;
   1032  var nNotOK = 0;
   1033  var nTodo = 0;
   1034  for (var i = 0; i < tests.length; ++i) {
   1035    var test = tests[i];
   1036    if (test.todo && !test.result) {
   1037      nTodo++;
   1038    } else if (test.result && !test.todo) {
   1039      nOK++;
   1040    } else {
   1041      nNotOK++;
   1042    }
   1043  }
   1044  return { OK: nOK, notOK: nNotOK, todo: nTodo };
   1045 };
   1046 
   1047 /**
   1048 * Print out table of any error messages found during looped run
   1049 */
   1050 TestRunner.displayLoopErrors = function (tableName, tests) {
   1051  if (TestRunner.countResults(tests).notOK > 0) {
   1052    var table = $(tableName);
   1053    var curtest;
   1054    if (!table.rows.length) {
   1055      //if table headers are not yet generated, make them
   1056      var row = table.insertRow(table.rows.length);
   1057      var cell = row.insertCell(0);
   1058      var textNode = document.createTextNode("Test File Name:");
   1059      cell.appendChild(textNode);
   1060      cell = row.insertCell(1);
   1061      textNode = document.createTextNode("Test:");
   1062      cell.appendChild(textNode);
   1063      cell = row.insertCell(2);
   1064      textNode = document.createTextNode("Error message:");
   1065      cell.appendChild(textNode);
   1066    }
   1067 
   1068    //find the broken test
   1069    for (var testnum in tests) {
   1070      curtest = tests[testnum];
   1071      if (
   1072        !(
   1073          (curtest.todo && !curtest.result) ||
   1074          (curtest.result && !curtest.todo)
   1075        )
   1076      ) {
   1077        //this is a failed test or the result of todo test. Display the related message
   1078        row = table.insertRow(table.rows.length);
   1079        cell = row.insertCell(0);
   1080        textNode = document.createTextNode(TestRunner.currentTestURL);
   1081        cell.appendChild(textNode);
   1082        cell = row.insertCell(1);
   1083        textNode = document.createTextNode(curtest.name);
   1084        cell.appendChild(textNode);
   1085        cell = row.insertCell(2);
   1086        textNode = document.createTextNode(curtest.diag ? curtest.diag : "");
   1087        cell.appendChild(textNode);
   1088      }
   1089    }
   1090  }
   1091 };
   1092 
   1093 TestRunner.updateUI = function (tests) {
   1094  var results = TestRunner.countResults(tests);
   1095  var passCount = parseInt($("pass-count").innerHTML) + results.OK;
   1096  var failCount = parseInt($("fail-count").innerHTML) + results.notOK;
   1097  var todoCount = parseInt($("todo-count").innerHTML) + results.todo;
   1098  $("pass-count").innerHTML = passCount;
   1099  $("fail-count").innerHTML = failCount;
   1100  $("todo-count").innerHTML = todoCount;
   1101 
   1102  // Set the top Green/Red bar
   1103  var indicator = $("indicator");
   1104  if (failCount > 0) {
   1105    indicator.innerHTML = "Status: Fail";
   1106    indicator.style.backgroundColor = "red";
   1107  } else if (passCount > 0) {
   1108    indicator.innerHTML = "Status: Pass";
   1109    indicator.style.backgroundColor = "#0d0";
   1110  } else {
   1111    indicator.innerHTML = "Status: ToDo";
   1112    indicator.style.backgroundColor = "orange";
   1113  }
   1114 
   1115  // Set the table values
   1116  var trID = "tr-" + $("current-test-path").innerHTML;
   1117  var row = $(trID);
   1118 
   1119  // Only update the row if it actually exists (autoUI)
   1120  if (row != null) {
   1121    var tds = row.getElementsByTagName("td");
   1122    tds[0].style.backgroundColor = "#0d0";
   1123    tds[0].innerHTML = parseInt(tds[0].innerHTML) + parseInt(results.OK);
   1124    tds[1].style.backgroundColor = results.notOK > 0 ? "red" : "#0d0";
   1125    tds[1].innerHTML = parseInt(tds[1].innerHTML) + parseInt(results.notOK);
   1126    tds[2].style.backgroundColor = results.todo > 0 ? "orange" : "#0d0";
   1127    tds[2].innerHTML = parseInt(tds[2].innerHTML) + parseInt(results.todo);
   1128  }
   1129 
   1130  //if we ran in a loop, display any found errors
   1131  if (TestRunner.repeat > 0) {
   1132    TestRunner.displayLoopErrors("fail-table", tests);
   1133  }
   1134 };
   1135 
   1136 // XOrigin Tests
   1137 // If "--enable-xorigin-tests" is set, mochitests are run in a cross origin iframe.
   1138 // The parent process will run at http://mochi.xorigin-test:8888", and individual
   1139 // mochitests will be launched in a cross-origin iframe at http://mochi.test:8888.
   1140 
   1141 var xOriginDispatchMap = {
   1142  runner: TestRunner,
   1143  logger: TestRunner.structuredLogger,
   1144  addFailedTest: TestRunner.addFailedTest,
   1145  expectAssertions: TestRunner.expectAssertions,
   1146  expectChildProcessCrash: TestRunner.expectChildProcessCrash,
   1147  requestLongerTimeout: TestRunner.requestLongerTimeout,
   1148  "structuredLogger.deactivateBuffering":
   1149    TestRunner.structuredLogger.deactivateBuffering,
   1150  "structuredLogger.activateBuffering":
   1151    TestRunner.structuredLogger.activateBuffering,
   1152  "structuredLogger.testStatus": TestRunner.structuredLogger.testStatus,
   1153  "structuredLogger.info": TestRunner.structuredLogger.info,
   1154  "structuredLogger.warning": TestRunner.structuredLogger.warning,
   1155  "structuredLogger.error": TestRunner.structuredLogger.error,
   1156  testFinished: TestRunner.testFinished,
   1157  addAssertionCount: TestRunner.addAssertionCount,
   1158 };
   1159 
   1160 function xOriginTestRunnerHandler(event) {
   1161  if (event.data.harnessType != "SimpleTest") {
   1162    return;
   1163  }
   1164  // Handles messages from xOriginRunner in SimpleTest.js.
   1165  if (event.data.command in xOriginDispatchMap) {
   1166    xOriginDispatchMap[event.data.command].apply(
   1167      xOriginDispatchMap[event.data.applyOn],
   1168      event.data.params
   1169    );
   1170  } else {
   1171    TestRunner.error(`Command ${event.data.command} not found
   1172      in xOriginDispatchMap`);
   1173  }
   1174 }
   1175 
   1176 TestRunner.setXOriginEventHandler = function () {
   1177  window.addEventListener("message", xOriginTestRunnerHandler);
   1178 };