2010-11-30 Mihai Parparita <mihaip@chromium.org>
[WebKit-https.git] / WebKitTools / Scripts / webkitpy / tool / commands / data / rebaselineserver / main.js
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('&nbsp;') + 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 selectedDirectory = getSelectValue('directory-selector');
208     selectedTests = testsByDirectory[selectedDirectory];
209
210     selectedTests.sort();
211
212     var testSelector = $('test-selector');
213     testSelector.innerHTML = '';
214
215     selectedTests.forEach(function(testName) {
216         var testOption = document.createElement('option');
217         testOption.value = testName;
218         var testDisplayName = testName;
219         if (testName.lastIndexOf(selectedDirectory) == 0) {
220             testDisplayName = testName.substring(selectedDirectory.length);
221         }
222         testOption.innerHTML = '&nbsp;&nbsp;' + testDisplayName;
223         testSelector.appendChild(testOption);
224     });
225
226     selectTest();
227 }
228
229 function getSelectedTest()
230 {
231     return getSelectValue('test-selector');
232 }
233
234 function selectTest()
235 {
236     var selectedTest = getSelectedTest();
237
238     if (results.tests[selectedTest].actual.indexOf('IMAGE') != -1) {
239         $('image-outputs').style.display = '';
240         displayImageResults(selectedTest);
241     } else {
242         $('image-outputs').style.display = 'none';
243     }
244
245     if (results.tests[selectedTest].actual.indexOf('TEXT') != -1) {
246         $('text-outputs').style.display = '';
247         displayTextResults(selectedTest);
248     } else {
249         $('text-outputs').style.display = 'none';
250     }
251
252     var currentBaselines = $('current-baselines');
253     currentBaselines.textContent = '';
254     var baselines = results.tests[selectedTest].baselines;
255     var testName = selectedTest.split('.').slice(0, -1).join('.');
256     getSortedKeys(baselines).forEach(function(platform, i) {
257         if (i != 0) {
258             currentBaselines.appendChild(document.createTextNode('; '));
259         }
260         var platformName = document.createElement('span');
261         platformName.className = 'platform';
262         platformName.textContent = platform;
263         currentBaselines.appendChild(platformName);
264         currentBaselines.appendChild(document.createTextNode(' ('));
265         getSortedKeys(baselines[platform]).forEach(function(extension, j) {
266             if (j != 0) {
267                 currentBaselines.appendChild(document.createTextNode(', '));
268             }
269             var link = document.createElement('a');
270             var baselinePath = '';
271             if (platform != 'base') {
272                 baselinePath += 'platform/' + platform + '/';
273             }
274             baselinePath += testName + '-expected' + extension;
275             link.href = getTracUrl(baselinePath);
276             if (extension == '.checksum') {
277                 link.textContent = 'chk';
278             } else {
279                 link.textContent = extension.substring(1);
280             }
281             link.target = '_blank';
282             if (baselines[platform][extension]) {
283                 link.className = 'was-used-for-test';
284             }
285             currentBaselines.appendChild(link);
286         });
287         currentBaselines.appendChild(document.createTextNode(')'));
288     });
289
290     updateState();
291     loupe.hide();
292
293     prefetchNextImageTest();
294 }
295
296 function prefetchNextImageTest()
297 {
298     var testSelector = $('test-selector');
299     if (testSelector.selectedIndex == testSelector.options.length - 1) {
300         return;
301     }
302     var nextTest = testSelector.options[testSelector.selectedIndex + 1].value;
303     if (results.tests[nextTest].actual.indexOf('IMAGE') != -1) {
304         new Image().src = getTestResultUrl(nextTest, 'expected-image');
305         new Image().src = getTestResultUrl(nextTest, 'actual-image');
306     }
307 }
308
309 function updateState()
310 {
311     var testName = getSelectedTest();
312     var testIndex = selectedTests.indexOf(testName);
313     var testCount = selectedTests.length
314     $('test-index').textContent = testIndex + 1;
315     $('test-count').textContent = testCount;
316
317     $('next-test').disabled = testIndex == testCount - 1;
318     $('previous-test').disabled = testIndex == 0;
319
320     $('test-link').href = getTracUrl(testName);
321
322     var state = results.tests[testName].state;
323     $('state').className = state;
324     $('state').innerHTML = STATE_TO_DISPLAY_STATE[state];
325
326     queue.updateState();
327 }
328
329 function getTestResultUrl(testName, mode)
330 {
331     return '/test_result?test=' + testName + '&mode=' + mode;
332 }
333
334 var currentExpectedImageTest;
335 var currentActualImageTest;
336
337 function displayImageResults(testName)
338 {
339     if (currentExpectedImageTest == currentActualImageTest
340         && currentExpectedImageTest == testName) {
341         return;
342     }
343
344     function displayImageResult(mode, callback) {
345         var image = $(mode);
346         image.className = 'loading';
347         image.src = getTestResultUrl(testName, mode);
348         image.onload = function() {
349             image.className = '';
350             callback();
351             updateImageDiff();
352         };
353     }
354
355     displayImageResult(
356         'expected-image',
357         function() { currentExpectedImageTest = testName; });
358     displayImageResult(
359         'actual-image',
360         function() { currentActualImageTest = testName; });
361
362     $('diff-canvas').className = 'loading';
363     $('diff-canvas').style.display = '';
364     $('diff-checksum').style.display = 'none';
365 }
366
367 /**
368  * Computes a graphical a diff between the expected and actual images by
369  * rendering each to a canvas, getting the image data, and comparing the RGBA
370  * components of each pixel. The output is put into the diff canvas, with
371  * identical pixels appearing at 12.5% opacity and different pixels being
372  * highlighted in red.
373  */
374 function updateImageDiff() {
375     if (currentExpectedImageTest != currentActualImageTest)
376         return;
377
378     var expectedImage = $('expected-image');
379     var actualImage = $('actual-image');
380
381     function getImageData(image) {
382         var imageCanvas = document.createElement('canvas');
383         imageCanvas.width = image.width;
384         imageCanvas.height = image.height;
385         imageCanvasContext = imageCanvas.getContext('2d');
386
387         imageCanvasContext.fillStyle = 'rgba(255, 255, 255, 1)';
388         imageCanvasContext.fillRect(
389             0, 0, image.width, image.height);
390
391         imageCanvasContext.drawImage(image, 0, 0);
392         return imageCanvasContext.getImageData(
393             0, 0, image.width, image.height);
394     }
395
396     var expectedImageData = getImageData(expectedImage);
397     var actualImageData = getImageData(actualImage);
398
399     var diffCanvas = $('diff-canvas');
400     var diffCanvasContext = diffCanvas.getContext('2d');
401     var diffImageData =
402         diffCanvasContext.createImageData(diffCanvas.width, diffCanvas.height);
403
404     // Avoiding property lookups for all these during the per-pixel loop below
405     // provides a significant performance benefit.
406     var expectedWidth = expectedImage.width;
407     var expectedHeight = expectedImage.height;
408     var expected = expectedImageData.data;
409
410     var actualWidth = actualImage.width;
411     var actual = actualImageData.data;
412
413     var diffWidth = diffImageData.width;
414     var diff = diffImageData.data;
415
416     var hadDiff = false;
417     for (var x = 0; x < expectedWidth; x++) {
418         for (var y = 0; y < expectedHeight; y++) {
419             var expectedOffset = (y * expectedWidth + x) * 4;
420             var actualOffset = (y * actualWidth + x) * 4;
421             var diffOffset = (y * diffWidth + x) * 4;
422             if (expected[expectedOffset] != actual[actualOffset] ||
423                 expected[expectedOffset + 1] != actual[actualOffset + 1] ||
424                 expected[expectedOffset + 2] != actual[actualOffset + 2] ||
425                 expected[expectedOffset + 3] != actual[actualOffset + 3]) {
426                 hadDiff = true;
427                 diff[diffOffset] = 255;
428                 diff[diffOffset + 1] = 0;
429                 diff[diffOffset + 2] = 0;
430                 diff[diffOffset + 3] = 255;
431             } else {
432                 diff[diffOffset] = expected[expectedOffset];
433                 diff[diffOffset + 1] = expected[expectedOffset + 1];
434                 diff[diffOffset + 2] = expected[expectedOffset + 2];
435                 diff[diffOffset + 3] = 32;
436             }
437         }
438     }
439
440     diffCanvasContext.putImageData(
441         diffImageData,
442         0, 0,
443         0, 0,
444         diffImageData.width, diffImageData.height);
445     diffCanvas.className = '';
446
447     if (!hadDiff) {
448         diffCanvas.style.display = 'none';
449         $('diff-checksum').style.display = '';
450         loadTextResult(currentExpectedImageTest, 'expected-checksum');
451         loadTextResult(currentExpectedImageTest, 'actual-checksum');
452     }
453 }
454
455 function loadTextResult(testName, mode)
456 {
457     loadText(getTestResultUrl(testName, mode), function(text) {
458         $(mode).textContent = text;
459     });
460 }
461
462 function displayTextResults(testName)
463 {
464     loadTextResult(testName, 'expected-text');
465     loadTextResult(testName, 'actual-text');
466     loadTextResult(testName, 'diff-text');
467 }
468
469 function nextTest()
470 {
471     var testSelector = $('test-selector');
472     var nextTestIndex = testSelector.selectedIndex + 1;
473     while (true) {
474         if (nextTestIndex == testSelector.options.length) {
475             return;
476         }
477         if (testSelector.options[nextTestIndex].disabled) {
478             nextTestIndex++;
479         } else {
480             testSelector.selectedIndex = nextTestIndex;
481             selectTest();
482             return;
483         }
484     }
485 }
486
487 function previousTest()
488 {
489     var testSelector = $('test-selector');
490     var previousTestIndex = testSelector.selectedIndex - 1;
491     while (true) {
492         if (previousTestIndex == -1) {
493             return;
494         }
495         if (testSelector.options[previousTestIndex].disabled) {
496             previousTestIndex--;
497         } else {
498             testSelector.selectedIndex = previousTestIndex;
499             selectTest();
500             return
501         }
502     }
503 }
504
505 window.addEventListener('DOMContentLoaded', main);