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>