Home | History | Annotate | Download | only in rebaselineserver
      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