tor-browser

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

reftest.sys.mjs (67881B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs";
      6 
      7 import { globals } from "resource://reftest/globals.sys.mjs";
      8 
      9 import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
     10 
     11 const {
     12  XHTML_NS,
     13  XUL_NS,
     14 
     15  DEBUG_CONTRACTID,
     16 
     17  TYPE_REFTEST_EQUAL,
     18  TYPE_REFTEST_NOTEQUAL,
     19  TYPE_LOAD,
     20  TYPE_SCRIPT,
     21  TYPE_PRINT,
     22 
     23  URL_TARGET_TYPE_TEST,
     24  URL_TARGET_TYPE_REFERENCE,
     25 
     26  EXPECTED_PASS,
     27  EXPECTED_FAIL,
     28  EXPECTED_RANDOM,
     29  EXPECTED_FUZZY,
     30 
     31  PREF_BOOLEAN,
     32  PREF_STRING,
     33  PREF_INTEGER,
     34 
     35  FOCUS_FILTER_NON_NEEDS_FOCUS_TESTS,
     36 
     37  g,
     38 } = globals;
     39 
     40 import { HttpServer } from "resource://reftest/httpd.sys.mjs";
     41 
     42 import {
     43  ReadTopManifest,
     44  CreateUrls,
     45 } from "resource://reftest/manifest.sys.mjs";
     46 import { StructuredLogger } from "resource://reftest/StructuredLog.sys.mjs";
     47 import { PerTestCoverageUtils } from "resource://reftest/PerTestCoverageUtils.sys.mjs";
     48 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     49 import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs";
     50 
     51 const lazy = {};
     52 
     53 XPCOMUtils.defineLazyServiceGetters(lazy, {
     54  proxyService: [
     55    "@mozilla.org/network/protocol-proxy-service;1",
     56    Ci.nsIProtocolProxyService,
     57  ],
     58 });
     59 
     60 function HasUnexpectedResult() {
     61  return (
     62    g.testResults.Exception > 0 ||
     63    g.testResults.FailedLoad > 0 ||
     64    g.testResults.UnexpectedFail > 0 ||
     65    g.testResults.UnexpectedPass > 0 ||
     66    g.testResults.AssertionUnexpected > 0 ||
     67    g.testResults.AssertionUnexpectedFixed > 0
     68  );
     69 }
     70 
     71 // By default we just log to stdout
     72 var gDumpFn = function (line) {
     73  dump(line);
     74  if (g.logFile) {
     75    g.logFile.writeString(line);
     76  }
     77 };
     78 var gDumpRawLog = function (record) {
     79  // Dump JSON representation of data on a single line
     80  var line = "\n" + JSON.stringify(record) + "\n";
     81  dump(line);
     82 
     83  if (g.logFile) {
     84    g.logFile.writeString(line);
     85  }
     86 };
     87 g.logger = new StructuredLogger("reftest", gDumpRawLog);
     88 var logger = g.logger;
     89 
     90 function TestBuffer(str) {
     91  logger.debug(str);
     92  g.testLog.push(str);
     93 }
     94 
     95 function isAndroidDevice() {
     96  // This is the best we can do for now; maybe in the future we'll have
     97  // more correct detection of this case.
     98  return Services.appinfo.OS == "Android" && g.browserIsRemote;
     99 }
    100 
    101 function FlushTestBuffer() {
    102  // In debug mode, we've dumped all these messages already.
    103  if (g.logLevel !== "debug") {
    104    for (var i = 0; i < g.testLog.length; ++i) {
    105      logger.info("Saved log: " + g.testLog[i]);
    106    }
    107  }
    108  g.testLog = [];
    109 }
    110 
    111 function LogWidgetLayersFailure() {
    112  logger.error(
    113    "Screen resolution is too low - USE_WIDGET_LAYERS was disabled. " +
    114      (g.browserIsRemote
    115        ? "Since E10s is enabled, there is no fallback rendering path!"
    116        : "The fallback rendering path is not reliably consistent with on-screen rendering.")
    117  );
    118 
    119  logger.error(
    120    "If you cannot increase your screen resolution you can try reducing " +
    121      "gecko's pixel scaling by adding something like '--setpref " +
    122      "layout.css.devPixelsPerPx=1.0' to your './mach reftest' command " +
    123      "(possibly as an alias in ~/.mozbuild/machrc). Note that this is " +
    124      "inconsistent with CI testing, and may interfere with HighDPI/" +
    125      "reftest-zoom tests."
    126  );
    127 }
    128 
    129 function AllocateCanvas() {
    130  if (g.recycledCanvases.length) {
    131    return g.recycledCanvases.shift();
    132  }
    133 
    134  var canvas = g.containingWindow.document.createElementNS(XHTML_NS, "canvas");
    135  var r = g.browser.getBoundingClientRect();
    136  canvas.setAttribute("width", Math.ceil(r.width));
    137  canvas.setAttribute("height", Math.ceil(r.height));
    138 
    139  return canvas;
    140 }
    141 
    142 function ReleaseCanvas(canvas) {
    143  // store a maximum of 2 canvases, if we're not caching
    144  if (!g.noCanvasCache || g.recycledCanvases.length < 2) {
    145    g.recycledCanvases.push(canvas);
    146  }
    147 }
    148 
    149 export function OnRefTestLoad(win) {
    150  g.crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
    151  g.crashDumpDir.append("minidumps");
    152 
    153  g.pendingCrashDumpDir = Services.dirsvc.get("UAppData", Ci.nsIFile);
    154  g.pendingCrashDumpDir.append("Crash Reports");
    155  g.pendingCrashDumpDir.append("pending");
    156 
    157  g.browserIsRemote = Services.appinfo.browserTabsRemoteAutostart;
    158  g.browserIsFission = Services.appinfo.fissionAutostart;
    159 
    160  g.browserIsIframe = Services.prefs.getBoolPref(
    161    "reftest.browser.iframe.enabled",
    162    false
    163  );
    164  g.useDrawSnapshot = Services.prefs.getBoolPref(
    165    "reftest.use-draw-snapshot",
    166    false
    167  );
    168 
    169  g.logLevel = Services.prefs.getStringPref("reftest.logLevel", "info");
    170 
    171  if (g.containingWindow == null && win != null) {
    172    g.containingWindow = win;
    173  }
    174 
    175  if (g.browserIsIframe) {
    176    g.browser = g.containingWindow.document.createElementNS(XHTML_NS, "iframe");
    177    g.browser.setAttribute("mozbrowser", "");
    178  } else {
    179    g.browser = g.containingWindow.document.createElementNS(
    180      XUL_NS,
    181      "xul:browser"
    182    );
    183  }
    184  g.browser.setAttribute("id", "browser");
    185  g.browser.setAttribute("type", "content");
    186  g.browser.setAttribute("primary", "true");
    187  // FIXME: This ideally shouldn't be needed, but on android and windows
    188  // sometimes the window is occluded / hidden, which causes some crashtests
    189  // to time out. Bug 1864255 might be able to help here.
    190  g.browser.setAttribute("manualactiveness", "true");
    191  g.browser.setAttribute("remote", g.browserIsRemote ? "true" : "false");
    192  // Make sure the browser element is exactly 800x1000, no matter
    193  // what size our window is
    194  g.browser.style.setProperty("padding", "0px");
    195  g.browser.style.setProperty("margin", "0px");
    196  g.browser.style.setProperty("border", "none");
    197  g.browser.style.setProperty("min-width", "800px");
    198  g.browser.style.setProperty("min-height", "1000px");
    199  g.browser.style.setProperty("max-width", "800px");
    200  g.browser.style.setProperty("max-height", "1000px");
    201  g.browser.style.setProperty(
    202    "color-scheme",
    203    "env(-moz-content-preferred-color-scheme)"
    204  );
    205 
    206  if (Services.appinfo.OS == "Android") {
    207    let doc = g.containingWindow.document.getElementById("main-window");
    208    while (doc.hasChildNodes()) {
    209      doc.firstChild.remove();
    210    }
    211    doc.appendChild(g.browser);
    212    // TODO Bug 1156817: reftests don't have most of GeckoView infra so we
    213    // can't register this actor
    214    ChromeUtils.unregisterWindowActor("LoadURIDelegate");
    215  } else {
    216    win.document.getElementById("reftest-window").appendChild(g.browser);
    217  }
    218 
    219  g.browserMessageManager = g.browser.frameLoader.messageManager;
    220  // See the comment above about manualactiveness.
    221  g.browser.docShellIsActive = true;
    222  // The content script waits for the initial onload, then notifies
    223  // us.
    224  RegisterMessageListenersAndLoadContentScript(false);
    225 }
    226 
    227 function InitAndStartRefTests() {
    228  try {
    229    Services.prefs.setBoolPref("android.widget_paints_background", false);
    230  } catch (e) {}
    231 
    232  // If fission is enabled, then also put data: URIs in the default web process,
    233  // since most reftests run in the file process, and this will make data:
    234  // <iframe>s OOP.
    235  if (g.browserIsFission) {
    236    Services.prefs.setBoolPref(
    237      "browser.tabs.remote.dataUriInDefaultWebProcess",
    238      true
    239    );
    240  }
    241 
    242  /* set the g.loadTimeout */
    243  g.loadTimeout = Services.prefs.getIntPref("reftest.timeout", 5 * 60 * 1000);
    244 
    245  /* Get the logfile for android tests */
    246  try {
    247    var logFile = Services.prefs.getStringPref("reftest.logFile");
    248    if (logFile) {
    249      var f = FileUtils.File(logFile);
    250      var out = FileUtils.openFileOutputStream(
    251        f,
    252        FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE
    253      );
    254      g.logFile = Cc[
    255        "@mozilla.org/intl/converter-output-stream;1"
    256      ].createInstance(Ci.nsIConverterOutputStream);
    257      g.logFile.init(out, null);
    258    }
    259  } catch (e) {}
    260 
    261  g.remote = Services.prefs.getBoolPref("reftest.remote", false);
    262 
    263  g.ignoreWindowSize = Services.prefs.getBoolPref(
    264    "reftest.ignoreWindowSize",
    265    false
    266  );
    267 
    268  /* Support for running a chunk (subset) of tests.  In separate try as this is optional */
    269  try {
    270    g.totalChunks = Services.prefs.getIntPref("reftest.totalChunks");
    271    g.thisChunk = Services.prefs.getIntPref("reftest.thisChunk");
    272  } catch (e) {
    273    g.totalChunks = 0;
    274    g.thisChunk = 0;
    275  }
    276 
    277  g.focusFilterMode = Services.prefs.getStringPref(
    278    "reftest.focusFilterMode",
    279    ""
    280  );
    281 
    282  g.isCoverageBuild = Services.prefs.getBoolPref(
    283    "reftest.isCoverageBuild",
    284    false
    285  );
    286 
    287  g.compareRetainedDisplayLists = Services.prefs.getBoolPref(
    288    "reftest.compareRetainedDisplayLists",
    289    false
    290  );
    291 
    292  try {
    293    // We have to set print.always_print_silent or a print dialog would
    294    // appear for each print operation, which would interrupt the test run.
    295    Services.prefs.setBoolPref("print.always_print_silent", true);
    296  } catch (e) {
    297    /* uh oh, print reftests may not work... */
    298    logger.warning("Failed to set silent printing pref, EXCEPTION: " + e);
    299  }
    300 
    301  g.windowUtils = g.containingWindow.windowUtils;
    302  if (!g.windowUtils || !g.windowUtils.compareCanvases) {
    303    throw new Error("nsIDOMWindowUtils inteface missing");
    304  }
    305 
    306  g.ioService = Services.io;
    307  g.debug = Cc[DEBUG_CONTRACTID].getService(Ci.nsIDebug2);
    308 
    309  RegisterProcessCrashObservers();
    310 
    311  if (g.remote) {
    312    g.server = null;
    313  } else {
    314    g.server = new HttpServer();
    315  }
    316  try {
    317    if (g.server) {
    318      StartHTTPServer();
    319    }
    320  } catch (ex) {
    321    //g.browser.loadURI('data:text/plain,' + ex);
    322    ++g.testResults.Exception;
    323    logger.error("EXCEPTION: " + ex);
    324    DoneTests();
    325  }
    326 
    327  // Focus the content browser.
    328  if (g.focusFilterMode != FOCUS_FILTER_NON_NEEDS_FOCUS_TESTS) {
    329    if (Services.focus.activeWindow != g.containingWindow) {
    330      Focus();
    331    }
    332    g.browser.addEventListener("focus", ReadTests, true);
    333    g.browser.focus();
    334  } else {
    335    ReadTests();
    336  }
    337 }
    338 
    339 function StartHTTPServer() {
    340  g.server.registerContentType("sjs", "sjs");
    341  g.server.start(-1);
    342 
    343  g.server.identity.add("http", "example.org", "80");
    344  g.server.identity.add("https", "example.org", "443");
    345 
    346  const proxyFilter = {
    347    proxyInfo: lazy.proxyService.newProxyInfo(
    348      "http", // type of proxy
    349      "localhost", //proxy host
    350      g.server.identity.primaryPort, // proxy host port
    351      "", // auth header
    352      "", // isolation key
    353      0, // flags
    354      4096, // timeout
    355      null // failover proxy
    356    ),
    357 
    358    applyFilter(channel, defaultProxyInfo, callback) {
    359      if (channel.URI.host == "example.org") {
    360        callback.onProxyFilterResult(this.proxyInfo);
    361      } else {
    362        callback.onProxyFilterResult(defaultProxyInfo);
    363      }
    364    },
    365  };
    366 
    367  lazy.proxyService.registerChannelFilter(proxyFilter, 0);
    368 
    369  g.httpServerPort = g.server.identity.primaryPort;
    370 }
    371 
    372 // Perform a Fisher-Yates shuffle of the array.
    373 function Shuffle(array) {
    374  for (var i = array.length - 1; i > 0; i--) {
    375    var j = Math.floor(Math.random() * (i + 1));
    376    var temp = array[i];
    377    array[i] = array[j];
    378    array[j] = temp;
    379  }
    380 }
    381 
    382 function ReadTests() {
    383  try {
    384    if (g.focusFilterMode != FOCUS_FILTER_NON_NEEDS_FOCUS_TESTS) {
    385      g.browser.removeEventListener("focus", ReadTests, true);
    386    }
    387 
    388    g.urls = [];
    389 
    390    /* There are three modes implemented here:
    391     * 1) reftest.manifests
    392     * 2) reftest.manifests and reftest.manifests.dumpTests
    393     * 3) reftest.tests
    394     *
    395     * The first will parse the specified manifests, then immediately
    396     * run the tests. The second will parse the manifests, save the test
    397     * objects to a file and exit. The third will load a file of test
    398     * objects and run them.
    399     *
    400     * The latter two modes are used to pass test data back and forth
    401     * with python harness.
    402     */
    403    let manifests = Services.prefs.getStringPref("reftest.manifests", null);
    404    let dumpTests = Services.prefs.getStringPref(
    405      "reftest.manifests.dumpTests",
    406      null
    407    );
    408    let testList = Services.prefs.getStringPref("reftest.tests", null);
    409 
    410    if ((testList && manifests) || !(testList || manifests)) {
    411      logger.error(
    412        "Exactly one of reftest.manifests or reftest.tests must be specified."
    413      );
    414      logger.debug("reftest.manifests is: " + manifests);
    415      logger.error("reftest.tests is: " + testList);
    416      DoneTests();
    417    }
    418 
    419    if (testList) {
    420      logger.debug("Reading test objects from: " + testList);
    421      IOUtils.readJSON(testList)
    422        .then(function onSuccess(json) {
    423          g.urls = json.map(CreateUrls);
    424          StartTests();
    425        })
    426        .catch(function onFailure(e) {
    427          logger.error("Failed to load test objects: " + e);
    428          DoneTests();
    429        });
    430    } else if (manifests) {
    431      // Parse reftest manifests
    432      logger.debug("Reading " + manifests.length + " manifests");
    433      manifests = JSON.parse(manifests);
    434      g.urlsFilterRegex = manifests.null;
    435 
    436      var globalFilter = null;
    437      if (manifests.hasOwnProperty("")) {
    438        let filterAndId = manifests[""];
    439        if (!Array.isArray(filterAndId)) {
    440          logger.error(`manifest[""] should be an array`);
    441          DoneTests();
    442        }
    443        if (filterAndId.length === 0) {
    444          logger.error(
    445            `manifest[""] should contain a filter pattern in the 1st item`
    446          );
    447          DoneTests();
    448        }
    449        let filter = filterAndId[0];
    450        if (typeof filter !== "string") {
    451          logger.error(`The first item of manifest[""] should be a string`);
    452          DoneTests();
    453        }
    454        globalFilter = new RegExp(filter);
    455        delete manifests[""];
    456      }
    457 
    458      var manifestURLs = Object.keys(manifests);
    459 
    460      // Ensure we read manifests from higher up the directory tree first so that we
    461      // process includes before reading the included manifest again
    462      manifestURLs.sort(function (a, b) {
    463        return a.length - b.length;
    464      });
    465      manifestURLs.forEach(function (manifestURL) {
    466        logger.info("Reading manifest " + manifestURL);
    467        var manifestInfo = manifests[manifestURL];
    468        var filter = manifestInfo[0] ? new RegExp(manifestInfo[0]) : null;
    469        var manifestID = manifestInfo[1];
    470        ReadTopManifest(manifestURL, [globalFilter, filter, false], manifestID);
    471      });
    472 
    473      if (dumpTests) {
    474        logger.debug("Dumping test objects to file: " + dumpTests);
    475        IOUtils.writeJSON(dumpTests, g.urls, { flush: true }).then(
    476          function onSuccess() {
    477            DoneTests();
    478          },
    479          function onFailure(reason) {
    480            logger.error("failed to write test data: " + reason);
    481            DoneTests();
    482          }
    483        );
    484      } else {
    485        logger.debug("Running " + g.urls.length + " test objects");
    486        g.manageSuite = true;
    487        g.urls = g.urls.map(CreateUrls);
    488        StartTests();
    489      }
    490    }
    491  } catch (e) {
    492    ++g.testResults.Exception;
    493    logger.error("EXCEPTION: " + e);
    494    DoneTests();
    495  }
    496 }
    497 
    498 function StartTests() {
    499  g.noCanvasCache = Services.prefs.getIntPref("reftest.nocache", false);
    500 
    501  g.shuffle = Services.prefs.getBoolPref("reftest.shuffle", false);
    502 
    503  g.runUntilFailure = Services.prefs.getBoolPref(
    504    "reftest.runUntilFailure",
    505    false
    506  );
    507 
    508  g.verify = Services.prefs.getBoolPref("reftest.verify", false);
    509 
    510  g.cleanupPendingCrashes = Services.prefs.getBoolPref(
    511    "reftest.cleanupPendingCrashes",
    512    false
    513  );
    514 
    515  // Check if there are any crash dump files from the startup procedure, before
    516  // we start running the first test. Otherwise the first test might get
    517  // blamed for producing a crash dump file when that was not the case.
    518  CleanUpCrashDumpFiles();
    519 
    520  // When we repeat this function is called again, so really only want to set
    521  // g.repeat once.
    522  if (g.repeat == null) {
    523    g.repeat = Services.prefs.getIntPref("reftest.repeat", 0);
    524  }
    525 
    526  g.runSlowTests = Services.prefs.getIntPref("reftest.skipslowtests", false);
    527 
    528  if (g.shuffle) {
    529    g.noCanvasCache = true;
    530  }
    531 
    532  try {
    533    BuildUseCounts();
    534 
    535    // Filter tests which will be skipped to get a more even distribution when chunking
    536    // tURLs is a temporary array containing all active tests
    537    var tURLs = [];
    538    for (var i = 0; i < g.urls.length; ++i) {
    539      if (g.urls[i].skip) {
    540        continue;
    541      }
    542 
    543      if (g.urls[i].needsFocus && !Focus()) {
    544        continue;
    545      }
    546 
    547      if (g.urls[i].slow && !g.runSlowTests) {
    548        continue;
    549      }
    550 
    551      tURLs.push(g.urls[i]);
    552    }
    553 
    554    var numActiveTests = tURLs.length;
    555 
    556    if (g.totalChunks > 0 && g.thisChunk > 0) {
    557      // Calculate start and end indices of this chunk if tURLs array were
    558      // divided evenly
    559      var testsPerChunk = tURLs.length / g.totalChunks;
    560      var start = Math.round((g.thisChunk - 1) * testsPerChunk);
    561      var end = Math.round(g.thisChunk * testsPerChunk);
    562      numActiveTests = end - start;
    563 
    564      // Map these indices onto the g.urls array. This avoids modifying the
    565      // g.urls array which prevents skipped tests from showing up in the log
    566      start = g.thisChunk == 1 ? 0 : g.urls.indexOf(tURLs[start]);
    567      end =
    568        g.thisChunk == g.totalChunks
    569          ? g.urls.length
    570          : g.urls.indexOf(tURLs[end + 1]) - 1;
    571 
    572      logger.info(
    573        "Running chunk " +
    574          g.thisChunk +
    575          " out of " +
    576          g.totalChunks +
    577          " chunks.  " +
    578          "tests " +
    579          (start + 1) +
    580          "-" +
    581          end +
    582          "/" +
    583          g.urls.length
    584      );
    585 
    586      g.urls = g.urls.slice(start, end);
    587    }
    588 
    589    if (g.manageSuite && !g.suiteStarted) {
    590      var ids = {};
    591      g.urls.forEach(function (test) {
    592        if (!(test.manifestID in ids)) {
    593          ids[test.manifestID] = [];
    594        }
    595        ids[test.manifestID].push(test.identifier);
    596      });
    597      var suite = Services.prefs.getStringPref("reftest.suite", "reftest");
    598      logger.suiteStart(ids, suite, {
    599        skipped: g.urls.length - numActiveTests,
    600      });
    601      g.suiteStarted = true;
    602    }
    603 
    604    if (g.shuffle) {
    605      Shuffle(g.urls);
    606    }
    607 
    608    g.totalTests = g.urls.length;
    609    if (!g.totalTests && !g.verify && !g.repeat) {
    610      throw new Error("No tests to run");
    611    }
    612 
    613    g.uriCanvases = {};
    614 
    615    PerTestCoverageUtils.beforeTest()
    616      .then(StartCurrentTest)
    617      .catch(e => {
    618        logger.error("EXCEPTION: " + e);
    619        DoneTests();
    620      });
    621  } catch (ex) {
    622    //g.browser.loadURI('data:text/plain,' + ex);
    623    ++g.testResults.Exception;
    624    logger.error("EXCEPTION: " + ex);
    625    DoneTests();
    626  }
    627 }
    628 
    629 export function OnRefTestUnload() {}
    630 
    631 function AddURIUseCount(uri) {
    632  if (uri == null) {
    633    return;
    634  }
    635 
    636  var spec = uri.spec;
    637  if (spec in g.uriUseCounts) {
    638    g.uriUseCounts[spec]++;
    639  } else {
    640    g.uriUseCounts[spec] = 1;
    641  }
    642 }
    643 
    644 function BuildUseCounts() {
    645  if (g.noCanvasCache) {
    646    return;
    647  }
    648 
    649  g.uriUseCounts = {};
    650  for (var i = 0; i < g.urls.length; ++i) {
    651    var url = g.urls[i];
    652    if (
    653      !url.skip &&
    654      (url.type == TYPE_REFTEST_EQUAL || url.type == TYPE_REFTEST_NOTEQUAL)
    655    ) {
    656      if (!url.prefSettings1.length) {
    657        AddURIUseCount(g.urls[i].url1);
    658      }
    659      if (!url.prefSettings2.length) {
    660        AddURIUseCount(g.urls[i].url2);
    661      }
    662    }
    663  }
    664 }
    665 
    666 // Return true iff this window is focused when this function returns.
    667 function Focus() {
    668  Services.focus.focusedWindow = g.containingWindow;
    669 
    670  try {
    671    var dock = Cc["@mozilla.org/widget/macdocksupport;1"].getService(
    672      Ci.nsIMacDockSupport
    673    );
    674    dock.activateApplication(true);
    675  } catch (ex) {}
    676 
    677  return true;
    678 }
    679 
    680 function Blur() {
    681  // On non-remote reftests, this will transfer focus to the dummy window
    682  // we created to hold focus for non-needs-focus tests.  Buggy tests
    683  // (ones which require focus but don't request needs-focus) will then
    684  // fail.
    685  g.containingWindow.blur();
    686 }
    687 
    688 async function StartCurrentTest() {
    689  g.testLog = [];
    690 
    691  // make sure we don't run tests that are expected to kill the browser
    692  while (g.urls.length) {
    693    var test = g.urls[0];
    694    logger.testStart(test.identifier);
    695    if (test.skip) {
    696      ++g.testResults.Skip;
    697      logger.testEnd(test.identifier, "SKIP");
    698      g.urls.shift();
    699    } else if (test.needsFocus && !Focus()) {
    700      // FIXME: Marking this as a known fail is dangerous!  What
    701      // if it starts failing all the time?
    702      ++g.testResults.Skip;
    703      logger.testEnd(test.identifier, "SKIP", null, "(COULDN'T GET FOCUS)");
    704      g.urls.shift();
    705    } else if (test.slow && !g.runSlowTests) {
    706      ++g.testResults.Slow;
    707      logger.testEnd(test.identifier, "SKIP", null, "(SLOW)");
    708      g.urls.shift();
    709    } else {
    710      break;
    711    }
    712  }
    713 
    714  if (
    715    (!g.urls.length && g.repeat == 0) ||
    716    (g.runUntilFailure && HasUnexpectedResult())
    717  ) {
    718    await RestoreChangedPreferences();
    719    DoneTests();
    720  } else if (!g.urls.length && g.repeat > 0) {
    721    // Repeat
    722    g.repeat--;
    723    ReadTests();
    724  } else {
    725    if (g.urls[0].chaosMode) {
    726      g.windowUtils.enterChaosMode();
    727    }
    728    if (!g.urls[0].needsFocus) {
    729      Blur();
    730    }
    731    var currentTest = g.totalTests - g.urls.length;
    732    g.containingWindow.document.title =
    733      "reftest: " +
    734      currentTest +
    735      " / " +
    736      g.totalTests +
    737      " (" +
    738      Math.floor(100 * (currentTest / g.totalTests)) +
    739      "%)";
    740    StartCurrentURI(URL_TARGET_TYPE_TEST);
    741  }
    742 }
    743 
    744 // A simplified version of the function with the same name in tabbrowser.js.
    745 function updateBrowserRemotenessByURL(aBrowser, aURL) {
    746  var oa = E10SUtils.predictOriginAttributes({ browser: aBrowser });
    747  let remoteType = E10SUtils.getRemoteTypeForURI(
    748    aURL,
    749    aBrowser.ownerGlobal.docShell.nsILoadContext.useRemoteTabs,
    750    aBrowser.ownerGlobal.docShell.nsILoadContext.useRemoteSubframes,
    751    aBrowser.remoteType,
    752    aBrowser.currentURI,
    753    oa
    754  );
    755  // Things get confused if we switch to not-remote
    756  // for chrome:// URIs, so lets not for now.
    757  if (remoteType == E10SUtils.NOT_REMOTE && g.browserIsRemote) {
    758    remoteType = aBrowser.remoteType;
    759  }
    760  if (aBrowser.remoteType != remoteType) {
    761    if (remoteType == E10SUtils.NOT_REMOTE) {
    762      aBrowser.removeAttribute("remote");
    763      aBrowser.removeAttribute("remoteType");
    764    } else {
    765      aBrowser.setAttribute("remote", "true");
    766      aBrowser.setAttribute("remoteType", remoteType);
    767    }
    768    aBrowser.changeRemoteness({ remoteType });
    769    aBrowser.construct();
    770 
    771    g.browserMessageManager = aBrowser.frameLoader.messageManager;
    772    RegisterMessageListenersAndLoadContentScript(true);
    773    return new Promise(resolve => {
    774      g.resolveContentReady = resolve;
    775    });
    776  }
    777 
    778  return Promise.resolve();
    779 }
    780 
    781 // This logic should match SpecialPowersParent._applyPrefs.
    782 function PrefRequiresRefresh(name) {
    783  return (
    784    name == "layout.css.prefers-color-scheme.content-override" ||
    785    name.startsWith("ui.") ||
    786    name.startsWith("browser.display.") ||
    787    name.startsWith("font.")
    788  );
    789 }
    790 
    791 async function StartCurrentURI(aURLTargetType) {
    792  const isStartingRef = aURLTargetType == URL_TARGET_TYPE_REFERENCE;
    793 
    794  g.currentURL = g.urls[0][isStartingRef ? "url2" : "url1"].spec;
    795  g.currentURLTargetType = aURLTargetType;
    796 
    797  await RestoreChangedPreferences();
    798 
    799  const prefSettings =
    800    g.urls[0][isStartingRef ? "prefSettings2" : "prefSettings1"];
    801 
    802  var prefsRequireRefresh = false;
    803 
    804  if (prefSettings.length) {
    805    var badPref = undefined;
    806    try {
    807      prefSettings.forEach(function (ps) {
    808        let prefExists = false;
    809        try {
    810          let prefType = Services.prefs.getPrefType(ps.name);
    811          prefExists = prefType != Services.prefs.PREF_INVALID;
    812        } catch (e) {}
    813        if (!prefExists) {
    814          logger.info("Pref " + ps.name + " not found, will be added");
    815        }
    816 
    817        let oldVal = undefined;
    818        if (prefExists) {
    819          if (ps.type == PREF_BOOLEAN) {
    820            // eslint-disable-next-line mozilla/use-default-preference-values
    821            try {
    822              oldVal = Services.prefs.getBoolPref(ps.name);
    823            } catch (e) {
    824              badPref = "boolean preference '" + ps.name + "'";
    825              throw new Error("bad pref");
    826            }
    827          } else if (ps.type == PREF_STRING) {
    828            try {
    829              oldVal = Services.prefs.getStringPref(ps.name);
    830            } catch (e) {
    831              badPref = "string preference '" + ps.name + "'";
    832              throw new Error("bad pref");
    833            }
    834          } else if (ps.type == PREF_INTEGER) {
    835            // eslint-disable-next-line mozilla/use-default-preference-values
    836            try {
    837              oldVal = Services.prefs.getIntPref(ps.name);
    838            } catch (e) {
    839              badPref = "integer preference '" + ps.name + "'";
    840              throw new Error("bad pref");
    841            }
    842          } else {
    843            throw new Error("internal error - unknown preference type");
    844          }
    845        }
    846        if (!prefExists || oldVal != ps.value) {
    847          var requiresRefresh = PrefRequiresRefresh(ps.name);
    848          prefsRequireRefresh = prefsRequireRefresh || requiresRefresh;
    849          g.prefsToRestore.push({
    850            name: ps.name,
    851            type: ps.type,
    852            value: oldVal,
    853            requiresRefresh,
    854            prefExisted: prefExists,
    855          });
    856          var value = ps.value;
    857          if (ps.type == PREF_BOOLEAN) {
    858            Services.prefs.setBoolPref(ps.name, value);
    859          } else if (ps.type == PREF_STRING) {
    860            Services.prefs.setStringPref(ps.name, value);
    861            value = '"' + value + '"';
    862          } else if (ps.type == PREF_INTEGER) {
    863            Services.prefs.setIntPref(ps.name, value);
    864          }
    865          logger.info("SET PREFERENCE pref(" + ps.name + "," + value + ")");
    866        }
    867      });
    868    } catch (e) {
    869      if (e.message == "bad pref") {
    870        var test = g.urls[0];
    871        if (test.expected == EXPECTED_FAIL) {
    872          logger.testEnd(
    873            test.identifier,
    874            "FAIL",
    875            "FAIL",
    876            "(SKIPPED; " + badPref + " not known or wrong type)"
    877          );
    878          ++g.testResults.Skip;
    879        } else {
    880          logger.testEnd(
    881            test.identifier,
    882            "FAIL",
    883            "PASS",
    884            badPref + " not known or wrong type"
    885          );
    886          ++g.testResults.UnexpectedFail;
    887        }
    888 
    889        // skip the test that had a bad preference
    890        g.urls.shift();
    891        await StartCurrentTest();
    892        return;
    893      }
    894      throw e;
    895    }
    896  }
    897 
    898  if (g.windowUtils.isWindowFullyOccluded || g.windowUtils.isCompositorPaused) {
    899    logger.warning(
    900      "g.windowUtils.isWindowFullyOccluded " +
    901        g.windowUtils.isWindowFullyOccluded
    902    );
    903    logger.warning(
    904      "g.windowUtils.isCompositorPaused " + g.windowUtils.isCompositorPaused
    905    );
    906  }
    907 
    908  if (
    909    !prefSettings.length &&
    910    g.uriCanvases[g.currentURL] &&
    911    (g.urls[0].type == TYPE_REFTEST_EQUAL ||
    912      g.urls[0].type == TYPE_REFTEST_NOTEQUAL) &&
    913    g.urls[0].maxAsserts == 0
    914  ) {
    915    // Pretend the document loaded --- RecordResult will notice
    916    // there's already a canvas for this URL
    917    g.containingWindow.setTimeout(RecordResult, 0);
    918  } else {
    919    var currentTest = g.totalTests - g.urls.length;
    920    // Log this to preserve the same overall log format,
    921    // should be removed if the format is updated
    922    gDumpFn(
    923      "REFTEST TEST-LOAD | " +
    924        g.currentURL +
    925        " | " +
    926        currentTest +
    927        " / " +
    928        g.totalTests +
    929        " (" +
    930        Math.floor(100 * (currentTest / g.totalTests)) +
    931        "%)\n"
    932    );
    933    TestBuffer("START " + g.currentURL);
    934 
    935    if (
    936      g.windowUtils.isWindowFullyOccluded ||
    937      g.windowUtils.isCompositorPaused
    938    ) {
    939      TestBuffer(
    940        "g.windowUtils.isWindowFullyOccluded " +
    941          g.windowUtils.isWindowFullyOccluded
    942      );
    943      TestBuffer(
    944        "g.windowUtils.isCompositorPaused " + g.windowUtils.isCompositorPaused
    945      );
    946    }
    947 
    948    await updateBrowserRemotenessByURL(g.browser, g.currentURL);
    949 
    950    if (prefsRequireRefresh) {
    951      await new Promise(resolve =>
    952        g.containingWindow.requestAnimationFrame(resolve)
    953      );
    954    }
    955 
    956    if (prefSettings.length) {
    957      // Some prefs affect CSS parsing.
    958      ChromeUtils.clearResourceCache({
    959        types: ["stylesheet"],
    960      });
    961    }
    962 
    963    var type = g.urls[0].type;
    964    if (TYPE_SCRIPT == type) {
    965      SendLoadScriptTest(g.currentURL, g.loadTimeout);
    966    } else if (TYPE_PRINT == type) {
    967      SendLoadPrintTest(g.currentURL, g.loadTimeout);
    968    } else {
    969      SendLoadTest(type, g.currentURL, g.currentURLTargetType, g.loadTimeout);
    970    }
    971  }
    972 }
    973 
    974 function DoneTests() {
    975  PerTestCoverageUtils.afterTest()
    976    .catch(e => logger.error("EXCEPTION: " + e))
    977    .then(() => {
    978      if (g.manageSuite) {
    979        g.suiteStarted = false;
    980        logger.suiteEnd({ results: g.testResults });
    981      } else {
    982        logger.logData("results", { results: g.testResults });
    983      }
    984      logger.info(
    985        "Slowest test took " +
    986          g.slowestTestTime +
    987          "ms (" +
    988          g.slowestTestURL +
    989          ")"
    990      );
    991      logger.info("Total canvas count = " + g.recycledCanvases.length);
    992      if (g.failedUseWidgetLayers) {
    993        LogWidgetLayersFailure();
    994      }
    995 
    996      function onStopped() {
    997        if (g.logFile) {
    998          g.logFile.close();
    999          g.logFile = null;
   1000        }
   1001        Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
   1002      }
   1003      if (g.server) {
   1004        g.server.stop(onStopped);
   1005      } else {
   1006        onStopped();
   1007      }
   1008    });
   1009 }
   1010 
   1011 function UpdateCanvasCache(url, canvas) {
   1012  var spec = url.spec;
   1013 
   1014  --g.uriUseCounts[spec];
   1015 
   1016  if (g.uriUseCounts[spec] == 0) {
   1017    ReleaseCanvas(canvas);
   1018    delete g.uriCanvases[spec];
   1019  } else if (g.uriUseCounts[spec] > 0) {
   1020    g.uriCanvases[spec] = canvas;
   1021  } else {
   1022    throw new Error("Use counts were computed incorrectly");
   1023  }
   1024 }
   1025 
   1026 // Recompute drawWindow flags for every drawWindow operation.
   1027 // We have to do this every time since our window can be
   1028 // asynchronously resized (e.g. by the window manager, to make
   1029 // it fit on screen) at unpredictable times.
   1030 // Fortunately this is pretty cheap.
   1031 async function DoDrawWindow(ctx, x, y, w, h) {
   1032  if (g.useDrawSnapshot) {
   1033    try {
   1034      let image = await g.browser.drawSnapshot(x, y, w, h, 1.0, "#fff");
   1035      ctx.drawImage(image, x, y);
   1036    } catch (ex) {
   1037      logger.error(g.currentURL + " | drawSnapshot failed: " + ex);
   1038      ++g.testResults.Exception;
   1039    }
   1040    return;
   1041  }
   1042 
   1043  var flags = ctx.DRAWWINDOW_DRAW_CARET | ctx.DRAWWINDOW_DRAW_VIEW;
   1044  var testRect = g.browser.getBoundingClientRect();
   1045  if (
   1046    g.ignoreWindowSize ||
   1047    (0 <= testRect.left &&
   1048      0 <= testRect.top &&
   1049      g.containingWindow.innerWidth >= testRect.right &&
   1050      g.containingWindow.innerHeight >= testRect.bottom)
   1051  ) {
   1052    // We can use the window's retained layer manager
   1053    // because the window is big enough to display the entire
   1054    // browser element
   1055    flags |= ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
   1056  } else if (g.browserIsRemote) {
   1057    logger.error(g.currentURL + " | can't drawWindow remote content");
   1058    ++g.testResults.Exception;
   1059  }
   1060 
   1061  if (g.drawWindowFlags != flags) {
   1062    // Every time the flags change, dump the new state.
   1063    g.drawWindowFlags = flags;
   1064    var flagsStr = "DRAWWINDOW_DRAW_CARET | DRAWWINDOW_DRAW_VIEW";
   1065    if (flags & ctx.DRAWWINDOW_USE_WIDGET_LAYERS) {
   1066      flagsStr += " | DRAWWINDOW_USE_WIDGET_LAYERS";
   1067    } else {
   1068      // Output a special warning because we need to be able to detect
   1069      // this whenever it happens.
   1070      LogWidgetLayersFailure();
   1071      g.failedUseWidgetLayers = true;
   1072    }
   1073    logger.info(
   1074      "drawWindow flags = " +
   1075        flagsStr +
   1076        "; window size = " +
   1077        g.containingWindow.innerWidth +
   1078        "," +
   1079        g.containingWindow.innerHeight +
   1080        "; test browser size = " +
   1081        testRect.width +
   1082        "," +
   1083        testRect.height
   1084    );
   1085  }
   1086 
   1087  TestBuffer("DoDrawWindow " + x + "," + y + "," + w + "," + h);
   1088  ctx.save();
   1089  ctx.translate(x, y);
   1090  ctx.drawWindow(
   1091    g.containingWindow,
   1092    x,
   1093    y,
   1094    w,
   1095    h,
   1096    "rgb(255,255,255)",
   1097    g.drawWindowFlags
   1098  );
   1099  ctx.restore();
   1100 }
   1101 
   1102 async function InitCurrentCanvasWithSnapshot() {
   1103  TestBuffer("Initializing canvas snapshot");
   1104 
   1105  if (
   1106    g.urls[0].type == TYPE_LOAD ||
   1107    g.urls[0].type == TYPE_SCRIPT ||
   1108    g.urls[0].type == TYPE_PRINT
   1109  ) {
   1110    // We don't want to snapshot this kind of test
   1111    return false;
   1112  }
   1113 
   1114  if (!g.currentCanvas) {
   1115    g.currentCanvas = AllocateCanvas();
   1116  }
   1117 
   1118  var ctx = g.currentCanvas.getContext("2d");
   1119  await DoDrawWindow(ctx, 0, 0, g.currentCanvas.width, g.currentCanvas.height);
   1120  return true;
   1121 }
   1122 
   1123 async function UpdateCurrentCanvasForInvalidation(rects) {
   1124  TestBuffer("Updating canvas for invalidation");
   1125 
   1126  if (!g.currentCanvas) {
   1127    return;
   1128  }
   1129 
   1130  var ctx = g.currentCanvas.getContext("2d");
   1131  for (var i = 0; i < rects.length; ++i) {
   1132    var r = rects[i];
   1133    // Set left/top/right/bottom to pixel boundaries
   1134    var left = Math.floor(r.left);
   1135    var top = Math.floor(r.top);
   1136    var right = Math.ceil(r.right);
   1137    var bottom = Math.ceil(r.bottom);
   1138 
   1139    // Clamp the values to the canvas size
   1140    left = Math.max(0, Math.min(left, g.currentCanvas.width));
   1141    top = Math.max(0, Math.min(top, g.currentCanvas.height));
   1142    right = Math.max(0, Math.min(right, g.currentCanvas.width));
   1143    bottom = Math.max(0, Math.min(bottom, g.currentCanvas.height));
   1144 
   1145    await DoDrawWindow(ctx, left, top, right - left, bottom - top);
   1146  }
   1147 }
   1148 
   1149 async function UpdateWholeCurrentCanvasForInvalidation() {
   1150  TestBuffer("Updating entire canvas for invalidation");
   1151 
   1152  if (!g.currentCanvas) {
   1153    return;
   1154  }
   1155 
   1156  var ctx = g.currentCanvas.getContext("2d");
   1157  await DoDrawWindow(ctx, 0, 0, g.currentCanvas.width, g.currentCanvas.height);
   1158 }
   1159 
   1160 // eslint-disable-next-line complexity
   1161 function RecordResult(testRunTime, errorMsg, typeSpecificResults) {
   1162  TestBuffer("RecordResult fired");
   1163 
   1164  if (g.windowUtils.isWindowFullyOccluded || g.windowUtils.isCompositorPaused) {
   1165    TestBuffer(
   1166      "g.windowUtils.isWindowFullyOccluded " +
   1167        g.windowUtils.isWindowFullyOccluded
   1168    );
   1169    TestBuffer(
   1170      "g.windowUtils.isCompositorPaused " + g.windowUtils.isCompositorPaused
   1171    );
   1172  }
   1173 
   1174  // Keep track of which test was slowest, and how long it took.
   1175  if (testRunTime > g.slowestTestTime) {
   1176    g.slowestTestTime = testRunTime;
   1177    g.slowestTestURL = g.currentURL;
   1178  }
   1179 
   1180  // Not 'const ...' because of 'EXPECTED_*' value dependency.
   1181  var outputs = {};
   1182  outputs[EXPECTED_PASS] = {
   1183    true: { s: ["PASS", "PASS"], n: "Pass" },
   1184    false: { s: ["FAIL", "PASS"], n: "UnexpectedFail" },
   1185  };
   1186  outputs[EXPECTED_FAIL] = {
   1187    true: { s: ["PASS", "FAIL"], n: "UnexpectedPass" },
   1188    false: { s: ["FAIL", "FAIL"], n: "KnownFail" },
   1189  };
   1190  outputs[EXPECTED_RANDOM] = {
   1191    true: { s: ["PASS", "PASS"], n: "Random" },
   1192    false: { s: ["FAIL", "FAIL"], n: "Random" },
   1193  };
   1194  // for EXPECTED_FUZZY we need special handling because we can have
   1195  // Pass, UnexpectedPass, or UnexpectedFail
   1196 
   1197  if (
   1198    (g.currentURLTargetType == URL_TARGET_TYPE_TEST &&
   1199      g.urls[0].wrCapture.test) ||
   1200    (g.currentURLTargetType == URL_TARGET_TYPE_REFERENCE &&
   1201      g.urls[0].wrCapture.ref)
   1202  ) {
   1203    logger.info("Running webrender capture");
   1204    g.windowUtils.wrCapture();
   1205  }
   1206 
   1207  var output;
   1208  var extra;
   1209 
   1210  if (g.urls[0].type == TYPE_LOAD) {
   1211    ++g.testResults.LoadOnly;
   1212    logger.testStatus(g.urls[0].identifier, "(LOAD ONLY)", "PASS", "PASS");
   1213    g.currentCanvas = null;
   1214    FinishTestItem();
   1215    return;
   1216  }
   1217  if (g.urls[0].type == TYPE_PRINT) {
   1218    switch (g.currentURLTargetType) {
   1219      case URL_TARGET_TYPE_TEST:
   1220        // First document has been loaded.
   1221        g.testPrintOutput = typeSpecificResults;
   1222        // Proceed to load the second document.
   1223        CleanUpCrashDumpFiles();
   1224        StartCurrentURI(URL_TARGET_TYPE_REFERENCE);
   1225        break;
   1226      case URL_TARGET_TYPE_REFERENCE: {
   1227        let pathToTestPdf = g.testPrintOutput;
   1228        let pathToRefPdf = typeSpecificResults;
   1229        comparePdfs(pathToTestPdf, pathToRefPdf, function (error, results) {
   1230          let expected = g.urls[0].expected;
   1231          // TODO: We should complain here if results is empty!
   1232          // (If it's empty, we'll spuriously succeed, regardless of
   1233          // our expectations)
   1234          if (error) {
   1235            output = outputs[expected].false;
   1236            extra = { status_msg: output.n };
   1237            ++g.testResults[output.n];
   1238            logger.testEnd(
   1239              g.urls[0].identifier,
   1240              output.s[0],
   1241              output.s[1],
   1242              error.message,
   1243              null,
   1244              extra
   1245            );
   1246          } else {
   1247            let outputPair = outputs[expected];
   1248            if (expected === EXPECTED_FAIL) {
   1249              let failureResults = results.filter(function (result) {
   1250                return !result.passed;
   1251              });
   1252              if (failureResults.length) {
   1253                // We got an expected failure. Let's get rid of the
   1254                // passes from the results so we don't trigger
   1255                // TEST_UNEXPECTED_PASS logging for those.
   1256                results = failureResults;
   1257              }
   1258              // (else, we expected a failure but got none!
   1259              // Leave results untouched so we can log them.)
   1260            }
   1261            results.forEach(function (result) {
   1262              output = outputPair[result.passed];
   1263              let extraOpt = { status_msg: output.n };
   1264              ++g.testResults[output.n];
   1265              logger.testEnd(
   1266                g.urls[0].identifier,
   1267                output.s[0],
   1268                output.s[1],
   1269                result.description,
   1270                null,
   1271                extraOpt
   1272              );
   1273            });
   1274          }
   1275          FinishTestItem();
   1276        });
   1277        break;
   1278      }
   1279      default:
   1280        throw new Error("Unexpected state.");
   1281    }
   1282    return;
   1283  }
   1284  if (g.urls[0].type == TYPE_SCRIPT) {
   1285    let expected = g.urls[0].expected;
   1286 
   1287    if (errorMsg) {
   1288      // Force an unexpected failure to alert the test author to fix the test.
   1289      expected = EXPECTED_PASS;
   1290    } else if (!typeSpecificResults.length) {
   1291      // This failure may be due to a JavaScript Engine bug causing
   1292      // early termination of the test. If we do not allow silent
   1293      // failure, report an error.
   1294      if (!g.urls[0].allowSilentFail) {
   1295        errorMsg = "No test results reported. (SCRIPT)\n";
   1296      } else {
   1297        logger.info("An expected silent failure occurred");
   1298      }
   1299    }
   1300 
   1301    if (errorMsg) {
   1302      output = outputs[expected].false;
   1303      extra = { status_msg: output.n };
   1304      ++g.testResults[output.n];
   1305      logger.testStatus(
   1306        g.urls[0].identifier,
   1307        errorMsg,
   1308        output.s[0],
   1309        output.s[1],
   1310        null,
   1311        null,
   1312        extra
   1313      );
   1314      FinishTestItem();
   1315      return;
   1316    }
   1317 
   1318    var anyFailed = typeSpecificResults.some(function (result) {
   1319      return !result.passed;
   1320    });
   1321    var outputPair;
   1322    if (anyFailed && expected == EXPECTED_FAIL) {
   1323      // If we're marked as expected to fail, and some (but not all) tests
   1324      // passed, treat those tests as though they were marked random
   1325      // (since we can't tell whether they were really intended to be
   1326      // marked failing or not).
   1327      outputPair = {
   1328        true: outputs[EXPECTED_RANDOM].true,
   1329        false: outputs[expected].false,
   1330      };
   1331    } else {
   1332      outputPair = outputs[expected];
   1333    }
   1334    var index = 0;
   1335    typeSpecificResults.forEach(function (result) {
   1336      var output2 = outputPair[result.passed];
   1337      var extraOpt = { status_msg: output2.n };
   1338 
   1339      ++g.testResults[output2.n];
   1340      logger.testStatus(
   1341        g.urls[0].identifier,
   1342        result.description + " item " + ++index,
   1343        output2.s[0],
   1344        output2.s[1],
   1345        null,
   1346        null,
   1347        extraOpt
   1348      );
   1349    });
   1350 
   1351    if (anyFailed && expected == EXPECTED_PASS) {
   1352      FlushTestBuffer();
   1353    }
   1354 
   1355    FinishTestItem();
   1356    return;
   1357  }
   1358 
   1359  const isRecordingRef = g.currentURLTargetType == URL_TARGET_TYPE_REFERENCE;
   1360  const prefSettings =
   1361    g.urls[0][isRecordingRef ? "prefSettings2" : "prefSettings1"];
   1362 
   1363  if (!prefSettings.length && g.uriCanvases[g.currentURL]) {
   1364    g.currentCanvas = g.uriCanvases[g.currentURL];
   1365  }
   1366  if (g.currentCanvas == null) {
   1367    logger.error(g.currentURL, "program error managing snapshots");
   1368    ++g.testResults.Exception;
   1369  }
   1370  g[isRecordingRef ? "canvas2" : "canvas1"] = g.currentCanvas;
   1371  g.currentCanvas = null;
   1372 
   1373  ResetRenderingState();
   1374 
   1375  switch (g.currentURLTargetType) {
   1376    case URL_TARGET_TYPE_TEST:
   1377      // First document has been loaded.
   1378      // Proceed to load the second document.
   1379 
   1380      CleanUpCrashDumpFiles();
   1381      StartCurrentURI(URL_TARGET_TYPE_REFERENCE);
   1382      break;
   1383    case URL_TARGET_TYPE_REFERENCE: {
   1384      // Both documents have been loaded. Compare the renderings and see
   1385      // if the comparison result matches the expected result specified
   1386      // in the manifest.
   1387 
   1388      // number of different pixels
   1389      var differences;
   1390      // whether the two renderings match:
   1391      var equal;
   1392      var maxDifference = {};
   1393      // whether the allowed fuzziness from the annotations is exceeded
   1394      // by the actual comparison results
   1395      var fuzz_exceeded = false;
   1396 
   1397      // what is expected on this platform (PASS, FAIL, RANDOM, or FUZZY)
   1398      let expected = g.urls[0].expected;
   1399 
   1400      differences = g.windowUtils.compareCanvases(
   1401        g.canvas1,
   1402        g.canvas2,
   1403        maxDifference
   1404      );
   1405 
   1406      if (g.urls[0].noAutoFuzz) {
   1407        // Autofuzzing is disabled
   1408      } else if (
   1409        isAndroidDevice() &&
   1410        maxDifference.value <= 2 &&
   1411        differences > 0
   1412      ) {
   1413        // Autofuzz for WR on Android physical devices: Reduce any
   1414        // maxDifference of 2 to 0, because we get a lot of off-by-ones
   1415        // and off-by-twos that are very random and hard to annotate.
   1416        // In cases where the difference on any pixel component is more
   1417        // than 2 we require manual annotation. Note that this applies
   1418        // to both == tests and != tests, so != tests don't
   1419        // inadvertently pass due to a random off-by-one pixel
   1420        // difference.
   1421        logger.info(
   1422          `REFTEST wr-on-android dropping fuzz of (${maxDifference.value}, ${differences}) to (0, 0)`
   1423        );
   1424        maxDifference.value = 0;
   1425        differences = 0;
   1426      }
   1427 
   1428      equal = differences == 0;
   1429 
   1430      if (maxDifference.value > 0 && equal) {
   1431        throw new Error("Inconsistent result from compareCanvases.");
   1432      }
   1433 
   1434      if (expected == EXPECTED_FUZZY) {
   1435        logger.info(
   1436          `REFTEST fuzzy test ` +
   1437            `(${g.urls[0].fuzzyMinDelta}, ${g.urls[0].fuzzyMinPixels}) <= ` +
   1438            `(${maxDifference.value}, ${differences}) <= ` +
   1439            `(${g.urls[0].fuzzyMaxDelta}, ${g.urls[0].fuzzyMaxPixels})`
   1440        );
   1441        fuzz_exceeded =
   1442          maxDifference.value > g.urls[0].fuzzyMaxDelta ||
   1443          differences > g.urls[0].fuzzyMaxPixels;
   1444        equal =
   1445          !fuzz_exceeded &&
   1446          maxDifference.value >= g.urls[0].fuzzyMinDelta &&
   1447          differences >= g.urls[0].fuzzyMinPixels;
   1448      }
   1449 
   1450      var failedExtraCheck =
   1451        g.failedNoPaint ||
   1452        g.failedNoDisplayList ||
   1453        g.failedDisplayList ||
   1454        g.failedOpaqueLayer ||
   1455        g.failedAssignedLayer;
   1456 
   1457      // whether the comparison result matches what is in the manifest
   1458      var test_passed =
   1459        equal == (g.urls[0].type == TYPE_REFTEST_EQUAL) && !failedExtraCheck;
   1460 
   1461      if (expected != EXPECTED_FUZZY) {
   1462        output = outputs[expected][test_passed];
   1463      } else if (test_passed) {
   1464        output = { s: ["PASS", "PASS"], n: "Pass" };
   1465      } else if (
   1466        g.urls[0].type == TYPE_REFTEST_EQUAL &&
   1467        !failedExtraCheck &&
   1468        !fuzz_exceeded
   1469      ) {
   1470        // If we get here, that means we had an '==' type test where
   1471        // at least one of the actual difference values was below the
   1472        // allowed range, but nothing else was wrong. So let's produce
   1473        // UNEXPECTED-PASS in this scenario. Also, if we enter this
   1474        // branch, 'equal' must be false so let's assert that to guard
   1475        // against logic errors.
   1476        if (equal) {
   1477          throw new Error(
   1478            "Logic error in reftest.sys.mjs fuzzy test handling!"
   1479          );
   1480        }
   1481        output = { s: ["PASS", "FAIL"], n: "UnexpectedPass" };
   1482      } else {
   1483        // In all other cases we fail the test
   1484        output = { s: ["FAIL", "PASS"], n: "UnexpectedFail" };
   1485      }
   1486      extra = { status_msg: output.n };
   1487 
   1488      ++g.testResults[output.n];
   1489 
   1490      // It's possible that we failed both an "extra check" and the normal comparison, but we don't
   1491      // have a way to annotate these separately, so just print an error for the extra check failures.
   1492      if (failedExtraCheck) {
   1493        var failures = [];
   1494        if (g.failedNoPaint) {
   1495          failures.push("failed reftest-no-paint");
   1496        }
   1497        if (g.failedNoDisplayList) {
   1498          failures.push("failed reftest-no-display-list");
   1499        }
   1500        if (g.failedDisplayList) {
   1501          failures.push("failed reftest-display-list");
   1502        }
   1503        // The g.failed*Messages arrays will contain messages from both the test and the reference.
   1504        if (g.failedOpaqueLayer) {
   1505          failures.push(
   1506            "failed reftest-opaque-layer: " +
   1507              g.failedOpaqueLayerMessages.join(", ")
   1508          );
   1509        }
   1510        if (g.failedAssignedLayer) {
   1511          failures.push(
   1512            "failed reftest-assigned-layer: " +
   1513              g.failedAssignedLayerMessages.join(", ")
   1514          );
   1515        }
   1516        var failureString = failures.join(", ");
   1517        logger.testStatus(
   1518          g.urls[0].identifier,
   1519          failureString,
   1520          output.s[0],
   1521          output.s[1],
   1522          null,
   1523          null,
   1524          extra
   1525        );
   1526      } else {
   1527        var message =
   1528          "image comparison, max difference: " +
   1529          maxDifference.value +
   1530          ", number of differing pixels: " +
   1531          differences;
   1532        if (
   1533          (!test_passed && expected == EXPECTED_PASS) ||
   1534          (!test_passed && expected == EXPECTED_FUZZY) ||
   1535          (test_passed && expected == EXPECTED_FAIL)
   1536        ) {
   1537          if (!equal) {
   1538            extra.max_difference = maxDifference.value;
   1539            extra.differences = differences;
   1540            let image1 = g.canvas1.toDataURL();
   1541            let image2 = g.canvas2.toDataURL();
   1542            extra.reftest_screenshots = [
   1543              {
   1544                url: g.urls[0].identifier[0],
   1545                screenshot: image1.slice(image1.indexOf(",") + 1),
   1546              },
   1547              g.urls[0].identifier[1],
   1548              {
   1549                url: g.urls[0].identifier[2],
   1550                screenshot: image2.slice(image2.indexOf(",") + 1),
   1551              },
   1552            ];
   1553            extra.image1 = image1;
   1554            extra.image2 = image2;
   1555          } else {
   1556            let image1 = g.canvas1.toDataURL();
   1557            extra.reftest_screenshots = [
   1558              {
   1559                url: g.urls[0].identifier[0],
   1560                screenshot: image1.slice(image1.indexOf(",") + 1),
   1561              },
   1562            ];
   1563            extra.image1 = image1;
   1564          }
   1565        }
   1566        extra.modifiers = g.urls[0].modifiers;
   1567 
   1568        logger.testStatus(
   1569          g.urls[0].identifier,
   1570          message,
   1571          output.s[0],
   1572          output.s[1],
   1573          null,
   1574          null,
   1575          extra
   1576        );
   1577 
   1578        if (g.noCanvasCache) {
   1579          ReleaseCanvas(g.canvas1);
   1580          ReleaseCanvas(g.canvas2);
   1581        } else {
   1582          if (!g.urls[0].prefSettings1.length) {
   1583            UpdateCanvasCache(g.urls[0].url1, g.canvas1);
   1584          }
   1585          if (!g.urls[0].prefSettings2.length) {
   1586            UpdateCanvasCache(g.urls[0].url2, g.canvas2);
   1587          }
   1588        }
   1589      }
   1590 
   1591      if (
   1592        (!test_passed && expected == EXPECTED_PASS) ||
   1593        (test_passed && expected == EXPECTED_FAIL)
   1594      ) {
   1595        FlushTestBuffer();
   1596      }
   1597 
   1598      CleanUpCrashDumpFiles();
   1599      FinishTestItem();
   1600      break;
   1601    }
   1602    default:
   1603      throw new Error("Unexpected state.");
   1604  }
   1605 }
   1606 
   1607 function LoadFailed(why) {
   1608  ++g.testResults.FailedLoad;
   1609  if (!why) {
   1610    // reftest-content.js sets an initial reason before it sets the
   1611    // timeout that will call us with the currently set reason, so we
   1612    // should never get here.  If we do then there's a logic error
   1613    // somewhere.  Perhaps tests are somehow running overlapped and the
   1614    // timeout for one test is not being cleared before the timeout for
   1615    // another is set?  Maybe there's some sort of race?
   1616    logger.error(
   1617      "load failed with unknown reason (we should always have a reason!)"
   1618    );
   1619  }
   1620  logger.testStatus(
   1621    g.urls[0].identifier,
   1622    "load failed: " + why,
   1623    "FAIL",
   1624    "PASS"
   1625  );
   1626  FlushTestBuffer();
   1627  FinishTestItem();
   1628 }
   1629 
   1630 function RemoveExpectedCrashDumpFiles() {
   1631  if (g.expectingProcessCrash) {
   1632    for (let crashFilename of g.expectedCrashDumpFiles) {
   1633      let file = g.crashDumpDir.clone();
   1634      file.append(crashFilename);
   1635      if (file.exists()) {
   1636        file.remove(false);
   1637      }
   1638    }
   1639  }
   1640  g.expectedCrashDumpFiles.length = 0;
   1641 }
   1642 
   1643 function FindUnexpectedCrashDumpFiles() {
   1644  if (!g.crashDumpDir.exists()) {
   1645    return;
   1646  }
   1647 
   1648  let entries = g.crashDumpDir.directoryEntries;
   1649  if (!entries) {
   1650    return;
   1651  }
   1652 
   1653  let foundCrashDumpFile = false;
   1654  while (entries.hasMoreElements()) {
   1655    let file = entries.nextFile;
   1656    let path = String(file.path);
   1657    if (path.match(/\.(dmp|extra)$/) && !g.unexpectedCrashDumpFiles[path]) {
   1658      if (!foundCrashDumpFile) {
   1659        ++g.testResults.UnexpectedFail;
   1660        foundCrashDumpFile = true;
   1661        if (g.currentURL) {
   1662          logger.testStatus(
   1663            g.urls[0].identifier,
   1664            "crash-check",
   1665            "FAIL",
   1666            "PASS",
   1667            "This test left crash dumps behind, but we weren't expecting it to!"
   1668          );
   1669        } else {
   1670          logger.error(
   1671            "Harness startup left crash dumps behind, but we weren't expecting it to!"
   1672          );
   1673        }
   1674      }
   1675      logger.info("Found unexpected crash dump file " + path);
   1676      g.unexpectedCrashDumpFiles[path] = true;
   1677    }
   1678  }
   1679 }
   1680 
   1681 function RemovePendingCrashDumpFiles() {
   1682  if (!g.pendingCrashDumpDir.exists()) {
   1683    return;
   1684  }
   1685 
   1686  let entries = g.pendingCrashDumpDir.directoryEntries;
   1687  while (entries.hasMoreElements()) {
   1688    let file = entries.nextFile;
   1689    if (file.isFile()) {
   1690      file.remove(false);
   1691      logger.info("This test left pending crash dumps; deleted " + file.path);
   1692    }
   1693  }
   1694 }
   1695 
   1696 function CleanUpCrashDumpFiles() {
   1697  RemoveExpectedCrashDumpFiles();
   1698  FindUnexpectedCrashDumpFiles();
   1699  if (g.cleanupPendingCrashes) {
   1700    RemovePendingCrashDumpFiles();
   1701  }
   1702  g.expectingProcessCrash = false;
   1703 }
   1704 
   1705 function FinishTestItem() {
   1706  logger.testEnd(g.urls[0].identifier, "OK");
   1707 
   1708  // Replace document with BLANK_URL_FOR_CLEARING in case there are
   1709  // assertions when unloading.
   1710  logger.debug("Loading a blank page");
   1711  // After clearing, content will notify us of the assertion count
   1712  // and tests will continue.
   1713  SendClear();
   1714  g.failedNoPaint = false;
   1715  g.failedNoDisplayList = false;
   1716  g.failedDisplayList = false;
   1717  g.failedOpaqueLayer = false;
   1718  g.failedOpaqueLayerMessages = [];
   1719  g.failedAssignedLayer = false;
   1720  g.failedAssignedLayerMessages = [];
   1721 }
   1722 
   1723 async function DoAssertionCheck(numAsserts) {
   1724  if (g.debug.isDebugBuild) {
   1725    if (g.browserIsRemote) {
   1726      // Count chrome-process asserts too when content is out of
   1727      // process.
   1728      var newAssertionCount = g.debug.assertionCount;
   1729      var numLocalAsserts = newAssertionCount - g.assertionCount;
   1730      g.assertionCount = newAssertionCount;
   1731 
   1732      numAsserts += numLocalAsserts;
   1733    }
   1734 
   1735    var minAsserts = g.urls[0].minAsserts;
   1736    var maxAsserts = g.urls[0].maxAsserts;
   1737 
   1738    if (numAsserts < minAsserts) {
   1739      ++g.testResults.AssertionUnexpectedFixed;
   1740    } else if (numAsserts > maxAsserts) {
   1741      ++g.testResults.AssertionUnexpected;
   1742    } else if (numAsserts != 0) {
   1743      ++g.testResults.AssertionKnown;
   1744    }
   1745    logger.assertionCount(
   1746      g.urls[0].identifier,
   1747      numAsserts,
   1748      minAsserts,
   1749      maxAsserts
   1750    );
   1751  }
   1752 
   1753  if (g.urls[0].chaosMode) {
   1754    g.windowUtils.leaveChaosMode();
   1755  }
   1756 
   1757  // And start the next test.
   1758  g.urls.shift();
   1759  await StartCurrentTest();
   1760 }
   1761 
   1762 function ResetRenderingState() {
   1763  SendResetRenderingState();
   1764  // We would want to clear any viewconfig here, if we add support for it
   1765 }
   1766 
   1767 async function RestoreChangedPreferences() {
   1768  // Restore any preferences set via SpecialPowers in a previous test.
   1769  // On Android, g.containingWindow typically doesn't doesn't have a
   1770  // SpecialPowers property because it was created before SpecialPowers was
   1771  // registered.
   1772  // Get a parent actor so that there is less waiting than with a child.
   1773  let { requiresRefresh } =
   1774    g.containingWindow.browsingContext.currentWindowGlobal
   1775      .getActor("SpecialPowers")
   1776      .flushPrefEnv();
   1777 
   1778  if (!g.prefsToRestore.length && !requiresRefresh) {
   1779    return;
   1780  }
   1781  g.prefsToRestore.reverse();
   1782  g.prefsToRestore.forEach(function (ps) {
   1783    requiresRefresh = requiresRefresh || ps.requiresRefresh;
   1784    if (ps.prefExisted) {
   1785      var value = ps.value;
   1786      if (ps.type == PREF_BOOLEAN) {
   1787        Services.prefs.setBoolPref(ps.name, value);
   1788      } else if (ps.type == PREF_STRING) {
   1789        Services.prefs.setStringPref(ps.name, value);
   1790        value = '"' + value + '"';
   1791      } else if (ps.type == PREF_INTEGER) {
   1792        Services.prefs.setIntPref(ps.name, value);
   1793      }
   1794      logger.info("RESTORE PREFERENCE pref(" + ps.name + "," + value + ")");
   1795    } else {
   1796      Services.prefs.clearUserPref(ps.name);
   1797      logger.info(
   1798        "RESTORE PREFERENCE pref(" +
   1799          ps.name +
   1800          ", <no value set>) (clearing user pref)"
   1801      );
   1802    }
   1803  });
   1804 
   1805  g.prefsToRestore = [];
   1806 
   1807  if (requiresRefresh) {
   1808    await new Promise(resolve =>
   1809      g.containingWindow.requestAnimationFrame(resolve)
   1810    );
   1811  }
   1812 }
   1813 
   1814 function RegisterMessageListenersAndLoadContentScript(aReload) {
   1815  g.browserMessageManager.addMessageListener(
   1816    "reftest:AssertionCount",
   1817    function (m) {
   1818      RecvAssertionCount(m.json.count);
   1819    }
   1820  );
   1821  g.browserMessageManager.addMessageListener(
   1822    "reftest:ContentReady",
   1823    function (m) {
   1824      return RecvContentReady(m.data);
   1825    }
   1826  );
   1827  g.browserMessageManager.addMessageListener("reftest:Exception", function (m) {
   1828    RecvException(m.json.what);
   1829  });
   1830  g.browserMessageManager.addMessageListener(
   1831    "reftest:FailedLoad",
   1832    function (m) {
   1833      RecvFailedLoad(m.json.why);
   1834    }
   1835  );
   1836  g.browserMessageManager.addMessageListener(
   1837    "reftest:FailedNoPaint",
   1838    function () {
   1839      RecvFailedNoPaint();
   1840    }
   1841  );
   1842  g.browserMessageManager.addMessageListener(
   1843    "reftest:FailedNoDisplayList",
   1844    function () {
   1845      RecvFailedNoDisplayList();
   1846    }
   1847  );
   1848  g.browserMessageManager.addMessageListener(
   1849    "reftest:FailedDisplayList",
   1850    function () {
   1851      RecvFailedDisplayList();
   1852    }
   1853  );
   1854  g.browserMessageManager.addMessageListener(
   1855    "reftest:FailedOpaqueLayer",
   1856    function (m) {
   1857      RecvFailedOpaqueLayer(m.json.why);
   1858    }
   1859  );
   1860  g.browserMessageManager.addMessageListener(
   1861    "reftest:FailedAssignedLayer",
   1862    function (m) {
   1863      RecvFailedAssignedLayer(m.json.why);
   1864    }
   1865  );
   1866  g.browserMessageManager.addMessageListener(
   1867    "reftest:InitCanvasWithSnapshot",
   1868    function () {
   1869      RecvInitCanvasWithSnapshot();
   1870    }
   1871  );
   1872  g.browserMessageManager.addMessageListener("reftest:Log", function (m) {
   1873    RecvLog(m.json.type, m.json.msg);
   1874  });
   1875  g.browserMessageManager.addMessageListener(
   1876    "reftest:ScriptResults",
   1877    function (m) {
   1878      RecvScriptResults(m.json.runtimeMs, m.json.error, m.json.results);
   1879    }
   1880  );
   1881  g.browserMessageManager.addMessageListener(
   1882    "reftest:StartPrint",
   1883    function (m) {
   1884      RecvStartPrint(m.json.isPrintSelection, m.json.printRange);
   1885    }
   1886  );
   1887  g.browserMessageManager.addMessageListener(
   1888    "reftest:PrintResult",
   1889    function (m) {
   1890      RecvPrintResult(m.json.runtimeMs, m.json.status, m.json.fileName);
   1891    }
   1892  );
   1893  g.browserMessageManager.addMessageListener("reftest:TestDone", function (m) {
   1894    RecvTestDone(m.json.runtimeMs);
   1895  });
   1896  g.browserMessageManager.addMessageListener(
   1897    "reftest:UpdateCanvasForInvalidation",
   1898    function (m) {
   1899      RecvUpdateCanvasForInvalidation(m.json.rects);
   1900    }
   1901  );
   1902  g.browserMessageManager.addMessageListener(
   1903    "reftest:UpdateWholeCanvasForInvalidation",
   1904    function () {
   1905      RecvUpdateWholeCanvasForInvalidation();
   1906    }
   1907  );
   1908  g.browserMessageManager.addMessageListener(
   1909    "reftest:ExpectProcessCrash",
   1910    function () {
   1911      RecvExpectProcessCrash();
   1912    }
   1913  );
   1914 
   1915  g.browserMessageManager.loadFrameScript(
   1916    "resource://reftest/reftest-content.js",
   1917    true,
   1918    true
   1919  );
   1920 
   1921  if (aReload) {
   1922    return;
   1923  }
   1924 
   1925  ChromeUtils.registerWindowActor("ReftestFission", {
   1926    parent: {
   1927      esModuleURI: "resource://reftest/ReftestFissionParent.sys.mjs",
   1928    },
   1929    child: {
   1930      esModuleURI: "resource://reftest/ReftestFissionChild.sys.mjs",
   1931      events: {
   1932        MozAfterPaint: {},
   1933      },
   1934    },
   1935    allFrames: true,
   1936    includeChrome: true,
   1937  });
   1938 }
   1939 
   1940 async function RecvAssertionCount(count) {
   1941  await DoAssertionCheck(count);
   1942 }
   1943 
   1944 function RecvContentReady(info) {
   1945  if (g.resolveContentReady) {
   1946    g.resolveContentReady();
   1947    g.resolveContentReady = null;
   1948  } else {
   1949    // Prevent a race with GeckoView:SetFocused, bug 1960620
   1950    // If about:blank loads synchronously, we'll RecvContentReady on the first tick,
   1951    // which is also the tick where GeckoViewContent processes messages from GeckoView.
   1952    setTimeout(() => {
   1953      g.contentGfxInfo = info.gfx;
   1954      InitAndStartRefTests();
   1955    }, 0);
   1956  }
   1957  return { remote: g.browserIsRemote };
   1958 }
   1959 
   1960 function RecvException(what) {
   1961  logger.error(g.currentURL + " | " + what);
   1962  ++g.testResults.Exception;
   1963 }
   1964 
   1965 function RecvFailedLoad(why) {
   1966  LoadFailed(why);
   1967 }
   1968 
   1969 function RecvFailedNoPaint() {
   1970  g.failedNoPaint = true;
   1971 }
   1972 
   1973 function RecvFailedNoDisplayList() {
   1974  g.failedNoDisplayList = true;
   1975 }
   1976 
   1977 function RecvFailedDisplayList() {
   1978  g.failedDisplayList = true;
   1979 }
   1980 
   1981 function RecvFailedOpaqueLayer(why) {
   1982  g.failedOpaqueLayer = true;
   1983  g.failedOpaqueLayerMessages.push(why);
   1984 }
   1985 
   1986 function RecvFailedAssignedLayer(why) {
   1987  g.failedAssignedLayer = true;
   1988  g.failedAssignedLayerMessages.push(why);
   1989 }
   1990 
   1991 async function RecvInitCanvasWithSnapshot() {
   1992  var painted = await InitCurrentCanvasWithSnapshot();
   1993  SendUpdateCurrentCanvasWithSnapshotDone(painted);
   1994 }
   1995 
   1996 function RecvLog(type, msg) {
   1997  msg = "[CONTENT] " + msg;
   1998  if (type == "info") {
   1999    TestBuffer(msg);
   2000  } else if (type == "warning") {
   2001    logger.warning(msg);
   2002  } else if (type == "error") {
   2003    logger.error(
   2004      "REFTEST TEST-UNEXPECTED-FAIL | " + g.currentURL + " | " + msg + "\n"
   2005    );
   2006    ++g.testResults.Exception;
   2007  } else {
   2008    logger.error(
   2009      "REFTEST TEST-UNEXPECTED-FAIL | " +
   2010        g.currentURL +
   2011        " | unknown log type " +
   2012        type +
   2013        "\n"
   2014    );
   2015    ++g.testResults.Exception;
   2016  }
   2017 }
   2018 
   2019 function RecvScriptResults(runtimeMs, error, results) {
   2020  RecordResult(runtimeMs, error, results);
   2021 }
   2022 
   2023 function RecvStartPrint(isPrintSelection, printRange) {
   2024  let fileName = `reftest-print-${Date.now()}-`;
   2025  crypto
   2026    .getRandomValues(new Uint8Array(4))
   2027    .forEach(x => (fileName += x.toString(16)));
   2028  fileName += ".pdf";
   2029  let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
   2030  file.append(fileName);
   2031 
   2032  let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
   2033    Ci.nsIPrintSettingsService
   2034  );
   2035  let ps = PSSVC.createNewPrintSettings();
   2036  ps.printSilent = true;
   2037  ps.printBGImages = true;
   2038  ps.printBGColors = true;
   2039  ps.unwriteableMarginTop = 0;
   2040  ps.unwriteableMarginRight = 0;
   2041  ps.unwriteableMarginLeft = 0;
   2042  ps.unwriteableMarginBottom = 0;
   2043  ps.outputDestination = Ci.nsIPrintSettings.kOutputDestinationFile;
   2044  ps.toFileName = file.path;
   2045  ps.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
   2046  ps.printSelectionOnly = isPrintSelection;
   2047  if (printRange && !isPrintSelection) {
   2048    ps.pageRanges = printRange
   2049      .split(",")
   2050      .map(function (r) {
   2051        let range = r.split("-");
   2052        return [+range[0] || 1, +range[1] || 1];
   2053      })
   2054      .flat();
   2055  }
   2056 
   2057  ps.printInColor = Services.prefs.getBoolPref("print.print_in_color", true);
   2058 
   2059  g.browser.browsingContext
   2060    .print(ps)
   2061    .then(() => SendPrintDone(Cr.NS_OK, file.path))
   2062    .catch(exception => SendPrintDone(exception.code, file.path));
   2063 }
   2064 
   2065 function RecvPrintResult(runtimeMs, status, fileName) {
   2066  if (!Components.isSuccessCode(status)) {
   2067    logger.error(
   2068      "REFTEST TEST-UNEXPECTED-FAIL | " +
   2069        g.currentURL +
   2070        " | error during printing\n"
   2071    );
   2072    ++g.testResults.Exception;
   2073  }
   2074  RecordResult(runtimeMs, "", fileName);
   2075 }
   2076 
   2077 function RecvTestDone(runtimeMs) {
   2078  RecordResult(runtimeMs, "", []);
   2079 }
   2080 
   2081 async function RecvUpdateCanvasForInvalidation(rects) {
   2082  await UpdateCurrentCanvasForInvalidation(rects);
   2083  SendUpdateCurrentCanvasWithSnapshotDone(true);
   2084 }
   2085 
   2086 async function RecvUpdateWholeCanvasForInvalidation() {
   2087  await UpdateWholeCurrentCanvasForInvalidation();
   2088  SendUpdateCurrentCanvasWithSnapshotDone(true);
   2089 }
   2090 
   2091 function OnProcessCrashed(subject, topic) {
   2092  let id;
   2093  let additionalDumps;
   2094  let propbag = subject.QueryInterface(Ci.nsIPropertyBag2);
   2095 
   2096  if (topic == "ipc:content-shutdown") {
   2097    id = propbag.get("dumpID");
   2098  }
   2099 
   2100  if (id) {
   2101    g.expectedCrashDumpFiles.push(id + ".dmp");
   2102    g.expectedCrashDumpFiles.push(id + ".extra");
   2103  }
   2104 
   2105  if (additionalDumps && additionalDumps.length) {
   2106    for (const name of additionalDumps.split(",")) {
   2107      g.expectedCrashDumpFiles.push(id + "-" + name + ".dmp");
   2108    }
   2109  }
   2110 }
   2111 
   2112 function RegisterProcessCrashObservers() {
   2113  Services.obs.addObserver(OnProcessCrashed, "ipc:content-shutdown");
   2114 }
   2115 
   2116 function RecvExpectProcessCrash() {
   2117  g.expectingProcessCrash = true;
   2118 }
   2119 
   2120 function SendClear() {
   2121  g.browserMessageManager.sendAsyncMessage("reftest:Clear");
   2122 }
   2123 
   2124 function SendLoadScriptTest(uri, timeout) {
   2125  g.browserMessageManager.sendAsyncMessage("reftest:LoadScriptTest", {
   2126    uri,
   2127    timeout,
   2128  });
   2129 }
   2130 
   2131 function SendLoadPrintTest(uri, timeout) {
   2132  g.browserMessageManager.sendAsyncMessage("reftest:LoadPrintTest", {
   2133    uri,
   2134    timeout,
   2135  });
   2136 }
   2137 
   2138 function SendLoadTest(type, uri, uriTargetType, timeout) {
   2139  g.browserMessageManager.sendAsyncMessage("reftest:LoadTest", {
   2140    type,
   2141    uri,
   2142    uriTargetType,
   2143    timeout,
   2144  });
   2145 }
   2146 
   2147 function SendResetRenderingState() {
   2148  g.browserMessageManager.sendAsyncMessage("reftest:ResetRenderingState");
   2149 }
   2150 
   2151 function SendPrintDone(status, fileName) {
   2152  g.browserMessageManager.sendAsyncMessage("reftest:PrintDone", {
   2153    status,
   2154    fileName,
   2155  });
   2156 }
   2157 
   2158 function SendUpdateCurrentCanvasWithSnapshotDone(painted) {
   2159  g.browserMessageManager.sendAsyncMessage(
   2160    "reftest:UpdateCanvasWithSnapshotDone",
   2161    { painted }
   2162  );
   2163 }
   2164 
   2165 var pdfjsHasLoaded;
   2166 
   2167 function pdfjsHasLoadedPromise() {
   2168  if (pdfjsHasLoaded === undefined) {
   2169    pdfjsHasLoaded = new Promise((resolve, reject) => {
   2170      let doc = g.containingWindow.document;
   2171      const script = doc.createElement("script");
   2172      script.type = "module";
   2173      script.src = "resource://pdf.js/build/pdf.mjs";
   2174      script.onload = resolve;
   2175      script.onerror = () => reject(new Error("PDF.js script load failed."));
   2176      doc.documentElement.appendChild(script);
   2177    });
   2178  }
   2179 
   2180  return pdfjsHasLoaded;
   2181 }
   2182 
   2183 function readPdf(path, callback) {
   2184  const win = g.containingWindow;
   2185 
   2186  IOUtils.read(path).then(
   2187    function (data) {
   2188      win.pdfjsLib.GlobalWorkerOptions.workerSrc =
   2189        "resource://pdf.js/build/pdf.worker.mjs";
   2190      win.pdfjsLib
   2191        .getDocument({
   2192          data,
   2193        })
   2194        .promise.then(
   2195          function (pdf) {
   2196            callback(null, pdf);
   2197          },
   2198          function (e) {
   2199            callback(new Error(`Couldn't parse ${path}, exception: ${e}`));
   2200          }
   2201        );
   2202    },
   2203    function (e) {
   2204      callback(new Error(`Couldn't read PDF ${path}, exception: ${e}`));
   2205    }
   2206  );
   2207 }
   2208 
   2209 function comparePdfs(pathToTestPdf, pathToRefPdf, callback) {
   2210  pdfjsHasLoadedPromise()
   2211    .then(() =>
   2212      Promise.all(
   2213        [pathToTestPdf, pathToRefPdf].map(function (path) {
   2214          return new Promise(function (resolve, reject) {
   2215            readPdf(path, function (error, pdf) {
   2216              // Resolve or reject outer promise. reject and resolve are
   2217              // passed to the callback function given as first arguments
   2218              // to the Promise constructor.
   2219              if (error) {
   2220                reject(error);
   2221              } else {
   2222                resolve(pdf);
   2223              }
   2224            });
   2225          });
   2226        })
   2227      )
   2228    )
   2229    .then(
   2230      function (pdfs) {
   2231        let numberOfPages = pdfs[1].numPages;
   2232        let sameNumberOfPages = numberOfPages === pdfs[0].numPages;
   2233 
   2234        let resultPromises = [
   2235          Promise.resolve({
   2236            passed: sameNumberOfPages,
   2237            description:
   2238              "Expected number of pages: " +
   2239              numberOfPages +
   2240              ", got " +
   2241              pdfs[0].numPages,
   2242          }),
   2243        ];
   2244 
   2245        if (sameNumberOfPages) {
   2246          for (let i = 0; i < numberOfPages; i++) {
   2247            let pageNum = i + 1;
   2248            let testPagePromise = pdfs[0].getPage(pageNum);
   2249            let refPagePromise = pdfs[1].getPage(pageNum);
   2250            resultPromises.push(
   2251              new Promise(function (resolve, reject) {
   2252                Promise.all([testPagePromise, refPagePromise]).then(function (
   2253                  pages
   2254                ) {
   2255                  let testTextPromise = pages[0].getTextContent();
   2256                  let refTextPromise = pages[1].getTextContent();
   2257                  Promise.all([testTextPromise, refTextPromise]).then(function (
   2258                    texts
   2259                  ) {
   2260                    let testTextItems = texts[0].items;
   2261                    let refTextItems = texts[1].items;
   2262                    let testText;
   2263                    let refText;
   2264                    let passed = refTextItems.every(function (o, index) {
   2265                      refText = o.str;
   2266                      if (!testTextItems[index]) {
   2267                        return false;
   2268                      }
   2269                      testText = testTextItems[index].str;
   2270                      return testText === refText;
   2271                    });
   2272                    let description;
   2273                    if (passed) {
   2274                      if (testTextItems.length > refTextItems.length) {
   2275                        passed = false;
   2276                        description =
   2277                          "Page " +
   2278                          pages[0].pageNumber +
   2279                          " contains unexpected text like '" +
   2280                          testTextItems[refTextItems.length].str +
   2281                          "'";
   2282                      } else {
   2283                        description =
   2284                          "Page " + pages[0].pageNumber + " contains same text";
   2285                      }
   2286                    } else {
   2287                      description =
   2288                        "Expected page " +
   2289                        pages[0].pageNumber +
   2290                        " to contain text '" +
   2291                        refText;
   2292                      if (testText) {
   2293                        description += "' but found '" + testText + "' instead";
   2294                      }
   2295                    }
   2296                    resolve({
   2297                      passed,
   2298                      description,
   2299                    });
   2300                  }, reject);
   2301                }, reject);
   2302              })
   2303            );
   2304          }
   2305        }
   2306 
   2307        Promise.all(resultPromises).then(function (results) {
   2308          callback(null, results);
   2309        });
   2310      },
   2311      function (error) {
   2312        callback(error);
   2313      }
   2314    );
   2315 }