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