tor-browser

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

reftest-analyzer.xhtml (39676B)


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