tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

reftest-analyzer.xhtml (45611B)


      1 <?xml version="1.0" encoding="UTF-8"?>
      2 <!-- This Source Code Form is subject to the terms of the Mozilla Public
      3   - License, v. 2.0. If a copy of the MPL was not distributed with this
      4   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
      5 <!--
      6 
      7 Features to add:
      8 * make the left and right parts of the viewer independently scrollable
      9 * make the test list filterable
     10 ** default to only showing unexpecteds
     11 * add other ways to highlight differences other than circling?
     12 * add zoom/pan to images
     13 * Add ability to load log via XMLHttpRequest (also triggered via URL param)
     14 * color the test list based on pass/fail and expected/unexpected/random/skip
     15 * ability to load multiple logs ?
     16 ** rename them by clicking on the name and editing
     17 ** turn the test list into a collapsing tree view
     18 ** move log loading into popup from viewer UI
     19 
     20 -->
     21 <!DOCTYPE html>
     22 <html lang="en-US" xml:lang="en-US" xmlns="http://www.w3.org/1999/xhtml">
     23  <head>
     24    <title>Reftest analyzer</title>
     25    <style type="text/css">
     26      <![CDATA[
     27 
     28        html, body { margin: 0; }
     29        html { padding: 0; }
     30        body { padding: 4px; }
     31 
     32        #pixelarea, #itemlist, #images { position: absolute; }
     33        #itemlist, #images { overflow: auto; }
     34        #pixelarea { top: 0; left: 0; width: 320px; height: 84px; overflow: visible }
     35        #itemlist { top: 84px; left: 0; width: 320px; bottom: 0; }
     36        #images { top: 0; bottom: 0; left: 320px; right: 0; }
     37 
     38        #leftpane { width: 320px; }
     39        #images { position: fixed; top: 10px; left: 340px; }
     40 
     41        form#imgcontrols { margin: 0; display: block; }
     42 
     43        #itemlist > table { border-collapse: collapse; }
     44        #itemlist > table > tbody > tr > td { border: 1px solid; padding: 1px; }
     45        #itemlist td.activeitem { background-color: yellow; }
     46 
     47        /*
     48        #itemlist > table > tbody > tr.pass > td.url { background: lime; }
     49        #itemlist > table > tbody > tr.fail > td.url { background: red; }
     50        */
     51 
     52        #magnification > svg { display: block; width: 84px; height: 84px; }
     53 
     54        #pixelinfo { font: small sans-serif; position: absolute; width: 200px; left: 84px; }
     55        #pixelinfo table { border-collapse: collapse; }
     56        #pixelinfo table th { white-space: nowrap; text-align: left; padding: 0; }
     57        #pixelinfo table td { font-family: monospace; padding: 0 0 0 0.25em; }
     58 
     59        #pixelhint { display: inline; color: #88f; cursor: help; }
     60        #pixelhint > * { display: none; position: absolute; margin: 8px 0 0 8px; padding: 4px; width: 400px; background: #ffa; color: black; box-shadow: 3px 3px 2px #888; z-index: 1; }
     61        #pixelhint:hover { color: #000; }
     62        #pixelhint:hover > * { display: block; }
     63        #pixelhint p { margin: 0; }
     64        #pixelhint p + p { margin-top: 1em; }
     65 
     66        ]]>
     67    </style>
     68    <script type="text/javascript">
     69      <![CDATA[
     70 
     71      var XLINK_NS = "http://www.w3.org/1999/xlink";
     72      var SVG_NS = "http://www.w3.org/2000/svg";
     73      var IMAGE_NOT_AVAILABLE = "data:image/png;base64,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";
     74 
     75      var gPhases = null;
     76 
     77      var gIDCache = {};
     78 
     79      var gMagPixPaths = [];     // 2D array of array-of-two <path> objects used in the pixel magnifier
     80      var gMagWidth = 5;         // number of zoomed in pixels to show horizontally
     81      var gMagHeight = 5;        // number of zoomed in pixels to show vertically
     82      var gMagZoom = 16;         // size of the zoomed in pixels
     83      var gImage1Data;           // ImageData object for the reference image
     84      var gImage2Data;           // ImageData object for the test output image
     85      var gFlashingPixels = [];  // array of <path> objects that should be flashed due to pixel color mismatch
     86      var gParams;
     87 
     88      function ID(id) {
     89        if (!(id in gIDCache))
     90          gIDCache[id] = document.getElementById(id);
     91        return gIDCache[id];
     92      }
     93 
     94      function hash_parameters() {
     95        var result = { };
     96        var params = window.location.hash.substr(1).split(/[&;]/);
     97        for (var i = 0; i < params.length; i++) {
     98          var parts = params[i].split("=");
     99          result[parts[0]] = unescape(unescape(parts[1]));
    100        }
    101        return result;
    102      }
    103 
    104      function load() {
    105        gPhases = [ ID("entry"), ID("loading"), ID("viewer") ];
    106        build_mag();
    107        gParams = hash_parameters();
    108        if (gParams.log) {
    109          show_phase("loading");
    110          process_log(gParams.log);
    111        } else if (gParams.logurl) {
    112          show_phase("loading");
    113          var req = new XMLHttpRequest();
    114          req.onreadystatechange = function() {
    115            if (req.readyState === 4) {
    116              process_log(req.responseText);
    117            }
    118          };
    119          req.open('GET', gParams.logurl, true);
    120          req.send();
    121        }
    122        window.addEventListener('keypress', handle_keyboard_shortcut);
    123        window.addEventListener('keydown',  handle_keydown);
    124        ID("image1").addEventListener('error', image_load_error);
    125        ID("image2").addEventListener('error', image_load_error);
    126      }
    127 
    128      function image_load_error(e) {
    129        e.target.setAttributeNS(XLINK_NS, "xlink:href", IMAGE_NOT_AVAILABLE);
    130      }
    131 
    132      function build_mag() {
    133        var mag = ID("mag");
    134 
    135        var r = document.createElementNS(SVG_NS, "rect");
    136        r.setAttribute("x", gMagZoom * -gMagWidth / 2);
    137        r.setAttribute("y", gMagZoom * -gMagHeight / 2);
    138        r.setAttribute("width", gMagZoom * gMagWidth);
    139        r.setAttribute("height", gMagZoom * gMagHeight);
    140        mag.appendChild(r);
    141 
    142        mag.setAttribute("transform", "translate(" + (gMagZoom * (gMagWidth / 2) + 1) + "," + (gMagZoom * (gMagHeight / 2) + 1) + ")");
    143 
    144        for (var x = 0; x < gMagWidth; x++) {
    145          gMagPixPaths[x] = [];
    146          for (var y = 0; y < gMagHeight; y++) {
    147            var p1 = document.createElementNS(SVG_NS, "path");
    148            p1.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "h" + -gMagZoom + "v" + gMagZoom);
    149            p1.setAttribute("stroke", "black");
    150            p1.setAttribute("stroke-width", "1px");
    151            p1.setAttribute("fill", "#aaa");
    152 
    153            var p2 = document.createElementNS(SVG_NS, "path");
    154            p2.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "v" + gMagZoom + "h" + -gMagZoom);
    155            p2.setAttribute("stroke", "black");
    156            p2.setAttribute("stroke-width", "1px");
    157            p2.setAttribute("fill", "#888");
    158 
    159            mag.appendChild(p1);
    160            mag.appendChild(p2);
    161            gMagPixPaths[x][y] = [p1, p2];
    162          }
    163        }
    164 
    165        var flashedOn = false;
    166        setInterval(function() {
    167          flashedOn = !flashedOn;
    168          flash_pixels(flashedOn);
    169        }, 500);
    170      }
    171 
    172      function show_phase(phaseid) {
    173        for (var i in gPhases) {
    174          var phase = gPhases[i];
    175          phase.style.display = (phase.id == phaseid) ? "" : "none";
    176        }
    177 
    178        if (phase == "viewer")
    179          ID("images").style.display = "none";
    180      }
    181 
    182      function fileentry_changed() {
    183        show_phase("loading");
    184        var input = ID("fileentry");
    185        var files = input.files;
    186        if (files.length) {
    187          // Only handle the first file; don't handle multiple selection.
    188          // The parts of the log we care about are ASCII-only.  Since we
    189          // can ignore lines we don't care about, best to read in as
    190          // iso-8859-1, which guarantees we don't get decoding errors.
    191          var fileReader = new FileReader();
    192          fileReader.onload = function(e) {
    193            var log = null;
    194 
    195            log = e.target.result;
    196 
    197            if (log)
    198              process_log(log);
    199            else
    200              show_phase("entry");
    201          }
    202          fileReader.readAsText(files[0], "iso-8859-1");
    203        }
    204        // So the user can process the same filename again (after
    205        // overwriting the log), clear the value on the form input so we
    206        // will always get an onchange event.
    207        input.value = "";
    208      }
    209 
    210      function log_pasted() {
    211        show_phase("loading");
    212        var entry = ID("logentry");
    213        var log = entry.value;
    214        entry.value = "";
    215        process_log(log);
    216      }
    217 
    218      var gTestItems;
    219 
    220      // This function is not used in production code, but can be invoked manually
    221      // from the devtools console in order to test changes to the parsing regexes
    222      // in process_log.
    223      function test_parsing() {
    224        // Note that the logs in these testcases have been manually edited to strip
    225        // out stuff for brevity.
    226        var testcases = [
    227          { "name": "empty log",
    228            "log": "",
    229            "expected": { "pass": 0, "unexpected": 0, "random": 0, "skip": 0 },
    230            "expected_images": 0,
    231          },
    232          { "name": "android log",
    233            "log": `[task 2018-12-28T10:36:45.718Z] 10:36:45     INFO -  REFTEST TEST-START | a == b
    234      [task 2018-12-28T10:36:45.719Z] 10:36:45     INFO -  REFTEST TEST-LOAD | a | 78 / 275 (28%)
    235      [task 2018-12-28T10:36:56.138Z] 10:36:56     INFO -  REFTEST TEST-LOAD | b | 78 / 275 (28%)
    236      [task 2018-12-28T10:37:06.559Z] 10:37:06     INFO -  REFTEST TEST-UNEXPECTED-FAIL | a == b | image comparison, max difference: 255, number of differing pixels: 5950
    237      [task 2018-12-28T10:37:06.568Z] 10:37:06     INFO -  REFTEST   IMAGE 1 (TEST): data:image/png;base64,
    238      [task 2018-12-28T10:37:06.577Z] 10:37:06     INFO -  REFTEST   IMAGE 2 (REFERENCE): data:image/png;base64,
    239      [task 2018-12-28T10:37:06.577Z] 10:37:06     INFO -  REFTEST INFO | Saved log: stuff trimmed here
    240      [task 2018-12-28T10:37:06.582Z] 10:37:06     INFO -  REFTEST TEST-END | a == b
    241      [task 2018-12-28T10:37:06.583Z] 10:37:06     INFO -  REFTEST TEST-START | a2 == b2
    242      [task 2018-12-28T10:37:06.583Z] 10:37:06     INFO -  REFTEST TEST-LOAD | a2 | 79 / 275 (28%)
    243      [task 2018-12-28T10:37:06.584Z] 10:37:06     INFO -  REFTEST TEST-LOAD | b2 | 79 / 275 (28%)
    244      [task 2018-12-28T10:37:16.982Z] 10:37:16     INFO -  REFTEST TEST-PASS | a2 == b2 | image comparison, max difference: 0, number of differing pixels: 0
    245      [task 2018-12-28T10:37:16.982Z] 10:37:16     INFO -  REFTEST TEST-END | a2 == b2`,
    246            "expected": { "pass": 1, "unexpected": 1, "random": 0, "skip": 0 },
    247            "expected_images": 2,
    248          },
    249          { "name": "local reftest run (Linux)",
    250            "log": `REFTEST TEST-START | file:///a == file:///b
    251      REFTEST TEST-LOAD | file:///a | 73 / 86 (84%)
    252      REFTEST TEST-LOAD | file:///b | 73 / 86 (84%)
    253      REFTEST TEST-PASS | file:///a == file:///b | image comparison, max difference: 0, number of differing pixels: 0
    254      REFTEST TEST-END | file:///a == file:///b`,
    255            "expected": { "pass": 1, "unexpected": 0, "random": 0, "skip": 0 },
    256            "expected_images": 0,
    257          },
    258          { "name": "wpt reftests (Linux automation)",
    259            "log": `16:50:43     INFO - TEST-START | /a
    260      16:50:43     INFO - PID 4276 | 1548694243694	Marionette	INFO	Testing http://web-platform.test:8000/a == http://web-platform.test:8000/b
    261      16:50:43     INFO - PID 4276 | 1548694243963	Marionette	INFO	No differences allowed
    262      16:50:44     INFO - TEST-PASS | /a | took 370ms
    263      16:50:44     INFO - TEST-START | /a2
    264      16:50:44     INFO - PID 4276 | 1548694244066	Marionette	INFO	Testing http://web-platform.test:8000/a2 == http://web-platform.test:8000/b2
    265      16:50:44     INFO - PID 4276 | 1548694244792	Marionette	INFO	No differences allowed
    266      16:50:44     INFO - PID 4276 | 1548694244792	Marionette	INFO	Found 28 pixels different, maximum difference per channel 14
    267      16:50:44     INFO - TEST-UNEXPECTED-FAIL | /a2 | Testing http://web-platform.test:8000/a2 == http://web-platform.test:8000/b2
    268      16:50:44     INFO - REFTEST   IMAGE 1 (TEST): data:image/png;base64,
    269      16:50:44     INFO - REFTEST   IMAGE 2 (REFERENCE): data:image/png;base64,
    270      16:50:44     INFO - TEST-INFO took 840ms`,
    271            "expected": { "pass": 1, "unexpected": 1, "random": 0, "skip": 0 },
    272            "expected_images": 2,
    273          },
    274          { "name": "windows log",
    275            "log": `12:17:14     INFO - REFTEST TEST-START | a == b
    276      12:17:14     INFO - REFTEST TEST-LOAD | a | 1603 / 2053 (78%)
    277      12:17:14     INFO - REFTEST TEST-LOAD | b | 1603 / 2053 (78%)
    278      12:17:14     INFO - REFTEST TEST-PASS(EXPECTED RANDOM) | a == b | image comparison, max difference: 0, number of differing pixels: 0
    279      12:17:14     INFO - REFTEST TEST-END | a == b
    280      12:17:14     INFO - REFTEST TEST-START | a2 == b2
    281      12:17:14     INFO - REFTEST TEST-LOAD | a2 | 1604 / 2053 (78%)
    282      12:17:14     INFO - REFTEST TEST-LOAD | b2 | 1604 / 2053 (78%)
    283      12:17:14     INFO - REFTEST TEST-UNEXPECTED-FAIL | a2 == b2 | image comparison, max difference: 255, number of differing pixels: 9976
    284      12:17:14     INFO - REFTEST   IMAGE 1 (TEST): data:image/png;base64,
    285      12:17:14     INFO - REFTEST   IMAGE 2 (REFERENCE): data:image/png;base64,
    286      12:17:14     INFO - REFTEST INFO | Saved log: stuff trimmed here
    287      12:17:14     INFO - REFTEST TEST-END | a2 == b2
    288      12:01:09     INFO - REFTEST TEST-START | a3 == b3
    289      12:01:09     INFO - REFTEST TEST-LOAD | a3 | 66 / 189 (34%)
    290      12:01:09     INFO - REFTEST TEST-LOAD | b3 | 66 / 189 (34%)
    291      12:01:09     INFO - REFTEST TEST-KNOWN-FAIL | a3 == b3 | image comparison, max difference: 255, number of differing pixels: 9654
    292      12:01:09     INFO - REFTEST TEST-END | a3 == b3`,
    293            "expected": { "pass": 1, "unexpected": 1, "random": 1, "skip": 0 },
    294            "expected_images": 2,
    295          },
    296          { "name": "webrender wrench log (windows)",
    297            "log": `[task 2018-12-29T04:29:48.800Z] REFTEST a == b
    298      [task 2018-12-29T04:29:48.984Z] REFTEST a2 == b2
    299      [task 2018-12-29T04:29:49.053Z] REFTEST TEST-UNEXPECTED-FAIL | a2 == b2 | image comparison, max difference: 255, number of differing pixels: 3128
    300      [task 2018-12-29T04:29:49.053Z] REFTEST   IMAGE 1 (TEST): data:image/png;
    301      [task 2018-12-29T04:29:49.053Z] REFTEST   IMAGE 2 (REFERENCE): data:image/png;
    302      [task 2018-12-29T04:29:49.053Z] REFTEST TEST-END | a2 == b2`,
    303            "expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
    304            "expected_images": 2,
    305          },
    306          { "name": "wpt reftests (Linux local; Bug 1530008)",
    307            "log": `SUITE-START | Running 1 tests
    308      TEST-START | /css/css-backgrounds/border-image-6.html
    309      TEST-UNEXPECTED-FAIL | /css/css-backgrounds/border-image-6.html | Testing http://web-platform.test:8000/css/css-backgrounds/border-image-6.html == http://web-platform.test:8000/css/css-backgrounds/border-image-6-ref.html
    310      REFTEST   IMAGE 1 (TEST): data:image/png;base64,
    311      REFTEST   IMAGE 2 (REFERENCE): data:image/png;base64,
    312      TEST-INFO took 425ms
    313      SUITE-END | took 2s`,
    314            "expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
    315            "expected_images": 2,
    316          },
    317          { "name": "wpt reftests (taskcluster log from macOS CI)",
    318            "log": `[task 2020-06-26T01:35:29.065Z] 01:35:29     INFO - TEST-START | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html
    319      [task 2020-06-26T01:35:29.065Z] 01:35:29     INFO - PID 1353 | 1593135329040    Marionette  INFO    Testing http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html == http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values-ref.html
    320      [task 2020-06-26T01:35:29.673Z] 01:35:29     INFO - PID 1353 | 1593135329633    Marionette  INFO    No differences allowed
    321      [task 2020-06-26T01:35:29.726Z] 01:35:29     INFO - TEST-KNOWN-INTERMITTENT-FAIL | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html | took 649ms
    322      [task 2020-06-26T01:35:29.726Z] 01:35:29     INFO - REFTEST   IMAGE 1 (TEST): data:image/png;
    323      [task 2020-06-26T01:35:29.726Z] 01:35:29     INFO - REFTEST   IMAGE 2 (REFERENCE): data:image/png;`,
    324            "expected": { "pass": 0, "unexpected": 0, "random": 1, "skip": 0 },
    325            "expected_images": 2,
    326          },
    327          { "name": "wpt reftests (taskcluster log from Windows CI)",
    328            "log": `[task 2020-06-26T01:41:19.205Z] 01:41:19     INFO - TEST-START | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html
    329      [task 2020-06-26T01:41:19.214Z] 01:41:19     INFO - PID 5920 | 1593135679202    Marionette  WARN    [24] http://web-platform.test:8000/css/WOFF2/metadatadisplay-schema-license-022-ref.xht overflows viewport (width: 783, height: 731)
    330      [task 2020-06-26T01:41:19.214Z] 01:41:19     INFO - PID 9692 | 1593135679208    Marionette  INFO    Testing http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html == http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values-ref.html
    331      [task 2020-06-26T01:41:19.638Z] 01:41:19     INFO - PID 9692 | 1593135679627    Marionette  INFO    No differences allowed
    332      [task 2020-06-26T01:41:19.688Z] 01:41:19     INFO - TEST-KNOWN-INTERMITTENT-PASS | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html | took 474ms
    333      [task 2020-06-26T01:41:19.688Z] 01:41:19     INFO - REFTEST   IMAGE 1 (TEST): data:image/png;
    334      [task 2020-06-26T01:41:19.689Z] 01:41:19     INFO - REFTEST   IMAGE 2 (REFERENCE): data:image/png;`,
    335            "expected": { "pass": 1, "unexpected": 0, "random": 1, "skip": 0 },
    336            "expected_images": 2,
    337          },
    338          { "name": "local reftest run with timestamps (Linux; Bug 1167712)",
    339            "log": ` 0:05.21 REFTEST TEST-START | a
    340       0:05.21 REFTEST REFTEST TEST-LOAD | a | 0 / 1 (0%)
    341       0:05.27 REFTEST REFTEST TEST-LOAD | b | 0 / 1 (0%)
    342       0:05.66 REFTEST TEST-UNEXPECTED-FAIL | a | image comparison (==), max difference: 106, number of differing pixels: 800
    343       0:05.67 REFTEST REFTEST   IMAGE 1 (TEST): data:image/png;base64,
    344       0:05.67 REFTEST REFTEST   IMAGE 2 (REFERENCE): data:image/png;base64,
    345       0:05.73 REFTEST REFTEST TEST-END | a`,
    346            "expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
    347            "expected_images": 2,
    348          },
    349          { "name": "reftest run with whitespace compressed (Treeherder; Bug 1084322)",
    350            "log": ` REFTEST TEST-START | a
    351      REFTEST TEST-LOAD | a | 0 / 1 (0%)
    352      REFTEST TEST-LOAD | b | 0 / 1 (0%)
    353      REFTEST TEST-UNEXPECTED-FAIL | a | image comparison (==), max difference: 106, number of differing pixels: 800
    354      REFTEST REFTEST IMAGE 1 (TEST): data:image/png;base64,
    355      REFTEST REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
    356      REFTEST REFTEST TEST-END | a`,
    357            "expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
    358            "expected_images": 2,
    359          },
    360        ];
    361 
    362        var current_test = 0;
    363 
    364        // Override the build_viewer function invoked at the end of process_log to
    365        // actually just check the results of parsing.
    366        // eslint-disable-next-line no-func-assign
    367        build_viewer = function() {
    368          var expected = testcases[current_test].expected;
    369          var expected_images = testcases[current_test].expected_images;
    370          for (let result of gTestItems) {
    371            for (let type in expected) { // type is "pass", "unexpected" etc.
    372              if (result[type]) {
    373                expected[type]--;
    374              }
    375            }
    376          }
    377          var failed = false;
    378          for (let type in expected) {
    379            if (expected[type] != 0) {
    380              console.log(`Failure: for testcase ${testcases[current_test].name} got ${expected[type]} fewer ${type} results than expected!`);
    381              failed = true;
    382            }
    383          }
    384 
    385          let total_images = 0;
    386          for (let result of gTestItems) {
    387            total_images += result.images.length;
    388          }
    389          if (total_images !== expected_images) {
    390            console.log(`Failure: for testcase ${testcases[current_test].name} got ${total_images} images, expected ${expected_images}`);
    391            failed = true;
    392          }
    393 
    394          if (!failed) {
    395            console.log(`Success for testcase ${testcases[current_test].name}`);
    396          }
    397        };
    398 
    399        while (current_test < testcases.length) {
    400          process_log(testcases[current_test].log);
    401          current_test++;
    402        }
    403      }
    404 
    405      function process_log(contents) {
    406        var lines = contents.split(/[\r\n]+/);
    407        gTestItems = [];
    408        for (var j in lines) {
    409 
    410          // !!!!!!
    411          // When making any changes to this code, please add a test to the
    412          // test_parsing function above, and ensure all existing tests pass.
    413          // !!!!!!
    414 
    415          var line = lines[j];
    416          // Ignore duplicated output in logcat.
    417          if (line.match(/I\/Gecko.*?REFTEST/))
    418            continue;
    419          var match = line.match(/^.*?(?:REFTEST\s+)+(.*)$/);
    420          if (!match) {
    421            // WPT reftests don't always have the "REFTEST" prefix but do have
    422            // mozharness prefixing. Trying to match both prefixes optionally with a
    423            // single regex either makes an unreadable mess or matches everything so
    424            // we do them separately.
    425            match = line.match(/^(?:.*? (?:INFO|ERROR) -\s+)(.*)$/);
    426          }
    427          if (match)
    428            line = match[1];
    429          match = line.match(/^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-FAIL|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL|TEST-DEBUG-INFO|TEST-KNOWN-INTERMITTENT-FAIL|TEST-KNOWN-INTERMITTENT-PASS)(\(EXPECTED RANDOM\)|) \| ([^\|]+)(?: \|(.*)|$)/);
    430          if (match) {
    431            var state = match[1];
    432            var random = match[2];
    433            var url = match[3];
    434            var extra = match[4];
    435            gTestItems.push(
    436              {
    437                pass: !state.match(/DEBUG-INFO$|FAIL$/),
    438                // only one of the following three should ever be true
    439                unexpected: !!state.match(/^TEST-UNEXPECTED/),
    440                random: (random == "(EXPECTED RANDOM)" || state == "TEST-KNOWN-INTERMITTENT-FAIL" || state == "TEST-KNOWN-INTERMITTENT-PASS"),
    441                skip: (extra == " (SKIP)"),
    442                url,
    443                images: [],
    444                imageLabels: []
    445              });
    446            continue;
    447          }
    448          match = line.match(/^IMAGE([^:]*): (data:.*)$/);
    449          if (match) {
    450            var item = gTestItems[gTestItems.length - 1];
    451            item.images.push(match[2]);
    452            item.imageLabels.push(match[1]);
    453          }
    454        }
    455 
    456        build_viewer();
    457      }
    458 
    459      function build_viewer() {
    460        if (!gTestItems.length) {
    461          show_phase("entry");
    462          return;
    463        }
    464 
    465        var cell = ID("itemlist");
    466        while (cell.childNodes.length)
    467          cell.removeChild(cell.childNodes[cell.childNodes.length - 1]);
    468 
    469        var table = document.createElement("table");
    470        var tbody = document.createElement("tbody");
    471        table.appendChild(tbody);
    472 
    473        for (var i in gTestItems) {
    474          var item = gTestItems[i];
    475 
    476          // optional url filter for only showing unexpected results
    477          if (parseInt(gParams.only_show_unexpected) && !item.unexpected)
    478            continue;
    479 
    480          // XXX regardless skip expected pass items until we have filtering UI
    481          if (item.pass && !item.unexpected)
    482            continue;
    483 
    484          var tr = document.createElement("tr");
    485          var td;
    486          var text;
    487 
    488          td = document.createElement("td");
    489          text = "";
    490          if (item.unexpected) { text += "!"; }
    491          if (item.random) { text += "R"; }
    492          if (item.skip) { text += "S"; }
    493          td.appendChild(document.createTextNode(text));
    494          tr.appendChild(td);
    495 
    496          td = document.createElement("td");
    497          td.id = "item" + i;
    498          td.className = "url";
    499          // Only display part of URL after "/mozilla/".
    500          var match = item.url.match(/\/mozilla\/(.*)/);
    501          text = document.createTextNode(match ? match[1] : item.url);
    502          if (item.images.length) {
    503            var a = document.createElement("a");
    504            a.href = "javascript:show_images(" + i + ")";
    505            a.appendChild(text);
    506            td.appendChild(a);
    507          } else {
    508            td.appendChild(text);
    509          }
    510          tr.appendChild(td);
    511 
    512          tbody.appendChild(tr);
    513        }
    514 
    515        cell.appendChild(table);
    516 
    517        show_phase("viewer");
    518      }
    519 
    520      function get_image_data(src, whenReady) {
    521        var img = new Image();
    522        img.onload = function() {
    523          var canvas = document.createElement("canvas");
    524          canvas.width = img.naturalWidth;
    525          canvas.height = img.naturalHeight;
    526 
    527          var ctx = canvas.getContext("2d");
    528          ctx.drawImage(img, 0, 0);
    529 
    530          whenReady(ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight));
    531        };
    532        img.src = src;
    533      }
    534 
    535      function sync_svg_size(imageData) {
    536        // We need the size of the 'svg' and its 'image' elements to match the size
    537        // of the ImageData objects that we're going to read pixels from or else our
    538        // magnify() function will be very broken.
    539        ID("svg").setAttribute("width", imageData.width);
    540        ID("svg").setAttribute("height", imageData.height);
    541      }
    542 
    543      function show_images(i) {
    544        var item = gTestItems[i];
    545        var cell = ID("images");
    546 
    547        // Remove activeitem class from any existing elements
    548        var activeItems = document.querySelectorAll(".activeitem");
    549        for (var activeItemIdx = activeItems.length; activeItemIdx-- != 0;) {
    550          activeItems[activeItemIdx].classList.remove("activeitem");
    551        }
    552 
    553        ID("item" + i).classList.add("activeitem");
    554        ID("image1").style.display = "";
    555        ID("image2").style.display = "none";
    556        ID("diffrect").style.display = "none";
    557        ID("imgcontrols").reset();
    558        ID("pixel-differences").textContent = "";
    559 
    560        ID("image1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
    561        // Making the href be #image1 doesn't seem to work
    562        ID("feimage1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
    563        if (item.images.length == 1) {
    564          ID("imgcontrols").style.display = "none";
    565        } else {
    566          ID("imgcontrols").style.display = "";
    567 
    568          ID("image2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
    569          // Making the href be #image2 doesn't seem to work
    570          ID("feimage2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
    571 
    572          ID("label1").textContent = 'Image ' + item.imageLabels[0];
    573          ID("label2").textContent = 'Image ' + item.imageLabels[1];
    574        }
    575 
    576        cell.style.display = "";
    577 
    578        let loaded = [false, false];
    579 
    580        function images_loaded(id) {
    581          loaded[id] = true;
    582          if (loaded.every(x => x)) {
    583            update_pixel_difference_text()
    584          }
    585        }
    586 
    587        get_image_data(item.images[0], function(data) { gImage1Data = data; sync_svg_size(gImage1Data); images_loaded(0)});
    588        get_image_data(item.images[1], function(data) { gImage2Data = data; images_loaded(1)});
    589 
    590      }
    591 
    592      function update_pixel_difference_text() {
    593        let differenceText;
    594        if (gImage1Data.height !== gImage2Data.height ||
    595            gImage1Data.width !== gImage2Data.width) {
    596          differenceText = "Images are different sizes"
    597        } else {
    598          let [numPixels, maxPerChannel] = get_pixel_differences();
    599          if (!numPixels) {
    600            differenceText = "Images are identical";
    601          } else {
    602            differenceText = `Maximum difference per channel ${maxPerChannel}, ${numPixels} pixels differ`;
    603          }
    604        }
    605        // Disable this for now, because per bug 1633504, the numbers may be
    606        // inaccurate and dependent on the browser's configuration.
    607        ID("pixel-differences").textContent = differenceText;
    608      }
    609 
    610      function get_pixel_differences() {
    611        let numPixels = 0;
    612        let maxPerChannel = 0;
    613        for (var i=0; i<gImage1Data.data.length; i+=4) {
    614          let r1 = gImage1Data.data[i];
    615          let r2 = gImage2Data.data[i];
    616          let g1 = gImage1Data.data[i+1];
    617          let g2 = gImage2Data.data[i+1];
    618          let b1 = gImage1Data.data[i+2];
    619          let b2 = gImage2Data.data[i+2];
    620          // Ignore alpha.
    621          if (r1 == r2 && g1 == g2 && b1 == b2) {
    622            continue;
    623          }
    624          numPixels += 1;
    625          let maxDiff = Math.max(Math.abs(r1-r2),
    626                                 Math.abs(g1-g2),
    627                                 Math.abs(b1-b2));
    628          if (maxDiff > maxPerChannel) {
    629            maxPerChannel = maxDiff
    630          }
    631        }
    632        return [numPixels, maxPerChannel];
    633      }
    634 
    635      function show_image(i) {
    636        if (i == 1) {
    637          ID("image1").style.display = "";
    638          ID("image2").style.display = "none";
    639        } else {
    640          ID("image1").style.display = "none";
    641          ID("image2").style.display = "";
    642        }
    643      }
    644 
    645      function handle_keyboard_shortcut(event) {
    646        switch (event.charCode) {
    647        case 49: // "1" key
    648          document.getElementById("radio1").checked = true;
    649          show_image(1);
    650          break;
    651        case 50: // "2" key
    652          document.getElementById("radio2").checked = true;
    653          show_image(2);
    654          break;
    655        case 100: // "d" key
    656          document.getElementById("differences").click();
    657          break;
    658        case 112: // "p" key
    659          shift_images(-1);
    660          break;
    661        case 110: // "n" key
    662          shift_images(1);
    663          break;
    664        }
    665      }
    666 
    667      function handle_keydown(event) {
    668        switch (event.keyCode) {
    669        case 37:  // left arrow
    670          move_pixel(-1, 0);
    671          break;
    672        case 38:  // up arrow
    673          move_pixel(0,-1);
    674          break;
    675        case 39:  // right arrow
    676          move_pixel(1, 0);
    677          break;
    678        case 40:  // down arrow
    679          move_pixel(0, 1);
    680          break;
    681        }
    682      }
    683 
    684      function shift_images(dir) {
    685        var activeItem = document.querySelector(".activeitem");
    686        if (!activeItem) {
    687          return;
    688        }
    689        for (var elm = activeItem; elm; elm = elm.parentElement) {
    690          if (elm.tagName != "tr") {
    691            continue;
    692          }
    693          elm = dir > 0 ? elm.nextElementSibling : elm.previousElementSibling;
    694          if (elm) {
    695            elm.getElementsByTagName("a")[0].click();
    696          }
    697          return;
    698        }
    699      }
    700 
    701      function show_differences(cb) {
    702        ID("diffrect").style.display = cb.checked ? "" : "none";
    703      }
    704 
    705      function flash_pixels(on) {
    706        var stroke = on ? "red" : "black";
    707        var strokeWidth = on ? "2px" : "1px";
    708        for (var i = 0; i < gFlashingPixels.length; i++) {
    709          gFlashingPixels[i].setAttribute("stroke", stroke);
    710          gFlashingPixels[i].setAttribute("stroke-width", strokeWidth);
    711        }
    712      }
    713 
    714      function cursor_point(evt) {
    715        var m = evt.target.getScreenCTM().inverse();
    716        var p = ID("svg").createSVGPoint();
    717        p.x = evt.clientX;
    718        p.y = evt.clientY;
    719        p = p.matrixTransform(m);
    720        return { x: Math.floor(p.x), y: Math.floor(p.y) };
    721      }
    722 
    723      function hex2(i) {
    724        return (i < 16 ? "0" : "") + i.toString(16);
    725      }
    726 
    727      function canvas_pixel_as_hex(data, x, y) {
    728        var offset = (y * data.width + x) * 4;
    729        var r = data.data[offset];
    730        var g = data.data[offset + 1];
    731        var b = data.data[offset + 2];
    732        return "#" + hex2(r) + hex2(g) + hex2(b);
    733      }
    734 
    735      function hex_as_rgb(hex) {
    736        return "rgb(" + [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)] + ")";
    737      }
    738 
    739      function magnify(evt) {
    740        var { x: x, y: y } = cursor_point(evt);
    741        do_magnify(x, y);
    742      }
    743 
    744      function do_magnify(x, y) {
    745        var centerPixelColor1, centerPixelColor2;
    746 
    747        var dx_lo = -Math.floor(gMagWidth / 2);
    748        var dx_hi = Math.floor(gMagWidth / 2);
    749        var dy_lo = -Math.floor(gMagHeight / 2);
    750        var dy_hi = Math.floor(gMagHeight / 2);
    751 
    752        flash_pixels(false);
    753        gFlashingPixels = [];
    754        for (var j = dy_lo; j <= dy_hi; j++) {
    755          for (var i = dx_lo; i <= dx_hi; i++) {
    756            var px = x + i;
    757            var py = y + j;
    758            var p1 = gMagPixPaths[i + dx_hi][j + dy_hi][0];
    759            var p2 = gMagPixPaths[i + dx_hi][j + dy_hi][1];
    760            // Here we just use the dimensions of gImage1Data since we expect test
    761            // and reference to have the same dimensions.
    762            if (px < 0 || py < 0 || px >= gImage1Data.width || py >= gImage1Data.height) {
    763              p1.setAttribute("fill", "#aaa");
    764              p2.setAttribute("fill", "#888");
    765            } else {
    766              var color1 = canvas_pixel_as_hex(gImage1Data, x + i, y + j);
    767              var color2 = canvas_pixel_as_hex(gImage2Data, x + i, y + j);
    768              p1.setAttribute("fill", color1);
    769              p2.setAttribute("fill", color2);
    770              if (color1 != color2) {
    771                gFlashingPixels.push(p1, p2);
    772                p1.parentNode.appendChild(p1);
    773                p2.parentNode.appendChild(p2);
    774              }
    775              if (i == 0 && j == 0) {
    776                centerPixelColor1 = color1;
    777                centerPixelColor2 = color2;
    778              }
    779            }
    780          }
    781        }
    782        flash_pixels(true);
    783        show_pixelinfo(x, y, centerPixelColor1, hex_as_rgb(centerPixelColor1), centerPixelColor2, hex_as_rgb(centerPixelColor2));
    784      }
    785 
    786      function show_pixelinfo(x, y, pix1rgb, pix1hex, pix2rgb, pix2hex) {
    787        ID("coords").textContent = [x, y];
    788        ID("pix1hex").textContent = pix1hex;
    789        ID("pix1rgb").textContent = pix1rgb;
    790        ID("pix2hex").textContent = pix2hex;
    791        ID("pix2rgb").textContent = pix2rgb;
    792      }
    793 
    794      function move_pixel(deltax, deltay) {
    795        let coords = ID("coords").textContent.split(',');
    796        let x = parseInt(coords[0]);
    797        let y = parseInt(coords[1]);
    798        if (isNaN(x) || isNaN(y)) {
    799          return;
    800        }
    801        x = x + deltax;
    802        y = y + deltay;
    803        if (x >= 0 && y >= 0 && x < gImage1Data.width && y < gImage1Data.height) {
    804          do_magnify(x, y);
    805        }
    806      }
    807 
    808        ]]>
    809    </script>
    810  </head>
    811  <body onload="load()">
    812    <div id="entry">
    813      <h1>Reftest analyzer: load reftest log</h1>
    814 
    815      <p>
    816        Either paste your log into this textarea:<br />
    817        <textarea cols="80" rows="10" id="logentry" /><br />
    818        <input
    819          type="button"
    820          value="Process pasted log"
    821          onclick="log_pasted()"
    822        />
    823      </p>
    824 
    825      <p>
    826        ... or load it from a file:<br />
    827        <input type="file" id="fileentry" onchange="fileentry_changed()" />
    828      </p>
    829    </div>
    830 
    831    <div id="loading" style="display: none">Loading log...</div>
    832 
    833    <div id="viewer" style="display: none">
    834      <div id="pixelarea">
    835        <div id="pixelinfo">
    836          <table>
    837            <tbody>
    838              <tr>
    839                <th>Pixel at:</th>
    840                <td colspan="2" id="coords" />
    841              </tr>
    842              <tr>
    843                <th>Image 1:</th>
    844                <td id="pix1rgb"></td>
    845                <td id="pix1hex"></td>
    846              </tr>
    847              <tr>
    848                <th>Image 2:</th>
    849                <td id="pix2rgb"></td>
    850                <td id="pix2hex"></td>
    851              </tr>
    852            </tbody>
    853          </table>
    854          <div>
    855            <div id="pixelhint">
    856     857              <div>
    858                <p>
    859                  Move the mouse over the reftest image on the right to show
    860                  magnified pixels on the left. The color information above is
    861                  for the pixel centered in the magnified view.
    862                </p>
    863                <p>
    864                  Image 1 is shown in the upper triangle of each pixel and Image
    865                  2 is shown in the lower triangle.
    866                </p>
    867              </div>
    868            </div>
    869          </div>
    870        </div>
    871        <div id="magnification">
    872          <svg
    873            xmlns="http://www.w3.org/2000/svg"
    874            width="84"
    875            height="84"
    876            shape-rendering="optimizeSpeed"
    877          >
    878            <g id="mag" />
    879          </svg>
    880        </div>
    881      </div>
    882      <div id="itemlist"></div>
    883      <div id="images" style="display: none">
    884        <form id="imgcontrols">
    885          <input
    886            id="radio1"
    887            type="radio"
    888            name="which"
    889            value="0"
    890            onchange="show_image(1)"
    891            checked="checked"
    892          /><label id="label1" title="1" for="radio1">Image 1</label>
    893          <input
    894            id="radio2"
    895            type="radio"
    896            name="which"
    897            value="1"
    898            onchange="show_image(2)"
    899          /><label id="label2" title="2" for="radio2">Image 2</label>
    900          <label
    901            ><input
    902              id="differences"
    903              type="checkbox"
    904              onchange="show_differences(this)"
    905            />Circle differences</label
    906          >
    907        </form>
    908        <svg
    909          xmlns="http://www.w3.org/2000/svg"
    910          xmlns:xlink="http://www.w3.org/1999/xlink"
    911          version="1.1"
    912          width="800"
    913          height="1000"
    914          id="svg"
    915        >
    916          <defs>
    917            <!-- use sRGB to avoid loss of data -->
    918            <filter
    919              id="showDifferences"
    920              x="0%"
    921              y="0%"
    922              width="100%"
    923              height="100%"
    924              style="color-interpolation-filters: sRGB"
    925            >
    926              <feImage id="feimage1" result="img1" xlink:href="#image1" />
    927              <feImage id="feimage2" result="img2" xlink:href="#image2" />
    928              <!-- inv1 and inv2 are the images with RGB inverted -->
    929              <feComponentTransfer result="inv1" in="img1">
    930                <feFuncR type="linear" slope="-1" intercept="1" />
    931                <feFuncG type="linear" slope="-1" intercept="1" />
    932                <feFuncB type="linear" slope="-1" intercept="1" />
    933              </feComponentTransfer>
    934              <feComponentTransfer result="inv2" in="img2">
    935                <feFuncR type="linear" slope="-1" intercept="1" />
    936                <feFuncG type="linear" slope="-1" intercept="1" />
    937                <feFuncB type="linear" slope="-1" intercept="1" />
    938              </feComponentTransfer>
    939              <!-- w1 will have non-white pixels anywhere that img2
    940               is brighter than img1, and w2 for the reverse.
    941               It would be nice not to have to go through these
    942               intermediate states, but feComposite
    943               type="arithmetic" can't transform the RGB channels
    944               and leave the alpha channel untouched. -->
    945              <feComposite
    946                result="w1"
    947                in="img1"
    948                in2="inv2"
    949                operator="arithmetic"
    950                k2="1"
    951                k3="1"
    952              />
    953              <feComposite
    954                result="w2"
    955                in="img2"
    956                in2="inv1"
    957                operator="arithmetic"
    958                k2="1"
    959                k3="1"
    960              />
    961              <!-- c1 will have non-black pixels anywhere that img2
    962               is brighter than img1, and c2 for the reverse -->
    963              <feComponentTransfer result="c1" in="w1">
    964                <feFuncR type="linear" slope="-1" intercept="1" />
    965                <feFuncG type="linear" slope="-1" intercept="1" />
    966                <feFuncB type="linear" slope="-1" intercept="1" />
    967              </feComponentTransfer>
    968              <feComponentTransfer result="c2" in="w2">
    969                <feFuncR type="linear" slope="-1" intercept="1" />
    970                <feFuncG type="linear" slope="-1" intercept="1" />
    971                <feFuncB type="linear" slope="-1" intercept="1" />
    972              </feComponentTransfer>
    973              <!-- c will be nonblack (and fully on) for every pixel+component where there are differences -->
    974              <feComposite
    975                result="c"
    976                in="c1"
    977                in2="c2"
    978                operator="arithmetic"
    979                k2="255"
    980                k3="255"
    981              />
    982              <!-- a will be opaque for every pixel with differences and transparent for all others -->
    983              <feColorMatrix
    984                result="a"
    985                type="matrix"
    986                values="0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  1 1 1 0 0"
    987              />
    988 
    989              <!-- a, dilated by 1 pixel -->
    990              <feMorphology
    991                result="dila1"
    992                in="a"
    993                operator="dilate"
    994                radius="1"
    995              />
    996              <!-- a, dilated by 2 pixels -->
    997              <feMorphology
    998                result="dila2"
    999                in="dila1"
   1000                operator="dilate"
   1001                radius="1"
   1002              />
   1003 
   1004              <!-- all the pixels in the 2-pixel dilation of a but not in the 1-pixel dilation, to highlight the diffs -->
   1005              <feComposite
   1006                result="highlight"
   1007                in="dila2"
   1008                in2="dila1"
   1009                operator="out"
   1010              />
   1011 
   1012              <feFlood result="red" flood-color="red" />
   1013              <feComposite
   1014                result="redhighlight"
   1015                in="red"
   1016                in2="highlight"
   1017                operator="in"
   1018              />
   1019              <feFlood result="black" flood-color="black" flood-opacity="0.5" />
   1020              <feMerge>
   1021                <feMergeNode in="black" />
   1022                <feMergeNode in="redhighlight" />
   1023              </feMerge>
   1024            </filter>
   1025          </defs>
   1026          <g onmousemove="magnify(evt)">
   1027            <image x="0" y="0" width="100%" height="100%" id="image1" />
   1028            <image x="0" y="0" width="100%" height="100%" id="image2" />
   1029          </g>
   1030          <rect
   1031            id="diffrect"
   1032            filter="url(#showDifferences)"
   1033            pointer-events="none"
   1034            x="0"
   1035            y="0"
   1036            width="100%"
   1037            height="100%"
   1038          />
   1039        </svg>
   1040        <div id="pixel-differences"></div>
   1041      </div>
   1042    </div>
   1043  </body>
   1044 </html>