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