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