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>