1 /* 2 * Copyright (c) 2010 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 var ALL_DIRECTORY_PATH = '[all]'; 32 33 var STATE_NEEDS_REBASELINE = 'needs_rebaseline'; 34 var STATE_REBASELINE_FAILED = 'rebaseline_failed'; 35 var STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded'; 36 var STATE_IN_QUEUE = 'in_queue'; 37 var STATE_TO_DISPLAY_STATE = {}; 38 STATE_TO_DISPLAY_STATE[STATE_NEEDS_REBASELINE] = 'Needs rebaseline'; 39 STATE_TO_DISPLAY_STATE[STATE_REBASELINE_FAILED] = 'Rebaseline failed'; 40 STATE_TO_DISPLAY_STATE[STATE_REBASELINE_SUCCEEDED] = 'Rebaseline succeeded'; 41 STATE_TO_DISPLAY_STATE[STATE_IN_QUEUE] = 'In queue'; 42 43 var results; 44 var testsByFailureType = {}; 45 var testsByDirectory = {}; 46 var selectedTests = []; 47 var loupe; 48 var queue; 49 50 function main() 51 { 52 $('failure-type-selector').addEventListener('change', selectFailureType); 53 $('directory-selector').addEventListener('change', selectDirectory); 54 $('test-selector').addEventListener('change', selectTest); 55 $('next-test').addEventListener('click', nextTest); 56 $('previous-test').addEventListener('click', previousTest); 57 58 $('toggle-log').addEventListener('click', function() { toggle('log'); }); 59 60 loupe = new Loupe(); 61 queue = new RebaselineQueue(); 62 63 document.addEventListener('keydown', function(event) { 64 if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { 65 return; 66 } 67 68 switch (event.keyIdentifier) { 69 case 'Left': 70 event.preventDefault(); 71 previousTest(); 72 break; 73 case 'Right': 74 event.preventDefault(); 75 nextTest(); 76 break; 77 case 'U+0051': // q 78 queue.addCurrentTest(); 79 break; 80 case 'U+0058': // x 81 queue.removeCurrentTest(); 82 break; 83 case 'U+0052': // r 84 queue.rebaseline(); 85 break; 86 } 87 }); 88 89 loadText('/platforms.json', function(text) { 90 var platforms = JSON.parse(text); 91 platforms.platforms.forEach(function(platform) { 92 var platformOption = document.createElement('option'); 93 platformOption.value = platform; 94 platformOption.textContent = platform; 95 96 var targetOption = platformOption.cloneNode(true); 97 targetOption.selected = platform == platforms.defaultPlatform; 98 $('baseline-target').appendChild(targetOption); 99 $('baseline-move-to').appendChild(platformOption.cloneNode(true)); 100 }); 101 }); 102 103 loadText('/results.json', function(text) { 104 results = JSON.parse(text); 105 displayResults(); 106 }); 107 } 108 109 /** 110 * Groups test results by failure type. 111 */ 112 function displayResults() 113 { 114 var failureTypeSelector = $('failure-type-selector'); 115 var failureTypes = []; 116 117 for (var testName in results.tests) { 118 var test = results.tests[testName]; 119 if (test.actual == 'PASS') { 120 continue; 121 } 122 var failureType = test.actual + ' (expected ' + test.expected + ')'; 123 if (!(failureType in testsByFailureType)) { 124 testsByFailureType[failureType] = []; 125 failureTypes.push(failureType); 126 } 127 testsByFailureType[failureType].push(testName); 128 } 129 130 // Sort by number of failures 131 failureTypes.sort(function(a, b) { 132 return testsByFailureType[b].length - testsByFailureType[a].length; 133 }); 134 135 for (var i = 0, failureType; failureType = failureTypes[i]; i++) { 136 var failureTypeOption = document.createElement('option'); 137 failureTypeOption.value = failureType; 138 failureTypeOption.textContent = failureType + ' - ' + testsByFailureType[failureType].length + ' tests'; 139 failureTypeSelector.appendChild(failureTypeOption); 140 } 141 142 selectFailureType(); 143 144 document.body.className = ''; 145 } 146 147 /** 148 * For a given failure type, gets all the tests and groups them by directory 149 * (populating the directory selector with them). 150 */ 151 function selectFailureType() 152 { 153 var selectedFailureType = getSelectValue('failure-type-selector'); 154 var tests = testsByFailureType[selectedFailureType]; 155 156 testsByDirectory = {} 157 var displayDirectoryNamesByDirectory = {}; 158 var directories = []; 159 160 // Include a special option for all tests 161 testsByDirectory[ALL_DIRECTORY_PATH] = tests; 162 displayDirectoryNamesByDirectory[ALL_DIRECTORY_PATH] = 'all'; 163 directories.push(ALL_DIRECTORY_PATH); 164 165 // Roll up tests by ancestor directories 166 tests.forEach(function(test) { 167 var pathPieces = test.split('/'); 168 var pathDirectories = pathPieces.slice(0, pathPieces.length -1); 169 var ancestorDirectory = ''; 170 171 pathDirectories.forEach(function(pathDirectory, index) { 172 ancestorDirectory += pathDirectory + '/'; 173 if (!(ancestorDirectory in testsByDirectory)) { 174 testsByDirectory[ancestorDirectory] = []; 175 var displayDirectoryName = new Array(index * 6).join(' ') + pathDirectory; 176 displayDirectoryNamesByDirectory[ancestorDirectory] = displayDirectoryName; 177 directories.push(ancestorDirectory); 178 } 179 180 testsByDirectory[ancestorDirectory].push(test); 181 }); 182 }); 183 184 directories.sort(); 185 186 var directorySelector = $('directory-selector'); 187 directorySelector.innerHTML = ''; 188 189 directories.forEach(function(directory) { 190 var directoryOption = document.createElement('option'); 191 directoryOption.value = directory; 192 directoryOption.innerHTML = 193 displayDirectoryNamesByDirectory[directory] + ' - ' + 194 testsByDirectory[directory].length + ' tests'; 195 directorySelector.appendChild(directoryOption); 196 }); 197 198 selectDirectory(); 199 } 200 201 /** 202 * For a given failure type and directory and failure type, gets all the tests 203 * in that directory and populatest the test selector with them. 204 */ 205 function selectDirectory() 206 { 207 var previouslySelectedTest = getSelectedTest(); 208 209 var selectedDirectory = getSelectValue('directory-selector'); 210 selectedTests = testsByDirectory[selectedDirectory]; 211 selectedTests.sort(); 212 213 var testsByState = {}; 214 selectedTests.forEach(function(testName) { 215 var state = results.tests[testName].state; 216 if (state == STATE_IN_QUEUE) { 217 state = STATE_NEEDS_REBASELINE; 218 } 219 if (!(state in testsByState)) { 220 testsByState[state] = []; 221 } 222 testsByState[state].push(testName); 223 }); 224 225 var optionIndexByTest = {}; 226 227 var testSelector = $('test-selector'); 228 testSelector.innerHTML = ''; 229 230 for (var state in testsByState) { 231 var stateOption = document.createElement('option'); 232 stateOption.textContent = STATE_TO_DISPLAY_STATE[state]; 233 stateOption.disabled = true; 234 testSelector.appendChild(stateOption); 235 236 testsByState[state].forEach(function(testName) { 237 var testOption = document.createElement('option'); 238 testOption.value = testName; 239 var testDisplayName = testName; 240 if (testName.lastIndexOf(selectedDirectory) == 0) { 241 testDisplayName = testName.substring(selectedDirectory.length); 242 } 243 testOption.innerHTML = ' ' + testDisplayName; 244 optionIndexByTest[testName] = testSelector.options.length; 245 testSelector.appendChild(testOption); 246 }); 247 } 248 249 if (previouslySelectedTest in optionIndexByTest) { 250 testSelector.selectedIndex = optionIndexByTest[previouslySelectedTest]; 251 } else if (STATE_NEEDS_REBASELINE in testsByState) { 252 testSelector.selectedIndex = 253 optionIndexByTest[testsByState[STATE_NEEDS_REBASELINE][0]]; 254 selectTest(); 255 } else { 256 testSelector.selectedIndex = 1; 257 selectTest(); 258 } 259 260 selectTest(); 261 } 262 263 function getSelectedTest() 264 { 265 return getSelectValue('test-selector'); 266 } 267 268 function selectTest() 269 { 270 var selectedTest = getSelectedTest(); 271 272 if (results.tests[selectedTest].actual.indexOf('IMAGE') != -1) { 273 $('image-outputs').style.display = ''; 274 displayImageResults(selectedTest); 275 } else { 276 $('image-outputs').style.display = 'none'; 277 } 278 279 if (results.tests[selectedTest].actual.indexOf('TEXT') != -1) { 280 $('text-outputs').style.display = ''; 281 displayTextResults(selectedTest); 282 } else { 283 $('text-outputs').style.display = 'none'; 284 } 285 286 var currentBaselines = $('current-baselines'); 287 currentBaselines.textContent = ''; 288 var baselines = results.tests[selectedTest].baselines; 289 var testName = selectedTest.split('.').slice(0, -1).join('.'); 290 getSortedKeys(baselines).forEach(function(platform, i) { 291 if (i != 0) { 292 currentBaselines.appendChild(document.createTextNode('; ')); 293 } 294 var platformName = document.createElement('span'); 295 platformName.className = 'platform'; 296 platformName.textContent = platform; 297 currentBaselines.appendChild(platformName); 298 currentBaselines.appendChild(document.createTextNode(' (')); 299 getSortedKeys(baselines[platform]).forEach(function(extension, j) { 300 if (j != 0) { 301 currentBaselines.appendChild(document.createTextNode(', ')); 302 } 303 var link = document.createElement('a'); 304 var baselinePath = ''; 305 if (platform != 'base') { 306 baselinePath += 'platform/' + platform + '/'; 307 } 308 baselinePath += testName + '-expected' + extension; 309 link.href = getTracUrl(baselinePath); 310 if (extension == '.checksum') { 311 link.textContent = 'chk'; 312 } else { 313 link.textContent = extension.substring(1); 314 } 315 link.target = '_blank'; 316 if (baselines[platform][extension]) { 317 link.className = 'was-used-for-test'; 318 } 319 currentBaselines.appendChild(link); 320 }); 321 currentBaselines.appendChild(document.createTextNode(')')); 322 }); 323 324 updateState(); 325 loupe.hide(); 326 327 prefetchNextImageTest(); 328 } 329 330 function prefetchNextImageTest() 331 { 332 var testSelector = $('test-selector'); 333 if (testSelector.selectedIndex == testSelector.options.length - 1) { 334 return; 335 } 336 var nextTest = testSelector.options[testSelector.selectedIndex + 1].value; 337 if (results.tests[nextTest].actual.indexOf('IMAGE') != -1) { 338 new Image().src = getTestResultUrl(nextTest, 'expected-image'); 339 new Image().src = getTestResultUrl(nextTest, 'actual-image'); 340 } 341 } 342 343 function updateState() 344 { 345 var testName = getSelectedTest(); 346 var testIndex = selectedTests.indexOf(testName); 347 var testCount = selectedTests.length 348 $('test-index').textContent = testIndex + 1; 349 $('test-count').textContent = testCount; 350 351 $('next-test').disabled = testIndex == testCount - 1; 352 $('previous-test').disabled = testIndex == 0; 353 354 $('test-link').href = getTracUrl(testName); 355 356 var state = results.tests[testName].state; 357 $('state').className = state; 358 $('state').innerHTML = STATE_TO_DISPLAY_STATE[state]; 359 360 queue.updateState(); 361 } 362 363 function getTestResultUrl(testName, mode) 364 { 365 return '/test_result?test=' + testName + '&mode=' + mode; 366 } 367 368 var currentExpectedImageTest; 369 var currentActualImageTest; 370 371 function displayImageResults(testName) 372 { 373 if (currentExpectedImageTest == currentActualImageTest 374 && currentExpectedImageTest == testName) { 375 return; 376 } 377 378 function displayImageResult(mode, callback) { 379 var image = $(mode); 380 image.className = 'loading'; 381 image.src = getTestResultUrl(testName, mode); 382 image.onload = function() { 383 image.className = ''; 384 callback(); 385 updateImageDiff(); 386 }; 387 } 388 389 displayImageResult( 390 'expected-image', 391 function() { currentExpectedImageTest = testName; }); 392 displayImageResult( 393 'actual-image', 394 function() { currentActualImageTest = testName; }); 395 396 $('diff-canvas').className = 'loading'; 397 $('diff-canvas').style.display = ''; 398 $('diff-checksum').style.display = 'none'; 399 } 400 401 /** 402 * Computes a graphical a diff between the expected and actual images by 403 * rendering each to a canvas, getting the image data, and comparing the RGBA 404 * components of each pixel. The output is put into the diff canvas, with 405 * identical pixels appearing at 12.5% opacity and different pixels being 406 * highlighted in red. 407 */ 408 function updateImageDiff() { 409 if (currentExpectedImageTest != currentActualImageTest) 410 return; 411 412 var expectedImage = $('expected-image'); 413 var actualImage = $('actual-image'); 414 415 function getImageData(image) { 416 var imageCanvas = document.createElement('canvas'); 417 imageCanvas.width = image.width; 418 imageCanvas.height = image.height; 419 imageCanvasContext = imageCanvas.getContext('2d'); 420 421 imageCanvasContext.fillStyle = 'rgba(255, 255, 255, 1)'; 422 imageCanvasContext.fillRect( 423 0, 0, image.width, image.height); 424 425 imageCanvasContext.drawImage(image, 0, 0); 426 return imageCanvasContext.getImageData( 427 0, 0, image.width, image.height); 428 } 429 430 var expectedImageData = getImageData(expectedImage); 431 var actualImageData = getImageData(actualImage); 432 433 var diffCanvas = $('diff-canvas'); 434 var diffCanvasContext = diffCanvas.getContext('2d'); 435 var diffImageData = 436 diffCanvasContext.createImageData(diffCanvas.width, diffCanvas.height); 437 438 // Avoiding property lookups for all these during the per-pixel loop below 439 // provides a significant performance benefit. 440 var expectedWidth = expectedImage.width; 441 var expectedHeight = expectedImage.height; 442 var expected = expectedImageData.data; 443 444 var actualWidth = actualImage.width; 445 var actual = actualImageData.data; 446 447 var diffWidth = diffImageData.width; 448 var diff = diffImageData.data; 449 450 var hadDiff = false; 451 for (var x = 0; x < expectedWidth; x++) { 452 for (var y = 0; y < expectedHeight; y++) { 453 var expectedOffset = (y * expectedWidth + x) * 4; 454 var actualOffset = (y * actualWidth + x) * 4; 455 var diffOffset = (y * diffWidth + x) * 4; 456 if (expected[expectedOffset] != actual[actualOffset] || 457 expected[expectedOffset + 1] != actual[actualOffset + 1] || 458 expected[expectedOffset + 2] != actual[actualOffset + 2] || 459 expected[expectedOffset + 3] != actual[actualOffset + 3]) { 460 hadDiff = true; 461 diff[diffOffset] = 255; 462 diff[diffOffset + 1] = 0; 463 diff[diffOffset + 2] = 0; 464 diff[diffOffset + 3] = 255; 465 } else { 466 diff[diffOffset] = expected[expectedOffset]; 467 diff[diffOffset + 1] = expected[expectedOffset + 1]; 468 diff[diffOffset + 2] = expected[expectedOffset + 2]; 469 diff[diffOffset + 3] = 32; 470 } 471 } 472 } 473 474 diffCanvasContext.putImageData( 475 diffImageData, 476 0, 0, 477 0, 0, 478 diffImageData.width, diffImageData.height); 479 diffCanvas.className = ''; 480 481 if (!hadDiff) { 482 diffCanvas.style.display = 'none'; 483 $('diff-checksum').style.display = ''; 484 loadTextResult(currentExpectedImageTest, 'expected-checksum'); 485 loadTextResult(currentExpectedImageTest, 'actual-checksum'); 486 } 487 } 488 489 function loadTextResult(testName, mode, responseIsHtml) 490 { 491 loadText(getTestResultUrl(testName, mode), function(text) { 492 if (responseIsHtml) { 493 $(mode).innerHTML = text; 494 } else { 495 $(mode).textContent = text; 496 } 497 }); 498 } 499 500 function displayTextResults(testName) 501 { 502 loadTextResult(testName, 'expected-text'); 503 loadTextResult(testName, 'actual-text'); 504 loadTextResult(testName, 'diff-text-pretty', true); 505 } 506 507 function nextTest() 508 { 509 var testSelector = $('test-selector'); 510 var nextTestIndex = testSelector.selectedIndex + 1; 511 while (true) { 512 if (nextTestIndex == testSelector.options.length) { 513 return; 514 } 515 if (testSelector.options[nextTestIndex].disabled) { 516 nextTestIndex++; 517 } else { 518 testSelector.selectedIndex = nextTestIndex; 519 selectTest(); 520 return; 521 } 522 } 523 } 524 525 function previousTest() 526 { 527 var testSelector = $('test-selector'); 528 var previousTestIndex = testSelector.selectedIndex - 1; 529 while (true) { 530 if (previousTestIndex == -1) { 531 return; 532 } 533 if (testSelector.options[previousTestIndex].disabled) { 534 previousTestIndex--; 535 } else { 536 testSelector.selectedIndex = previousTestIndex; 537 selectTest(); 538 return 539 } 540 } 541 } 542 543 window.addEventListener('DOMContentLoaded', main); 544