Clean up ChunkedUpdateDrawingAreaProxy
[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(' ') + 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);