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