[JAEGER] Merge from tracemonkey.
[mozilla-central.git] / layout / tools / reftest / reftest.js
blobb7d0f57beeebf9383187f08ec2d1186469591235
1 /* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- /
2 /* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */
3 /* ***** BEGIN LICENSE BLOCK *****
4  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
5  *
6  * The contents of this file are subject to the Mozilla Public License Version
7  * 1.1 (the "License"); you may not use this file except in compliance with
8  * the License. You may obtain a copy of the License at
9  * http://www.mozilla.org/MPL/
10  *
11  * Software distributed under the License is distributed on an "AS IS" basis,
12  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
13  * for the specific language governing rights and limitations under the
14  * License.
15  *
16  * The Original Code is Mozilla's layout acceptance tests.
17  *
18  * The Initial Developer of the Original Code is the Mozilla Foundation.
19  * Portions created by the Initial Developer are Copyright (C) 2006
20  * the Initial Developer. All Rights Reserved.
21  *
22  * Contributor(s):
23  *   L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
24  *
25  * Alternatively, the contents of this file may be used under the terms of
26  * either the GNU General Public License Version 2 or later (the "GPL"), or
27  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28  * in which case the provisions of the GPL or the LGPL are applicable instead
29  * of those above. If you wish to allow use of your version of this file only
30  * under the terms of either the GPL or the LGPL, and not to allow others to
31  * use your version of this file under the terms of the MPL, indicate your
32  * decision by deleting the provisions above and replace them with the notice
33  * and other provisions required by the GPL or the LGPL. If you do not delete
34  * the provisions above, a recipient may use your version of this file under
35  * the terms of any one of the MPL, the GPL or the LGPL.
36  *
37  * ***** END LICENSE BLOCK ***** */
39 const CC = Components.classes;
40 const CI = Components.interfaces;
41 const CR = Components.results;
43 const XHTML_NS = "http://www.w3.org/1999/xhtml";
45 const NS_LOCAL_FILE_CONTRACTID = "@mozilla.org/file/local;1";
46 const IO_SERVICE_CONTRACTID = "@mozilla.org/network/io-service;1";
47 const DEBUG_CONTRACTID = "@mozilla.org/xpcom/debug;1";
48 const NS_LOCALFILEINPUTSTREAM_CONTRACTID =
49           "@mozilla.org/network/file-input-stream;1";
50 const NS_SCRIPTSECURITYMANAGER_CONTRACTID =
51           "@mozilla.org/scriptsecuritymanager;1";
52 const NS_REFTESTHELPER_CONTRACTID =
53           "@mozilla.org/reftest-helper;1";
54 const NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX =
55           "@mozilla.org/network/protocol;1?name=";
56 const NS_XREAPPINFO_CONTRACTID =
57           "@mozilla.org/xre/app-info;1";
59 var gLoadTimeout = 0;
60 var gRemote = false;
61 var gTotalChunks = 0;
62 var gThisChunk = 0;
64 // "<!--CLEAR-->"
65 const BLANK_URL_FOR_CLEARING = "data:text/html,%3C%21%2D%2DCLEAR%2D%2D%3E";
67 var gBrowser;
68 var gCanvas1, gCanvas2;
69 // gCurrentCanvas is non-null between InitCurrentCanvasWithSnapshot and the next
70 // DocumentLoaded.
71 var gCurrentCanvas = null;
72 var gURLs;
73 // Map from URI spec to the number of times it remains to be used
74 var gURIUseCounts;
75 // Map from URI spec to the canvas rendered for that URI
76 var gURICanvases;
77 var gTestResults = {
78   // Successful...
79   Pass: 0,
80   LoadOnly: 0,
81   // Unexpected...
82   Exception: 0,
83   FailedLoad: 0,
84   UnexpectedFail: 0,
85   UnexpectedPass: 0,
86   AssertionUnexpected: 0,
87   AssertionUnexpectedFixed: 0,
88   // Known problems...
89   KnownFail : 0,
90   AssertionKnown: 0,
91   Random : 0,
92   Skip: 0,
94 var gTotalTests = 0;
95 var gState;
96 var gCurrentURL;
97 var gFailureTimeout = null;
98 var gFailureReason;
99 var gServer;
100 var gCount = 0;
101 var gAssertionCount = 0;
103 var gIOService;
104 var gDebug;
105 var gWindowUtils;
107 var gCurrentTestStartTime;
108 var gSlowestTestTime = 0;
109 var gSlowestTestURL;
110 var gClearingForAssertionCheck = false;
112 var gDrawWindowFlags;
114 const TYPE_REFTEST_EQUAL = '==';
115 const TYPE_REFTEST_NOTEQUAL = '!=';
116 const TYPE_LOAD = 'load';     // test without a reference (just test that it does
117                               // not assert, crash, hang, or leak)
118 const TYPE_SCRIPT = 'script'; // test contains individual test results
120 const EXPECTED_PASS = 0;
121 const EXPECTED_FAIL = 1;
122 const EXPECTED_RANDOM = 2;
123 const EXPECTED_DEATH = 3;  // test must be skipped to avoid e.g. crash/hang
125 const gProtocolRE = /^\w+:/;
127 var HTTP_SERVER_PORT = 4444;
128 const HTTP_SERVER_PORTS_TO_TRY = 50;
130 // whether we should skip caching canvases
131 var gNoCanvasCache = false;
133 var gRecycledCanvases = new Array();
135 function AllocateCanvas()
137     var windowElem = document.documentElement;
139     if (gRecycledCanvases.length > 0)
140         return gRecycledCanvases.shift();
142     var canvas = document.createElementNS(XHTML_NS, "canvas");
143     canvas.setAttribute("width", windowElem.getAttribute("width"));
144     canvas.setAttribute("height", windowElem.getAttribute("height"));
146     return canvas;
149 function ReleaseCanvas(canvas)
151     // store a maximum of 2 canvases, if we're not caching
152     if (!gNoCanvasCache || gRecycledCanvases.length < 2)
153         gRecycledCanvases.push(canvas);
156 function OnRefTestLoad()
158     gBrowser = document.getElementById("browser");
160     /* set the gLoadTimeout */
161     try {
162       var prefs = Components.classes["@mozilla.org/preferences-service;1"].
163                   getService(Components.interfaces.nsIPrefBranch2);
164       gLoadTimeout = prefs.getIntPref("reftest.timeout");
165       gRemote = prefs.getBoolPref("reftest.remote");
166     }
167     catch(e) {
168       gLoadTimeout = 5 * 60 * 1000; //5 minutes as per bug 479518
169     }
172     /* Support for running a chunk (subset) of tests.  In separate try as this is optional */
173     try {
174       gTotalChunks = prefs.getIntPref("reftest.totalChunks");
175       gThisChunk = prefs.getIntPref("reftest.thisChunk");
176     }
177     catch(e) {
178       gTotalChunks = 0;
179       gThisChunk = 0;
180     }
182     gBrowser.addEventListener("load", OnDocumentLoad, true);
184     try {
185         gWindowUtils = window.QueryInterface(CI.nsIInterfaceRequestor).getInterface(CI.nsIDOMWindowUtils);
186         if (gWindowUtils && !gWindowUtils.compareCanvases)
187             gWindowUtils = null;
188     } catch (e) {
189         gWindowUtils = null;
190     }
192     var windowElem = document.documentElement;
194     gIOService = CC[IO_SERVICE_CONTRACTID].getService(CI.nsIIOService);
195     gDebug = CC[DEBUG_CONTRACTID].getService(CI.nsIDebug2);
196     
197     if (gRemote) {
198       gServer = null;
199     } else {
200       gServer = CC["@mozilla.org/server/jshttp;1"].
201                     createInstance(CI.nsIHttpServer);
202     }
203     try {
204         if (gServer)
205             StartHTTPServer();
206     } catch (ex) {
207         //gBrowser.loadURI('data:text/plain,' + ex);
208         ++gTestResults.Exception;
209         dump("REFTEST TEST-UNEXPECTED-FAIL | | EXCEPTION: " + ex + "\n");
210         DoneTests();
211     }
213     // Focus the content browser
214     gBrowser.focus();
216     StartTests();
219 function StartHTTPServer()
221     gServer.registerContentType("sjs", "sjs");
222     // We want to try different ports in case the port we want
223     // is being used.
224     var tries = HTTP_SERVER_PORTS_TO_TRY;
225     do {
226         try {
227             gServer.start(HTTP_SERVER_PORT);
228             return;
229         } catch (ex) {
230             ++HTTP_SERVER_PORT;
231             if (--tries == 0)
232                 throw ex;
233         }
234     } while (true);
237 function StartTests()
239     try {
240         // Need to read the manifest once we have the final HTTP_SERVER_PORT.
241         var args = window.arguments[0].wrappedJSObject;
243         if ("nocache" in args && args["nocache"])
244             gNoCanvasCache = true;
246         ReadTopManifest(args.uri);
247         BuildUseCounts();
249         if (gTotalChunks > 0 && gThisChunk > 0) {
250           var testsPerChunk = gURLs.length / gTotalChunks;
251           var start = Math.round((gThisChunk-1) * testsPerChunk);
252           var end = Math.round(gThisChunk * testsPerChunk);
253           gURLs = gURLs.slice(start, end);
254           dump("REFTEST INFO | Running chunk " + gThisChunk + " out of " + gTotalChunks + " chunks.  ")
255           dump("tests " + (start+1) + "-" + end + "/" + gURLs.length + "\n");
256         }
257         gTotalTests = gURLs.length;
259         if (!gTotalTests)
260             throw "No tests to run";
262         gURICanvases = {};
263         StartCurrentTest();
264     } catch (ex) {
265         //gBrowser.loadURI('data:text/plain,' + ex);
266         ++gTestResults.Exception;
267         dump("REFTEST TEST-UNEXPECTED-FAIL | | EXCEPTION: " + ex + "\n");
268         DoneTests();
269     }
272 function OnRefTestUnload()
274     /* Clear the sRGB forcing pref to leave the profile as we found it. */
275     var prefs = Components.classes["@mozilla.org/preferences-service;1"].
276                 getService(Components.interfaces.nsIPrefBranch2);
277     prefs.clearUserPref("gfx.color_management.force_srgb");
279     gBrowser.removeEventListener("load", OnDocumentLoad, true);
282 // Read all available data from an input stream and return it
283 // as a string.
284 function getStreamContent(inputStream)
286   var streamBuf = "";
287   var sis = CC["@mozilla.org/scriptableinputstream;1"].
288                 createInstance(CI.nsIScriptableInputStream);
289   sis.init(inputStream);
291   var available;
292   while ((available = sis.available()) != 0) {
293     streamBuf += sis.read(available);
294   }
295   
296   return streamBuf;
299 // Build the sandbox for fails-if(), etc., condition evaluation.
300 function BuildConditionSandbox(aURL) {
301     var sandbox = new Components.utils.Sandbox(aURL.spec);
302     var xr = CC[NS_XREAPPINFO_CONTRACTID].getService(CI.nsIXULRuntime);
303     sandbox.isDebugBuild = gDebug.isDebugBuild;
304     sandbox.xulRuntime = {widgetToolkit: xr.widgetToolkit, OS: xr.OS};
306     // xr.XPCOMABI throws exception for configurations without full ABI
307     // support (mobile builds on ARM)
308     try {
309       sandbox.xulRuntime.XPCOMABI = xr.XPCOMABI;
310     } catch(e) {
311       sandbox.xulRuntime.XPCOMABI = "";
312     }
314     // Backwards compatibility from when we preprocessed autoconf.mk.
315     sandbox.MOZ_WIDGET_TOOLKIT = xr.widgetToolkit;
317     // Shortcuts for widget toolkits.
318     sandbox.cocoaWidget = xr.widgetToolkit == "cocoa";
319     sandbox.gtk2Widget = xr.widgetToolkit == "gtk2";
320     sandbox.qtWidget = xr.widgetToolkit == "qt";
321     sandbox.winWidget = xr.widgetToolkit == "windows";
323     var hh = CC[NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX + "http"].
324                  getService(CI.nsIHttpProtocolHandler);
325     sandbox.http = {};
326     for each (var prop in [ "userAgent", "appName", "appVersion",
327                             "vendor", "vendorSub", "vendorComment",
328                             "product", "productSub", "productComment",
329                             "platform", "oscpu", "language", "misc" ])
330         sandbox.http[prop] = hh[prop];
331     // see if we have the test plugin available,
332     // and set a sandox prop accordingly
333     sandbox.haveTestPlugin = false;
334     for (var i = 0; i < navigator.mimeTypes.length; i++) {
335         if (navigator.mimeTypes[i].type == "application/x-test" &&
336             navigator.mimeTypes[i].enabledPlugin != null &&
337             navigator.mimeTypes[i].enabledPlugin.name == "Test Plug-in") {
338             sandbox.haveTestPlugin = true;
339             break;
340         }
341     }
343     // Set a flag on sandbox if the windows default theme is active
344     var box = document.createElement("box");
345     box.setAttribute("id", "_box_windowsDefaultTheme");
346     document.documentElement.appendChild(box);
347     sandbox.windowsDefaultTheme = (getComputedStyle(box, null).display == "none");
348     document.documentElement.removeChild(box);
350     var prefs = CC["@mozilla.org/preferences-service;1"].
351                 getService(CI.nsIPrefBranch2);
352     try {
353         sandbox.nativeThemePref = !prefs.getBoolPref("mozilla.widget.disable-native-theme");
354     } catch (e) {
355         sandbox.nativeThemePref = true;
356     }
358     new XPCSafeJSObjectWrapper(sandbox).prefs = {
359       __exposedProps__: {
360         getBoolPref: 'r',
361         getIntPref: 'r',
362       },
363       _prefs:      prefs,
364       getBoolPref: function(p) { return this._prefs.getBoolPref(p); },
365       getIntPref:  function(p) { return this._prefs.getIntPref(p); }
366     }
368     dump("REFTEST INFO | Dumping JSON representation of sandbox \n");
369     dump("REFTEST INFO | " + JSON.stringify(sandbox) + " \n");
371     return sandbox;
374 function ReadTopManifest(aFileURL)
376     gURLs = new Array();
377     var url = gIOService.newURI(aFileURL, null, null);
378     if (!url)
379       throw "Expected a file or http URL for the manifest.";
380     ReadManifest(url);
383 // Note: If you materially change the reftest manifest parsing,
384 // please keep the parser in print-manifest-dirs.py in sync.
385 function ReadManifest(aURL)
387     var secMan = CC[NS_SCRIPTSECURITYMANAGER_CONTRACTID]
388                      .getService(CI.nsIScriptSecurityManager);
390     var listURL = aURL;
391     var channel = gIOService.newChannelFromURI(aURL);
392     var inputStream = channel.open();
393     if (channel instanceof Components.interfaces.nsIHttpChannel
394         && channel.responseStatus != 200) {
395       dump("REFTEST TEST-UNEXPECTED-FAIL | | HTTP ERROR : " + 
396         channel.responseStatus + "\n");
397     }
398     var streamBuf = getStreamContent(inputStream);
399     inputStream.close();
400     var lines = streamBuf.split(/(\n|\r|\r\n)/);
402     // Build the sandbox for fails-if(), etc., condition evaluation.
403     var sandbox = BuildConditionSandbox(aURL);
405     var lineNo = 0;
406     var urlprefix = "";
407     for each (var str in lines) {
408         ++lineNo;
409         if (str.charAt(0) == "#")
410             continue; // entire line was a comment
411         var i = str.search(/\s+#/);
412         if (i >= 0)
413             str = str.substring(0, i);
414         // strip leading and trailing whitespace
415         str = str.replace(/^\s*/, '').replace(/\s*$/, '');
416         if (!str || str == "")
417             continue;
418         var items = str.split(/\s+/); // split on whitespace
420         if (items[0] == "url-prefix") {
421             if (items.length != 2)
422                 throw "url-prefix requires one url in manifest file " + aURL.spec + " line " + lineNo;
423             urlprefix = items[1];
424             continue;
425         }
427         var expected_status = EXPECTED_PASS;
428         var minAsserts = 0;
429         var maxAsserts = 0;
430         while (items[0].match(/^(fails|random|skip|asserts)/)) {
431             var item = items.shift();
432             var stat;
433             var cond;
434             var m = item.match(/^(fails|random|skip)-if(\(.*\))$/);
435             if (m) {
436                 stat = m[1];
437                 // Note: m[2] contains the parentheses, and we want them.
438                 cond = Components.utils.evalInSandbox(m[2], sandbox);
439             } else if (item.match(/^(fails|random|skip)$/)) {
440                 stat = item;
441                 cond = true;
442             } else if ((m = item.match(/^asserts\((\d+)(-\d+)?\)$/))) {
443                 cond = false;
444                 minAsserts = Number(m[1]);
445                 maxAsserts = (m[2] == undefined) ? minAsserts
446                                                  : Number(m[2].substring(1));
447             } else if ((m = item.match(/^asserts-if\((.*?),(\d+)(-\d+)?\)$/))) {
448                 cond = false;
449                 if (Components.utils.evalInSandbox("(" + m[1] + ")", sandbox)) {
450                     minAsserts = Number(m[2]);
451                     maxAsserts =
452                       (m[3] == undefined) ? minAsserts
453                                           : Number(m[3].substring(1));
454                 }
455             } else {
456                 throw "Error 1 in manifest file " + aURL.spec + " line " + lineNo;
457             }
459             if (cond) {
460                 if (stat == "fails") {
461                     expected_status = EXPECTED_FAIL;
462                 } else if (stat == "random") {
463                     expected_status = EXPECTED_RANDOM;
464                 } else if (stat == "skip") {
465                     expected_status = EXPECTED_DEATH;
466                 }
467             }
468         }
470         if (minAsserts > maxAsserts) {
471             throw "Bad range in manifest file " + aURL.spec + " line " + lineNo;
472         }
474         var runHttp = false;
475         var httpDepth;
476         if (items[0] == "HTTP") {
477             runHttp = (aURL.scheme == "file"); // We can't yet run the local HTTP server
478                                                // for non-local reftests.
479             httpDepth = 0;
480             items.shift();
481         } else if (items[0].match(/HTTP\(\.\.(\/\.\.)*\)/)) {
482             // Accept HTTP(..), HTTP(../..), HTTP(../../..), etc.
483             runHttp = (aURL.scheme == "file"); // We can't yet run the local HTTP server
484                                                // for non-local reftests.
485             httpDepth = (items[0].length - 5) / 3;
486             items.shift();
487         }
489         // do not prefix the url for include commands or urls specifying
490         // a protocol
491         if (urlprefix && items[0] != "include") {
492             if (items.length > 1 && !items[1].match(gProtocolRE)) {
493                 items[1] = urlprefix + items[1];
494             }
495             if (items.length > 2 && !items[2].match(gProtocolRE)) {
496                 items[2] = urlprefix + items[2];
497             }
498         }
500         if (items[0] == "include") {
501             if (items.length != 2 || runHttp)
502                 throw "Error 2 in manifest file " + aURL.spec + " line " + lineNo;
503             var incURI = gIOService.newURI(items[1], null, listURL);
504             secMan.checkLoadURI(aURL, incURI,
505                                 CI.nsIScriptSecurityManager.DISALLOW_SCRIPT);
506             ReadManifest(incURI);
507         } else if (items[0] == TYPE_LOAD) {
508             if (items.length != 2 ||
509                 (expected_status != EXPECTED_PASS &&
510                  expected_status != EXPECTED_DEATH))
511                 throw "Error 3 in manifest file " + aURL.spec + " line " + lineNo;
512             var [testURI] = runHttp
513                             ? ServeFiles(aURL, httpDepth,
514                                          listURL, [items[1]])
515                             : [gIOService.newURI(items[1], null, listURL)];
516             var prettyPath = runHttp
517                            ? gIOService.newURI(items[1], null, listURL).spec
518                            : testURI.spec;
519             secMan.checkLoadURI(aURL, testURI,
520                                 CI.nsIScriptSecurityManager.DISALLOW_SCRIPT);
521             gURLs.push( { type: TYPE_LOAD,
522                           expected: expected_status,
523                           prettyPath: prettyPath,
524                           minAsserts: minAsserts,
525                           maxAsserts: maxAsserts,
526                           url1: testURI,
527                           url2: null } );
528         } else if (items[0] == TYPE_SCRIPT) {
529             if (items.length != 2)
530                 throw "Error 4 in manifest file " + aURL.spec + " line " + lineNo;
531             var [testURI] = runHttp
532                             ? ServeFiles(aURL, httpDepth,
533                                          listURL, [items[1]])
534                             : [gIOService.newURI(items[1], null, listURL)];
535             var prettyPath = runHttp
536                            ? gIOService.newURI(items[1], null, listURL).spec
537                            : testURI.spec;
538             secMan.checkLoadURI(aURL, testURI,
539                                 CI.nsIScriptSecurityManager.DISALLOW_SCRIPT);
540             gURLs.push( { type: TYPE_SCRIPT,
541                           expected: expected_status,
542                           prettyPath: prettyPath,
543                           minAsserts: minAsserts,
544                           maxAsserts: maxAsserts,
545                           url1: testURI,
546                           url2: null } );
547         } else if (items[0] == TYPE_REFTEST_EQUAL || items[0] == TYPE_REFTEST_NOTEQUAL) {
548             if (items.length != 3)
549                 throw "Error 5 in manifest file " + aURL.spec + " line " + lineNo;
550             var [testURI, refURI] = runHttp
551                                   ? ServeFiles(aURL, httpDepth,
552                                                listURL, [items[1], items[2]])
553                                   : [gIOService.newURI(items[1], null, listURL),
554                                      gIOService.newURI(items[2], null, listURL)];
555             var prettyPath = runHttp
556                            ? gIOService.newURI(items[1], null, listURL).spec
557                            : testURI.spec;
558             secMan.checkLoadURI(aURL, testURI,
559                                 CI.nsIScriptSecurityManager.DISALLOW_SCRIPT);
560             secMan.checkLoadURI(aURL, refURI,
561                                 CI.nsIScriptSecurityManager.DISALLOW_SCRIPT);
562             gURLs.push( { type: items[0],
563                           expected: expected_status,
564                           prettyPath: prettyPath,
565                           minAsserts: minAsserts,
566                           maxAsserts: maxAsserts,
567                           url1: testURI,
568                           url2: refURI } );
569         } else {
570             throw "Error 6 in manifest file " + aURL.spec + " line " + lineNo;
571         }
572     }
575 function AddURIUseCount(uri)
577     if (uri == null)
578         return;
580     var spec = uri.spec;
581     if (spec in gURIUseCounts) {
582         gURIUseCounts[spec]++;
583     } else {
584         gURIUseCounts[spec] = 1;
585     }
588 function BuildUseCounts()
590     gURIUseCounts = {};
591     for (var i = 0; i < gURLs.length; ++i) {
592         var url = gURLs[i];
593         if (url.expected != EXPECTED_DEATH &&
594             (url.type == TYPE_REFTEST_EQUAL ||
595              url.type == TYPE_REFTEST_NOTEQUAL)) {
596             AddURIUseCount(gURLs[i].url1);
597             AddURIUseCount(gURLs[i].url2);
598         }
599     }
602 function ServeFiles(manifestURL, depth, aURL, files)
604     var listURL = aURL.QueryInterface(CI.nsIFileURL);
605     var directory = listURL.file.parent;
607     // Allow serving a tree that's an ancestor of the directory containing
608     // the files so that they can use resources in ../ (etc.).
609     var dirPath = "/";
610     while (depth > 0) {
611         dirPath = "/" + directory.leafName + dirPath;
612         directory = directory.parent;
613         --depth;
614     }
616     gCount++;
617     var path = "/" + Date.now() + "/" + gCount;
618     gServer.registerDirectory(path + "/", directory);
620     var secMan = CC[NS_SCRIPTSECURITYMANAGER_CONTRACTID]
621                      .getService(CI.nsIScriptSecurityManager);
623     var testbase = gIOService.newURI("http://localhost:" + HTTP_SERVER_PORT +
624                                          path + dirPath,
625                                      null, null);
627     function FileToURI(file)
628     {
629         // Only serve relative URIs via the HTTP server, not absolute
630         // ones like about:blank.
631         var testURI = gIOService.newURI(file, null, testbase);
633         // XXX necessary?  manifestURL guaranteed to be file, others always HTTP
634         secMan.checkLoadURI(manifestURL, testURI,
635                             CI.nsIScriptSecurityManager.DISALLOW_SCRIPT);
637         return testURI;
638     }
640     return files.map(FileToURI);
643 function StartCurrentTest()
645     // make sure we don't run tests that are expected to kill the browser
646     while (gURLs.length > 0 && gURLs[0].expected == EXPECTED_DEATH) {
647         ++gTestResults.Skip;
648         dump("REFTEST TEST-KNOWN-FAIL | " + gURLs[0].url1.spec + " | (SKIP)\n");
649         gURLs.shift();
650     }
652     if (gURLs.length == 0) {
653         DoneTests();
654     }
655     else {
656         var currentTest = gTotalTests - gURLs.length;
657         document.title = "reftest: " + currentTest + " / " + gTotalTests +
658             " (" + Math.floor(100 * (currentTest / gTotalTests)) + "%)";
659         StartCurrentURI(1);
660     }
663 function StartCurrentURI(aState)
665     gCurrentTestStartTime = Date.now();
666     if (gFailureTimeout != null) {
667         dump("REFTEST TEST-UNEXPECTED-FAIL | " +
668              "| program error managing timeouts\n");
669         ++gTestResults.Exception;
670     }
671     gFailureTimeout = setTimeout(LoadFailed, gLoadTimeout);
672     gFailureReason = "timed out waiting for onload to fire";
674     gState = aState;
675     gCurrentURL = gURLs[0]["url" + aState].spec;
677     if (gURICanvases[gCurrentURL] &&
678         (gURLs[0].type == TYPE_REFTEST_EQUAL ||
679          gURLs[0].type == TYPE_REFTEST_NOTEQUAL) &&
680         gURLs[0].maxAsserts == 0) {
681         // Pretend the document loaded --- DocumentLoaded will notice
682         // there's already a canvas for this URL
683         setTimeout(DocumentLoaded, 0);
684     } else {
685         dump("REFTEST TEST-START | " + gCurrentURL + "\n");
686         gBrowser.loadURI(gCurrentURL);
687     }
690 function DoneTests()
692     dump("REFTEST FINISHED: Slowest test took " + gSlowestTestTime +
693          "ms (" + gSlowestTestURL + ")\n");
695     dump("REFTEST INFO | Result summary:\n");
696     var count = gTestResults.Pass + gTestResults.LoadOnly;
697     dump("REFTEST INFO | Successful: " + count + " (" +
698          gTestResults.Pass + " pass, " +
699          gTestResults.LoadOnly + " load only)\n");
700     count = gTestResults.Exception + gTestResults.FailedLoad +
701             gTestResults.UnexpectedFail + gTestResults.UnexpectedPass +
702             gTestResults.AssertionUnexpected +
703             gTestResults.AssertionUnexpectedFixed;
704     dump("REFTEST INFO | Unexpected: " + count + " (" +
705          gTestResults.UnexpectedFail + " unexpected fail, " +
706          gTestResults.UnexpectedPass + " unexpected pass, " +
707          gTestResults.AssertionUnexpected + " unexpected asserts, " +
708          gTestResults.AssertionUnexpectedFixed + " unexpected fixed asserts, " +
709          gTestResults.FailedLoad + " failed load, " +
710          gTestResults.Exception + " exception)\n");
711     count = gTestResults.KnownFail + gTestResults.AssertionKnown +
712             gTestResults.Random + gTestResults.Skip;
713     dump("REFTEST INFO | Known problems: " + count + " (" +
714          gTestResults.KnownFail + " known fail, " +
715          gTestResults.AssertionKnown + " known asserts, " +
716          gTestResults.Random + " random, " +
717          gTestResults.Skip + " skipped)\n");
719     dump("REFTEST INFO | Total canvas count = " + gRecycledCanvases.length + "\n");
721     dump("REFTEST TEST-START | Shutdown\n");
722     function onStopped() {
723         goQuitApplication();
724     }
725     if (gServer)
726         gServer.stop(onStopped);
727     else
728         onStopped();
731 function setupZoom(contentRootElement) {
732     if (!contentRootElement || !contentRootElement.hasAttribute('reftest-zoom'))
733         return;
734     gBrowser.markupDocumentViewer.fullZoom =
735         contentRootElement.getAttribute('reftest-zoom');
738 function resetZoom() {
739     gBrowser.markupDocumentViewer.fullZoom = 1.0;
742 function OnDocumentLoad(event)
744     if (event.target != gBrowser.contentDocument)
745         // Ignore load events for subframes.
746         return;
748     if (gClearingForAssertionCheck &&
749         gBrowser.contentDocument.location.href == BLANK_URL_FOR_CLEARING) {
750         DoAssertionCheck();
751         return;
752     }
754     if (gBrowser.contentDocument.location.href != gCurrentURL)
755         // Ignore load events for previous documents.
756         return;
758     var contentRootElement = gBrowser.contentDocument.documentElement;
760     function shouldWait() {
761         // use getAttribute because className works differently in HTML and SVG
762         return contentRootElement &&
763                contentRootElement.hasAttribute('class') &&
764                contentRootElement.getAttribute('class').split(/\s+/)
765                                  .indexOf("reftest-wait") != -1;
766     }
768     function doPrintMode() {
769         // use getAttribute because className works differently in HTML and SVG
770         return contentRootElement &&
771                contentRootElement.hasAttribute('class') &&
772                contentRootElement.getAttribute('class').split(/\s+/)
773                                  .indexOf("reftest-print") != -1;
774     }
776     function setupPrintMode() {
777        var PSSVC = Components.classes["@mozilla.org/gfx/printsettings-service;1"]
778                   .getService(Components.interfaces.nsIPrintSettingsService);
779        var ps = PSSVC.newPrintSettings;
780        ps.paperWidth = 5;
781        ps.paperHeight = 3;
783        // Override any os-specific unwriteable margins
784        ps.unwriteableMarginTop = 0;
785        ps.unwriteableMarginLeft = 0;
786        ps.unwriteableMarginBottom = 0;
787        ps.unwriteableMarginRight = 0;
789        ps.headerStrLeft = "";
790        ps.headerStrCenter = "";
791        ps.headerStrRight = "";
792        ps.footerStrLeft = "";
793        ps.footerStrCenter = "";
794        ps.footerStrRight = "";
795        gBrowser.docShell.contentViewer.setPageMode(true, ps);
796     }
798     setupZoom(contentRootElement);
800     if (shouldWait()) {
801         // The testcase will let us know when the test snapshot should be made.
802         // Register a mutation listener to know when the 'reftest-wait' class
803         // gets removed.
804         gFailureReason = "timed out waiting for reftest-wait to be removed (after onload fired)"
806         var stopAfterPaintReceived = false;
807         var currentDoc = gBrowser.contentDocument;
808         var utils = gBrowser.contentWindow.QueryInterface(CI.nsIInterfaceRequestor)
809             .getInterface(CI.nsIDOMWindowUtils);
811         function FlushRendering() {
812             // Flush pending restyles and reflows
813             contentRootElement.getBoundingClientRect();
814             // Flush out invalidation
815             utils.processUpdates();
816         }
818         function WhenMozAfterPaintFlushed(continuation) {
819             if (utils.isMozAfterPaintPending) {
820                 function handler() {
821                     gBrowser.removeEventListener("MozAfterPaint", handler, false);
822                     continuation();
823                 }
824                 gBrowser.addEventListener("MozAfterPaint", handler, false);
825             } else {
826                 continuation();
827             }
828         }
830         function AfterPaintListener(event) {
831             if (event.target.document != currentDoc) {
832                 // ignore paint events for subframes or old documents in the window.
833                 // Invalidation in subframes will cause invalidation in the main document anyway.
834                 return;
835             }
837             FlushRendering();
838             UpdateCurrentCanvasForEvent(event);
839             // When stopAfteraintReceived is set, we can stop --- but we should keep going as long
840             // as there are paint events coming (there probably shouldn't be any, but it doesn't
841             // hurt to process them)
842             if (stopAfterPaintReceived && !utils.isMozAfterPaintPending) {
843                 FinishWaitingForTestEnd();
844             }
845         }
847         function FinishWaitingForTestEnd() {
848             gBrowser.removeEventListener("MozAfterPaint", AfterPaintListener, false);
849             setTimeout(DocumentLoaded, 0);
850         }
852         function AttrModifiedListener() {
853             if (shouldWait())
854                 return;
856             // We don't want to be notified again
857             contentRootElement.removeEventListener("DOMAttrModified", AttrModifiedListener, false);
858             // Wait for the next return-to-event-loop before continuing to flush rendering and
859             // check isMozAfterPaintPending --- for example, the attribute may have been modified
860             // in an subdocument's load event handler, in which case we need load event processing
861             // to complete and unsuppress painting before we check isMozAfterPaintPending.
862             setTimeout(AttrModifiedListenerContinuation, 0);
863         }
865         function AttrModifiedListenerContinuation() {
866             if (doPrintMode())
867                 setupPrintMode();
868             FlushRendering();
870             if (utils.isMozAfterPaintPending) {
871                 // Wait for the last invalidation to have happened and been snapshotted before
872                 // we stop the test
873                 stopAfterPaintReceived = true;
874             } else {
875                 // Nothing to wait for, so stop now
876                 FinishWaitingForTestEnd();
877             }
878         }
880         function StartWaitingForTestEnd() {
881             FlushRendering();
883             function continuation() {
884                 gBrowser.addEventListener("MozAfterPaint", AfterPaintListener, false);
885                 contentRootElement.addEventListener("DOMAttrModified", AttrModifiedListener, false);
887                 // Take a snapshot of the window in its current state
888                 InitCurrentCanvasWithSnapshot();
890                 if (!shouldWait()) {
891                     // reftest-wait was already removed (during the interval between OnDocumentLoaded
892                     // calling setTimeout(StartWaitingForTestEnd,0) below, and this function
893                     // actually running), so let's fake a direct notification of the attribute
894                     // change.
895                     AttrModifiedListener();
896                     return;
897                 }
899                 // Notify the test document that now is a good time to test some invalidation
900                 var notification = document.createEvent("Events");
901                 notification.initEvent("MozReftestInvalidate", true, false);
902                 contentRootElement.dispatchEvent(notification);
903             }
904             WhenMozAfterPaintFlushed(continuation);
905         }
907         // After this load event has finished being dispatched, painting is normally
908         // unsuppressed, which invalidates the entire window. So ensure
909         // StartWaitingForTestEnd runs after that invalidation has been requested.
910         setTimeout(StartWaitingForTestEnd, 0);
911     } else {
912         if (doPrintMode())
913             setupPrintMode();
915         // Since we can't use a bubbling-phase load listener from chrome,
916         // this is a capturing phase listener.  So do setTimeout twice, the
917         // first to get us after the onload has fired in the content, and
918         // the second to get us after any setTimeout(foo, 0) in the content.
919         setTimeout(setTimeout, 0, DocumentLoaded, 0);
920     }
923 function UpdateCanvasCache(url, canvas)
925     var spec = url.spec;
927     --gURIUseCounts[spec];
929     if (gNoCanvasCache || gURIUseCounts[spec] == 0) {
930         ReleaseCanvas(canvas);
931         delete gURICanvases[spec];
932     } else if (gURIUseCounts[spec] > 0) {
933         gURICanvases[spec] = canvas;
934     } else {
935         throw "Use counts were computed incorrectly";
936     }
939 // Compute drawWindow flags lazily so the window is set up and can be
940 // measured accurately
941 function DoDrawWindow(ctx, win, x, y, w, h)
943     if (typeof gDrawWindowFlags == "undefined") {
944         gDrawWindowFlags = ctx.DRAWWINDOW_DRAW_CARET |
945                            ctx.DRAWWINDOW_DRAW_VIEW;
946         var flags = "DRAWWINDOW_DRAW_CARET | DRAWWINDOW_DRAW_VIEW";
947         if (window.innerWidth == gCurrentCanvas.width &&
948             window.innerHeight == gCurrentCanvas.height) {
949             // We can use the window's retained layers
950             // because the window is big enough to display the entire reftest
951             gDrawWindowFlags |= ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
952             flags += " | DRAWWINDOW_USE_WIDGET_LAYERS";
953         }
954         dump("REFTEST INFO | drawWindow flags = " + flags + "\n");
955     }
957     var scrollX = 0;
958     var scrollY = 0;
959     if (!(gDrawWindowFlags & ctx.DRAWWINDOW_DRAW_VIEW)) {
960         scrollX = win.scrollX;
961         scrollY = win.scrollY;
962     }
963     ctx.drawWindow(win, scrollX + x, scrollY + y, w, h, "rgb(255,255,255)",
964                    gDrawWindowFlags);
967 function InitCurrentCanvasWithSnapshot()
969     if (gURLs[0].type == TYPE_LOAD || gURLs[0].type == TYPE_SCRIPT) {
970         // We don't want to snapshot this kind of test
971         return;
972     }
974     gCurrentCanvas = AllocateCanvas();
976     /* XXX This needs to be rgb(255,255,255) because otherwise we get
977      * black bars at the bottom of every test that are different size
978      * for the first test and the rest (scrollbar-related??) */
979     var win = gBrowser.contentWindow;
980     var ctx = gCurrentCanvas.getContext("2d");
981     var scale = gBrowser.markupDocumentViewer.fullZoom;
982     ctx.save();
983     // drawWindow always draws one canvas pixel for each CSS pixel in the source
984     // window, so scale the drawing to show the zoom (making each canvas pixel be one
985     // device pixel instead)
986     ctx.scale(scale, scale);
987     DoDrawWindow(ctx, win, 0, 0,
988                  Math.ceil(gCurrentCanvas.width / scale),
989                  Math.ceil(gCurrentCanvas.height / scale));
990     ctx.restore();
993 function roundTo(x, fraction)
995     return Math.round(x/fraction)*fraction;
998 function UpdateCurrentCanvasForEvent(event)
1000     if (!gCurrentCanvas)
1001         return;
1003     var win = gBrowser.contentWindow;
1004     var ctx = gCurrentCanvas.getContext("2d");
1005     var scale = gBrowser.markupDocumentViewer.fullZoom;
1007     var rectList = event.clientRects;
1008     for (var i = 0; i < rectList.length; ++i) {
1009         var r = rectList[i];
1010         // Set left/top/right/bottom to "device pixel" boundaries
1011         var left = Math.floor(roundTo(r.left*scale, 0.001))/scale;
1012         var top = Math.floor(roundTo(r.top*scale, 0.001))/scale;
1013         var right = Math.ceil(roundTo(r.right*scale, 0.001))/scale;
1014         var bottom = Math.ceil(roundTo(r.bottom*scale, 0.001))/scale;
1016         ctx.save();
1017         ctx.scale(scale, scale);
1018         ctx.translate(left, top);
1019         DoDrawWindow(ctx, win, left, top, right - left, bottom - top);
1020         ctx.restore();
1021     }
1024 function DocumentLoaded()
1026     // Keep track of which test was slowest, and how long it took.
1027     var currentTestRunTime = Date.now() - gCurrentTestStartTime;
1028     if (currentTestRunTime > gSlowestTestTime) {
1029         gSlowestTestTime = currentTestRunTime;
1030         gSlowestTestURL  = gCurrentURL;
1031     }
1033     clearTimeout(gFailureTimeout);
1034     gFailureReason = null;
1035     gFailureTimeout = null;
1037     // Not 'const ...' because of 'EXPECTED_*' value dependency.
1038     var outputs = {};
1039     const randomMsg = "(EXPECTED RANDOM)";
1040     outputs[EXPECTED_PASS] = {
1041         true:  {s: "TEST-PASS"                  , n: "Pass"},
1042         false: {s: "TEST-UNEXPECTED-FAIL"       , n: "UnexpectedFail"}
1043     };
1044     outputs[EXPECTED_FAIL] = {
1045         true:  {s: "TEST-UNEXPECTED-PASS"       , n: "UnexpectedPass"},
1046         false: {s: "TEST-KNOWN-FAIL"            , n: "KnownFail"}
1047     };
1048     outputs[EXPECTED_RANDOM] = {
1049         true:  {s: "TEST-PASS" + randomMsg      , n: "Random"},
1050         false: {s: "TEST-KNOWN-FAIL" + randomMsg, n: "Random"}
1051     };
1052     var output;
1054     if (gURLs[0].type == TYPE_LOAD) {
1055         ++gTestResults.LoadOnly;
1056         dump("REFTEST TEST-PASS | " + gURLs[0].prettyPath + " | (LOAD ONLY)\n");
1057         gCurrentCanvas = null;
1058         FinishTestItem();
1059         return;
1060     }
1061     if (gURLs[0].type == TYPE_SCRIPT) {
1062         var missing_msg = false;
1063         var testwindow = gBrowser.contentWindow;
1064         expected = gURLs[0].expected;
1066         if (testwindow.wrappedJSObject)
1067             testwindow = testwindow.wrappedJSObject;
1069         var testcases;
1071         if (!testwindow.getTestCases || typeof testwindow.getTestCases != "function") {
1072             // Force an unexpected failure to alert the test author to fix the test.
1073             expected = EXPECTED_PASS;
1074             missing_msg = "test must provide a function getTestCases(). (SCRIPT)\n";
1075         }
1076         else if (!(testcases = testwindow.getTestCases())) {
1077             // Force an unexpected failure to alert the test author to fix the test.
1078             expected = EXPECTED_PASS;
1079             missing_msg = "test's getTestCases() must return an Array-like Object. (SCRIPT)\n";
1080         }
1081         else if (testcases.length == 0) {
1082             // This failure may be due to a JavaScript Engine bug causing
1083             // early termination of the test.
1084             missing_msg = "No test results reported. (SCRIPT)\n";
1085         }
1087         if (missing_msg) {
1088             output = outputs[expected][false];
1089             ++gTestResults[output.n];
1090             var result = "REFTEST " + output.s + " | " +
1091                 gURLs[0].prettyPath + " | " + // the URL being tested
1092                 missing_msg;
1094             dump(result);
1095             FinishTestItem();
1096             return;
1097         }
1099         var results = testcases.map(function(test) {
1100                 return { passed: test.testPassed(), description: test.testDescription()};
1101             });
1102         var anyFailed = results.some(function(result) { return !result.passed; });
1103         var outputPair;
1104         if (anyFailed && expected == EXPECTED_FAIL) {
1105             // If we're marked as expected to fail, and some (but not all) tests
1106             // passed, treat those tests as though they were marked random
1107             // (since we can't tell whether they were really intended to be
1108             // marked failing or not).
1109             outputPair = { true: outputs[EXPECTED_RANDOM][true],
1110                            false: outputs[expected][false] };
1111         } else {
1112             outputPair = outputs[expected];
1113         }
1114         var index = 0;
1115         results.forEach(function(result) {
1116                 var output = outputPair[result.passed];
1118                 ++gTestResults[output.n];
1119                 result = "REFTEST " + output.s + " | " +
1120                     gURLs[0].prettyPath + " | " + // the URL being tested
1121                     result.description + " item " + (++index) + "\n";
1122                 dump(result);
1123             });
1125         FinishTestItem();
1126         return;
1127     }
1129     if (gURICanvases[gCurrentURL]) {
1130         gCurrentCanvas = gURICanvases[gCurrentURL];
1131     } else if (gCurrentCanvas == null) {
1132         InitCurrentCanvasWithSnapshot();
1133     }
1134     if (gState == 1) {
1135         gCanvas1 = gCurrentCanvas;
1136     } else {
1137         gCanvas2 = gCurrentCanvas;
1138     }
1139     gCurrentCanvas = null;
1141     resetZoom();
1143     switch (gState) {
1144         case 1:
1145             // First document has been loaded.
1146             // Proceed to load the second document.
1148             StartCurrentURI(2);
1149             break;
1150         case 2:
1151             // Both documents have been loaded. Compare the renderings and see
1152             // if the comparison result matches the expected result specified
1153             // in the manifest.
1155             // number of different pixels
1156             var differences;
1157             // whether the two renderings match:
1158             var equal;
1160             if (gWindowUtils) {
1161                 differences = gWindowUtils.compareCanvases(gCanvas1, gCanvas2, {});
1162                 equal = (differences == 0);
1163             } else {
1164                 differences = -1;
1165                 var k1 = gCanvas1.toDataURL();
1166                 var k2 = gCanvas2.toDataURL();
1167                 equal = (k1 == k2);
1168             }
1170             // whether the comparison result matches what is in the manifest
1171             var test_passed = (equal == (gURLs[0].type == TYPE_REFTEST_EQUAL));
1172             // what is expected on this platform (PASS, FAIL, or RANDOM)
1173             var expected = gURLs[0].expected;
1174             output = outputs[expected][test_passed];
1176             ++gTestResults[output.n];
1178             var result = "REFTEST " + output.s + " | " +
1179                          gURLs[0].prettyPath + " | "; // the URL being tested
1180             if (gURLs[0].type == TYPE_REFTEST_NOTEQUAL) {
1181                 result += "(!=) ";
1182             }
1183             dump(result + "\n");
1185             if (!test_passed && expected == EXPECTED_PASS ||
1186                 test_passed && expected == EXPECTED_FAIL) {
1187                 if (!equal) {
1188                     dump("REFTEST   IMAGE 1 (TEST): " + gCanvas1.toDataURL() + "\n");
1189                     dump("REFTEST   IMAGE 2 (REFERENCE): " + gCanvas2.toDataURL() + "\n");
1190                     dump("REFTEST number of differing pixels: " + differences + "\n");
1191                 } else {
1192                     dump("REFTEST   IMAGE: " + gCanvas1.toDataURL() + "\n");
1193                 }
1194             }
1196             UpdateCanvasCache(gURLs[0].url1, gCanvas1);
1197             UpdateCanvasCache(gURLs[0].url2, gCanvas2);
1199             FinishTestItem();
1200             break;
1201         default:
1202             throw "Unexpected state.";
1203     }
1206 function LoadFailed()
1208     gFailureTimeout = null;
1209     ++gTestResults.FailedLoad;
1210     dump("REFTEST TEST-UNEXPECTED-FAIL | " +
1211          gURLs[0]["url" + gState].spec + " | " + gFailureReason + "\n");
1212     FinishTestItem();
1215 function FinishTestItem()
1217     // Replace document with BLANK_URL_FOR_CLEARING in case there are
1218     // assertions when unloading.
1219     dump("REFTEST INFO | Loading a blank page\n");
1220     gClearingForAssertionCheck = true;
1221     gBrowser.loadURI(BLANK_URL_FOR_CLEARING);
1224 function DoAssertionCheck()
1226     gClearingForAssertionCheck = false;
1228     if (gDebug.isDebugBuild) {
1229         var newAssertionCount = gDebug.assertionCount;
1230         var numAsserts = newAssertionCount - gAssertionCount;
1231         gAssertionCount = newAssertionCount;
1233         var minAsserts = gURLs[0].minAsserts;
1234         var maxAsserts = gURLs[0].maxAsserts;
1236         var expectedAssertions = "expected " + minAsserts;
1237         if (minAsserts != maxAsserts) {
1238             expectedAssertions += " to " + maxAsserts;
1239         }
1240         expectedAssertions += " assertions";
1242         if (numAsserts < minAsserts) {
1243             ++gTestResults.AssertionUnexpectedFixed;
1244             dump("REFTEST TEST-UNEXPECTED-PASS | " + gURLs[0].prettyPath +
1245                  " | assertion count " + numAsserts + " is less than " +
1246                  expectedAssertions + "\n");
1247         } else if (numAsserts > maxAsserts) {
1248             ++gTestResults.AssertionUnexpected;
1249             dump("REFTEST TEST-UNEXPECTED-FAIL | " + gURLs[0].prettyPath +
1250                  " | assertion count " + numAsserts + " is more than " +
1251                  expectedAssertions + "\n");
1252         } else if (numAsserts != 0) {
1253             ++gTestResults.AssertionKnown;
1254             dump("REFTEST TEST-KNOWN-FAIL | " + gURLs[0].prettyPath +
1255                  " | assertion count " + numAsserts + " matches " +
1256                  expectedAssertions + "\n");
1257         }
1258     }
1260     // And start the next test.
1261     gURLs.shift();
1262     StartCurrentTest();