Build fixes for the flakiness dashboard.
[WebKit-https.git] / Tools / TestResultServer / static-dashboards / flakiness_dashboard.js
1 // Copyright (C) 2012 Google Inc. All rights reserved.
2 //
3 // Redistribution and use in source and binary forms, with or without
4 // modification, are permitted provided that the following conditions are
5 // met:
6 //
7 //     * Redistributions of source code must retain the above copyright
8 // notice, this list of conditions and the following disclaimer.
9 //     * Redistributions in binary form must reproduce the above
10 // copyright notice, this list of conditions and the following disclaimer
11 // in the documentation and/or other materials provided with the
12 // distribution.
13 //     * Neither the name of Google Inc. nor the names of its
14 // contributors may be used to endorse or promote products derived from
15 // this software without specific prior written permission.
16 //
17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 //////////////////////////////////////////////////////////////////////////////
30 // CONSTANTS
31 //////////////////////////////////////////////////////////////////////////////
32 var ALL = 'ALL';
33 var FORWARD = 'forward';
34 var BACKWARD = 'backward';
35 var GTEST_MODIFIERS = ['FLAKY', 'FAILS', 'MAYBE', 'DISABLED'];
36 var TEST_URL_BASE_PATH_TRAC = 'http://trac.webkit.org/browser/trunk/LayoutTests/';
37 var TEST_URL_BASE_PATH = "http://svn.webkit.org/repository/webkit/trunk/LayoutTests/";
38 var EXPECTATIONS_URL_BASE_PATH = TEST_URL_BASE_PATH + "platform/";
39 var TEST_RESULTS_BASE_PATH = 'http://build.chromium.org/f/chromium/layout_test_results/';
40 var GPU_RESULTS_BASE_PATH = 'http://chromium-browser-gpu-tests.commondatastorage.googleapis.com/runs/'
41
42 var PLATFORMS = {
43     'APPLE': {
44         subPlatforms: {
45             'MAC': {
46                 expectationsDirectory: 'mac',
47                 subPlatforms: {
48                     'LION': {
49                         expectationsDirectory: 'mac-lion',
50                         subPlatforms: {
51                             'WK1': { fallbackPlatforms: ['APPLE_MAC_LION', 'APPLE_MAC'] },
52                             'WK2': { fallbackPlatforms: ['APPLE_MAC_LION', 'APPLE_MAC', 'WK2'], expectationsDirectory: 'mac-wk2'}
53                         }
54                     },
55                     'MOUNTAINLION': {
56                         subPlatforms: {
57                             'WK1': { fallbackPlatforms: ['APPLE_MAC_MOUNTAINLION', 'APPLE_MAC'] },
58                             'WK2': { fallbackPlatforms: ['APPLE_MAC_MOUNTAINLION', 'APPLE_MAC', 'WK2'], expectationsDirectory: 'mac-wk2'}
59                         }
60                     },
61                 }
62             },
63             'WIN': {
64                 expectationsDirectory: 'win',
65                 subPlatforms: {
66                     'XP': { fallbackPlatforms: ['APPLE_WIN'] },
67                     'WIN7': { fallbackPlatforms: ['APPLE_WIN'] }
68                 }
69             }
70         }
71     },
72     'GTK': {
73         expectationsDirectory: 'gtk',
74         subPlatforms: {
75             'LINUX': {
76                 subPlatforms: {
77                     'WK1': { fallbackPlatforms: ['GTK'] },
78                     'WK2': { fallbackPlatforms: ['GTK', 'WK2'], expectationsDirectory: 'gtk-wk2' }
79                 }
80             }
81         }
82     },
83     'QT': {
84         expectationsDirectory: 'qt',
85         subPlatforms: {
86             'LINUX': { fallbackPlatforms: ['QT'] }
87         }
88     },
89     'EFL': {
90         expectationsDirectory: 'efl',
91         subPlatforms: {
92             'LINUX': {
93                 subPlatforms: {
94                     'WK1': { fallbackPlatforms: ['EFL'], expectationsDirectory: 'efl-wk1' },
95                     'WK2': { fallbackPlatforms: ['EFL', 'WK2'], expectationsDirectory: 'efl-wk2' }
96                 }
97             }
98         }
99     },
100     'WK2': {
101         basePlatform: true,
102         expectationsDirectory: 'wk2'
103     }
104 };
105
106 var BUILD_TYPES = {'DEBUG': 'DBG', 'RELEASE': 'RELEASE'};
107 var MIN_SECONDS_FOR_SLOW_TEST = 4;
108 var MIN_SECONDS_FOR_SLOW_TEST_DEBUG = 2 * MIN_SECONDS_FOR_SLOW_TEST;
109 var FAIL_RESULTS = ['IMAGE', 'IMAGE+TEXT', 'TEXT', 'MISSING'];
110 var CHUNK_SIZE = 25;
111 var MAX_RESULTS = 1500;
112
113 // FIXME: Figure out how to make this not be hard-coded.
114 var VIRTUAL_SUITES = {
115     'platform/chromium/virtual/gpu/fast/canvas': 'fast/canvas',
116     'platform/chromium/virtual/gpu/canvas/philip': 'canvas/philip'
117 };
118
119 var resourceLoader;
120
121 function generatePage(historyInstance)
122 {
123     if (historyInstance.crossDashboardState.useTestData)
124         return;
125
126     document.body.innerHTML = '<div id="loading-ui">LOADING...</div>';
127     resourceLoader.showErrors();
128
129     // tests expands to all tests that match the CSV list.
130     // result expands to all tests that ever have the given result
131     if (historyInstance.dashboardSpecificState.tests || historyInstance.dashboardSpecificState.result)
132         generatePageForIndividualTests(individualTests());
133     else if (historyInstance.dashboardSpecificState.expectationsUpdate)
134         generatePageForExpectationsUpdate();
135     else
136         generatePageForBuilder(historyInstance.dashboardSpecificState.builder || currentBuilderGroup().defaultBuilder());
137
138     for (var builder in currentBuilders())
139         processTestResultsForBuilderAsync(builder);
140
141     postHeightChangedMessage();
142 }
143
144 function handleValidHashParameter(historyInstance, key, value)
145 {
146     switch(key) {
147     case 'tests':
148         history.validateParameter(historyInstance.dashboardSpecificState, key, value,
149             function() {
150                 return string.isValidName(value);
151             });
152         return true;
153
154     case 'result':
155         value = value.toUpperCase();
156         history.validateParameter(historyInstance.dashboardSpecificState, key, value,
157             function() {
158                 for (var result in LAYOUT_TEST_EXPECTATIONS_MAP_) {
159                     if (value == LAYOUT_TEST_EXPECTATIONS_MAP_[result])
160                         return true;
161                 }
162                 return false;
163             });
164         return true;
165
166     case 'builder':
167         history.validateParameter(historyInstance.dashboardSpecificState, key, value,
168             function() {
169                 return value in currentBuilders();
170             });
171
172         return true;
173
174     case 'sortColumn':
175         history.validateParameter(historyInstance.dashboardSpecificState, key, value,
176             function() {
177                 // Get all possible headers since the actual used set of headers
178                 // depends on the values in historyInstance.dashboardSpecificState, which are currently being set.
179                 var headers = tableHeaders(true);
180                 for (var i = 0; i < headers.length; i++) {
181                     if (value == sortColumnFromTableHeader(headers[i]))
182                         return true;
183                 }
184                 return value == 'test' || value == 'builder';
185             });
186         return true;
187
188     case 'sortOrder':
189         history.validateParameter(historyInstance.dashboardSpecificState, key, value,
190             function() {
191                 return value == FORWARD || value == BACKWARD;
192             });
193         return true;
194
195     case 'resultsHeight':
196     case 'updateIndex':
197     case 'revision':
198         history.validateParameter(historyInstance.dashboardSpecificState, key, Number(value),
199             function() {
200                 return value.match(/^\d+$/);
201             });
202         return true;
203
204     case 'showChrome':
205     case 'showCorrectExpectations':
206     case 'showWrongExpectations':
207     case 'showExpectations':
208     case 'showFlaky':
209     case 'showLargeExpectations':
210     case 'legacyExpectationsSemantics':
211     case 'showSkipped':
212     case 'showSlow':
213     case 'showUnexpectedPasses':
214     case 'showWontFixSkip':
215     case 'expectationsUpdate':
216         historyInstance.dashboardSpecificState[key] = value == 'true';
217         return true;
218
219     default:
220         return false;
221     }
222 }
223
224 // @param {Object} params New or modified query parameters as key: value.
225 function handleQueryParameterChange(historyInstance, params)
226 {
227     for (key in params) {
228         if (key == 'tests') {
229             // Entering cross-builder view, only keep valid keys for that view.
230             for (var currentKey in historyInstance.dashboardSpecificState) {
231               if (isInvalidKeyForCrossBuilderView(currentKey)) {
232                 delete historyInstance.dashboardSpecificState[currentKey];
233               }
234             }
235         } else if (isInvalidKeyForCrossBuilderView(key)) {
236             delete historyInstance.dashboardSpecificState.tests;
237             delete historyInstance.dashboardSpecificState.result;
238         }
239     }
240
241     return true;
242 }
243
244 var defaultDashboardSpecificStateValues = {
245     sortOrder: BACKWARD,
246     sortColumn: 'flakiness',
247     showExpectations: false,
248     showFlaky: true,
249     showLargeExpectations: false,
250     legacyExpectationsSemantics: true,
251     showChrome: true,
252     showCorrectExpectations: false,
253     showWrongExpectations: false,
254     showWontFixSkip: false,
255     showSlow: false,
256     showSkipped: false,
257     showUnexpectedPasses: false,
258     expectationsUpdate: false,
259     updateIndex: 0,
260     resultsHeight: 300,
261     revision: null,
262     tests: '',
263     result: '',
264     builder: null
265 };
266
267 var DB_SPECIFIC_INVALIDATING_PARAMETERS = {
268     'tests' : 'builder',
269     'testType': 'builder',
270     'group': 'builder'
271 };
272
273
274 var flakinessConfig = {
275     defaultStateValues: defaultDashboardSpecificStateValues,
276     generatePage: generatePage,
277     handleValidHashParameter: handleValidHashParameter,
278     handleQueryParameterChange: handleQueryParameterChange,
279     invalidatingHashParameters: DB_SPECIFIC_INVALIDATING_PARAMETERS
280 };
281
282 // FIXME(jparent): Eventually remove all usage of global history object.
283 var g_history = new history.History(flakinessConfig);
284 g_history.parseCrossDashboardParameters();
285
286 //////////////////////////////////////////////////////////////////////////////
287 // GLOBALS
288 //////////////////////////////////////////////////////////////////////////////
289
290 var g_perBuilderPlatformAndBuildType = {};
291 var g_perBuilderFailures = {};
292 // Map of builder to arrays of tests that are listed in the expectations file
293 // but have for that builder.
294 var g_perBuilderWithExpectationsButNoFailures = {};
295 // Map of builder to arrays of paths that are skipped. This shows the raw
296 // path used in TestExpectations rather than the test path since we
297 // don't actually have any data here for skipped tests.
298 var g_perBuilderSkippedPaths = {};
299 // Maps test path to an array of {builder, testResults} objects.
300 var g_testToResultsMap = {};
301 // Tests that the user wants to update expectations for.
302 var g_confirmedTests = {};
303
304 function traversePlatformsTree(callback)
305 {
306     function traverse(platformObject, parentPlatform) {
307         Object.keys(platformObject).forEach(function(platformName) {
308             var platform = platformObject[platformName];
309             platformName = parentPlatform ? parentPlatform + platformName : platformName;
310
311             if (platform.subPlatforms)
312                 traverse(platform.subPlatforms, platformName + '_');
313             else if (!platform.basePlatform)
314                 callback(platform, platformName);
315         });
316     }
317     traverse(PLATFORMS, null);
318 }
319
320 function createResultsObjectForTest(test, builder)
321 {
322     return {
323         test: test,
324         builder: builder,
325         // HTML for display of the results in the flakiness column
326         html: '',
327         flips: 0,
328         slowestTime: 0,
329         slowestNonTimeoutCrashTime: 0,
330         meetsExpectations: true,
331         isWontFixSkip: false,
332         isFlaky: false,
333         // Sorted string of missing expectations
334         missing: '',
335         // String of extra expectations (i.e. expectations that never occur).
336         extra: '',
337         modifiers: '',
338         bugs: '',
339         expectations : '',
340         rawResults: '',
341         // List of all the results the test actually has.
342         actualResults: []
343     };
344 }
345
346 function matchingElement(stringToMatch, elementsMap)
347 {
348     for (var element in elementsMap) {
349         if (string.contains(stringToMatch, elementsMap[element]))
350             return element;
351     }
352 }
353
354 function determineWKPlatform(builderName, basePlatform)
355 {
356     var isWK2Builder = string.contains(builderName, 'WK2') || string.contains(builderName, 'WEBKIT2');
357     return basePlatform + (isWK2Builder ? '_WK2' : '_WK1');
358 }
359
360 function nonChromiumPlatform(builderNameUpperCase)
361 {
362     if (string.contains(builderNameUpperCase, 'WINDOWS 7'))
363         return 'APPLE_WIN_WIN7';
364     if (string.contains(builderNameUpperCase, 'WINDOWS XP'))
365         return 'APPLE_WIN_XP';
366     if (string.contains(builderNameUpperCase, 'QT LINUX'))
367         return 'QT_LINUX';
368
369     if (string.contains(builderNameUpperCase, 'LION'))
370         return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_LION');
371     if (string.contains(builderNameUpperCase, 'SNOWLEOPARD'))
372         return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_SNOWLEOPARD');
373     if (string.contains(builderNameUpperCase, 'GTK LINUX'))
374         return determineWKPlatform(builderNameUpperCase, 'GTK_LINUX');
375     if (string.contains(builderNameUpperCase, 'EFL'))
376         return determineWKPlatform(builderNameUpperCase, 'EFL_LINUX');
377 }
378
379 function chromiumPlatform(builderNameUpperCase)
380 {
381     if (string.contains(builderNameUpperCase, 'MAC')) {
382         if (string.contains(builderNameUpperCase, '10.7'))
383             return 'CHROMIUM_LION';
384         // The webkit.org 'Chromium Mac Release (Tests)' bot runs SnowLeopard.
385         return 'CHROMIUM_SNOWLEOPARD';
386     }
387     if (string.contains(builderNameUpperCase, 'WIN7'))
388         return 'CHROMIUM_WIN7';
389     if (string.contains(builderNameUpperCase, 'VISTA'))
390         return 'CHROMIUM_VISTA';
391     if (string.contains(builderNameUpperCase, 'WIN') || string.contains(builderNameUpperCase, 'XP'))
392         return 'CHROMIUM_XP';
393     if (string.contains(builderNameUpperCase, 'LINUX'))
394         return 'CHROMIUM_LUCID';
395     if (string.contains(builderNameUpperCase, 'ANDROID'))
396         return 'CHROMIUM_ANDROID';
397     // The interactive bot is XP, but doesn't have an OS in it's name.
398     if (string.contains(builderNameUpperCase, 'INTERACTIVE'))
399         return 'CHROMIUM_XP';
400 }
401
402
403 function platformAndBuildType(builderName)
404 {
405     if (!g_perBuilderPlatformAndBuildType[builderName]) {
406         var builderNameUpperCase = builderName.toUpperCase();
407         
408         var platform = '';
409         if (g_history.isLayoutTestResults() && currentBuilderGroupName() == '@ToT - webkit.org' && !string.contains(builderNameUpperCase, 'CHROMIUM'))
410             platform = nonChromiumPlatform(builderNameUpperCase);
411         else
412             platform = chromiumPlatform(builderNameUpperCase);
413         
414         if (!platform)
415             console.error('Could not resolve platform for builder: ' + builderName);
416
417         var buildType = string.contains(builderNameUpperCase, 'DBG') || string.contains(builderNameUpperCase, 'DEBUG') ? 'DEBUG' : 'RELEASE';
418         g_perBuilderPlatformAndBuildType[builderName] = {platform: platform, buildType: buildType};
419     }
420     return g_perBuilderPlatformAndBuildType[builderName];
421 }
422
423 function isDebug(builderName)
424 {
425     return platformAndBuildType(builderName).buildType == 'DEBUG';
426 }
427
428 // Returns the expectation string for the given single character result.
429 // This string should match the expectations that are put into
430 // test_expectations.py.
431 //
432 // For example, if we start explicitly listing IMAGE result failures,
433 // this function should start returning 'IMAGE'.
434 function expectationsFileStringForResult(result)
435 {
436     // For the purposes of comparing against the expecations of a test,
437     // consider simplified diff failures as just text failures since
438     // the test_expectations file doesn't treat them specially.
439     if (result == 'S')
440         return 'TEXT';
441
442     if (result == 'N')
443         return '';
444
445     return expectationsMap()[result];
446 }
447
448 var TestTrie = function(builders, resultsByBuilder)
449 {
450     this._trie = {};
451
452     for (var builder in builders) {
453         var testsForBuilder = resultsByBuilder[builder].tests;
454         for (var test in testsForBuilder)
455             this._addTest(test.split('/'), this._trie);
456     }
457 }
458
459 TestTrie.prototype.forEach = function(callback, startingTriePath)
460 {
461     var testsTrie = this._trie;
462     if (startingTriePath) {
463         var splitPath = startingTriePath.split('/');
464         while (splitPath.length && testsTrie)
465             testsTrie = testsTrie[splitPath.shift()];
466     }
467
468     if (!testsTrie)
469         return;
470
471     function traverse(trie, triePath) {
472         if (trie == true)
473             callback(triePath);
474         else {
475             for (var member in trie)
476                 traverse(trie[member], triePath ? triePath + '/' + member : member);
477         }
478     }
479     traverse(testsTrie, startingTriePath);
480 }
481
482 TestTrie.prototype._addTest = function(test, trie)
483 {
484     var rootComponent = test.shift();
485     if (!test.length) {
486         if (!trie[rootComponent])
487             trie[rootComponent] = true;
488         return;
489     }
490
491     if (!trie[rootComponent] || trie[rootComponent] == true)
492         trie[rootComponent] = {};
493     this._addTest(test, trie[rootComponent]);
494 }
495
496 // Map of all tests to true values. This is just so we can have the list of
497 // all tests across all the builders.
498 var g_allTestsTrie;
499
500 function getAllTestsTrie()
501 {
502     if (!g_allTestsTrie)
503         g_allTestsTrie = new TestTrie(currentBuilders(), g_resultsByBuilder);
504
505     return g_allTestsTrie;
506 }
507
508 // Returns an array of tests to be displayed in the individual tests view.
509 // Note that a directory can be listed as a test, so we expand that into all
510 // tests in the directory.
511 function individualTests()
512 {
513     if (g_history.dashboardSpecificState.result)
514         return allTestsWithResult(g_history.dashboardSpecificState.result);
515
516     if (!g_history.dashboardSpecificState.tests)
517         return [];
518
519     return individualTestsForSubstringList();
520 }
521
522 function substringList()
523 {
524     // Convert windows slashes to unix slashes.
525     var tests = g_history.dashboardSpecificState.tests.replace(/\\/g, '/');
526     var separator = string.contains(tests, ' ') ? ' ' : ',';
527     var testList = tests.split(separator);
528
529     if (g_history.isLayoutTestResults())
530         return testList;
531
532     var testListWithoutModifiers = [];
533     testList.forEach(function(path) {
534         GTEST_MODIFIERS.forEach(function(modifier) {
535             path = path.replace('.' + modifier + '_', '.');
536         });
537         testListWithoutModifiers.push(path);
538     });
539     return testListWithoutModifiers;
540 }
541
542 function individualTestsForSubstringList()
543 {
544     var testList = substringList();
545
546     // Put the tests into an object first and then move them into an array
547     // as a way of deduping.
548     var testsMap = {};
549     for (var i = 0; i < testList.length; i++) {
550         var path = testList[i];
551
552         // Ignore whitespace entries as they'd match every test.
553         if (path.match(/^\s*$/))
554             continue;
555
556         var hasAnyMatches = false;
557         getAllTestsTrie().forEach(function(triePath) {
558             if (string.caseInsensitiveContains(triePath, path)) {
559                 testsMap[triePath] = 1;
560                 hasAnyMatches = true;
561             }
562         });
563
564         // If a path doesn't match any tests, then assume it's a full path
565         // to a test that passes on all builders.
566         if (!hasAnyMatches)
567             testsMap[path] = 1;
568     }
569
570     var testsArray = [];
571     for (var test in testsMap)
572         testsArray.push(test);
573     return testsArray;
574 }
575
576 // Returns whether this test's slowest time is above the cutoff for
577 // being a slow test.
578 function isSlowTest(resultsForTest)
579 {
580     var maxTime = isDebug(resultsForTest.builder) ? MIN_SECONDS_FOR_SLOW_TEST_DEBUG : MIN_SECONDS_FOR_SLOW_TEST;
581     return resultsForTest.slowestNonTimeoutCrashTime > maxTime;
582 }
583
584 // Returns whether this test's slowest time is *well* below the cutoff for
585 // being a slow test.
586 function isFastTest(resultsForTest)
587 {
588     var maxTime = isDebug(resultsForTest.builder) ? MIN_SECONDS_FOR_SLOW_TEST_DEBUG : MIN_SECONDS_FOR_SLOW_TEST;
589     return resultsForTest.slowestNonTimeoutCrashTime < maxTime / 2;
590 }
591
592 function allTestsWithResult(result)
593 {
594     processTestRunsForAllBuilders();
595     var retVal = [];
596
597     getAllTestsTrie().forEach(function(triePath) {
598         for (var i = 0; i < g_testToResultsMap[triePath].length; i++) {
599             if (g_testToResultsMap[triePath][i].actualResults.indexOf(result) != -1) {
600                 retVal.push(triePath);
601                 break;
602             }
603         }
604     });
605
606     return retVal;
607 }
608
609
610 // Adds all the tests for the given builder to the testMapToPopulate.
611 function addTestsForBuilder(builder, testMapToPopulate)
612 {
613     var tests = g_resultsByBuilder[builder].tests;
614     for (var test in tests) {
615         testMapToPopulate[test] = true;
616     }
617 }
618
619 // Map of all tests to true values by platform and build type.
620 // e.g. g_allTestsByPlatformAndBuildType['XP']['DEBUG'] will have the union
621 // of all tests run on the xp-debug builders.
622 var g_allTestsByPlatformAndBuildType = {};
623 traversePlatformsTree(function(platform, platformName) {
624     g_allTestsByPlatformAndBuildType[platformName] = {};
625 });
626
627 // Map of all tests to true values by platform and build type.
628 // e.g. g_allTestsByPlatformAndBuildType['WIN']['DEBUG'] will have the union
629 // of all tests run on the win-debug builders.
630 function allTestsWithSamePlatformAndBuildType(platform, buildType)
631 {
632     if (!g_allTestsByPlatformAndBuildType[platform][buildType]) {
633         var tests = {};
634         for (var thisBuilder in currentBuilders()) {
635             var thisBuilderBuildInfo = platformAndBuildType(thisBuilder);
636             if (thisBuilderBuildInfo.buildType == buildType && thisBuilderBuildInfo.platform == platform) {
637                 addTestsForBuilder(thisBuilder, tests);
638             }
639         }
640         g_allTestsByPlatformAndBuildType[platform][buildType] = tests;
641     }
642
643     return g_allTestsByPlatformAndBuildType[platform][buildType];
644 }
645
646 function getExpectations(test, platform, buildType)
647 {
648     var testObject = g_allExpectations[test];
649     if (!testObject)
650         return null;
651
652     var platformObject = testObject[platform];
653     if (!platformObject)
654         return null;
655         
656     return platformObject[buildType];
657 }
658
659 function filterBugs(modifiers)
660 {
661     var bugs = modifiers.match(/\b(Bug|webkit.org|crbug.com|code.google.com)\S*/g);
662     if (!bugs)
663         return {bugs: '', modifiers: modifiers};
664     for (var j = 0; j < bugs.length; j++)
665         modifiers = modifiers.replace(bugs[j], '');
666     return {bugs: bugs.join(' '), modifiers: string.collapseWhitespace(string.trimString(modifiers))};
667 }
668
669 function populateExpectationsData(resultsObject)
670 {
671     var buildInfo = platformAndBuildType(resultsObject.builder);
672     var expectations = getExpectations(resultsObject.test, buildInfo.platform, buildInfo.buildType);
673     if (!expectations)
674         return;
675
676     resultsObject.expectations = expectations.expectations;
677     var filteredModifiers = filterBugs(expectations.modifiers);
678     resultsObject.modifiers = filteredModifiers.modifiers;
679     resultsObject.bugs = filteredModifiers.bugs;
680     resultsObject.isWontFixSkip = string.contains(expectations.modifiers, 'WONTFIX') || string.contains(expectations.modifiers, 'SKIP'); 
681 }
682
683 function platformObjectForName(platformName)
684 {
685     var platformsList = platformName.split("_");
686     var platformObject = PLATFORMS[platformsList.shift()];
687     platformsList.forEach(function(platformName) {
688         platformObject = platformObject.subPlatforms[platformName];
689     });
690     return platformObject;
691 }
692
693 // Data structure to hold the processed expectations.
694 // g_allExpectations[testPath][platform][buildType] gets the object that has
695 // expectations and modifiers properties for this platform/buildType.
696 //
697 // platform and buildType both go through fallback sets of keys from most
698 // specific key to least specific. For example, on Windows XP, we first
699 // check the platform WIN-XP, if there's no such object, we check WIN,
700 // then finally we check ALL. For build types, we check the current
701 // buildType, then ALL.
702 var g_allExpectations;
703
704 function getParsedExpectations(data)
705 {
706     var expectations = [];
707     var lines = data.split('\n');
708     lines.forEach(function(line) {
709         line = string.trimString(line);
710         if (!line || string.startsWith(line, '#'))
711             return;
712
713         // This code mimics _tokenize_line_using_new_format() in
714         // Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py
715         //
716         // FIXME: consider doing more error checking here.
717         //
718         // FIXME: Clean this all up once we've fully cut over to the new syntax.
719         var tokens = line.split(/\s+/)
720         var parsed_bugs = [];
721         var parsed_modifiers = [];
722         var parsed_path;
723         var parsed_expectations = [];
724         var state = 'start';
725
726         // This clones _configuration_tokens_list in test_expectations.py.
727         // FIXME: unify with the platforms constants at the top of the file.
728         var configuration_tokens = {
729             'Release': 'RELEASE',
730             'Debug': 'DEBUG',
731             'Mac': 'MAC',
732             'Win': 'WIN',
733             'Linux': 'LINUX',
734             'SnowLeopard': 'SNOWLEOPARD',
735             'Lion': 'LION',
736             'MountainLion': 'MOUNTAINLION',
737             'Win7': 'WIN7',
738             'XP': 'XP',
739             'Vista': 'VISTA',
740             'Android': 'ANDROID',
741         };
742
743         var expectation_tokens = {
744             'Crash': 'CRASH',
745             'Failure': 'FAIL',
746             'ImageOnlyFailure': 'IMAGE',
747             'Missing': 'MISSING',
748             'Pass': 'PASS',
749             'Rebaseline': 'REBASELINE',
750             'Skip': 'SKIP',
751             'Slow': 'SLOW',
752             'Timeout': 'TIMEOUT',
753             'WontFix': 'WONTFIX',
754         };
755
756             
757         tokens.forEach(function(token) {
758           if (token.indexOf('Bug') != -1 ||
759               token.indexOf('webkit.org') != -1 ||
760               token.indexOf('crbug.com') != -1 ||
761               token.indexOf('code.google.com') != -1) {
762               parsed_bugs.push(token);
763           } else if (token == '[') {
764               if (state == 'start') {
765                   state = 'configuration';
766               } else if (state == 'name_found') {
767                   state = 'expectations';
768               }
769           } else if (token == ']') {
770               if (state == 'configuration') {
771                   state = 'name';
772               } else if (state == 'expectations') {
773                   state = 'done';
774               }
775           } else if (state == 'configuration') {
776               parsed_modifiers.push(configuration_tokens[token]);
777           } else if (state == 'expectations') {
778               if (token == 'Rebaseline' || token == 'Skip' || token == 'Slow' || token == 'WontFix') {
779                   parsed_modifiers.push(token.toUpperCase());
780               } else {
781                   parsed_expectations.push(expectation_tokens[token]);
782               }
783           } else if (token == '#') {
784               state = 'done';
785           } else if (state == 'name' || state == 'start') {
786               parsed_path = token;
787               state = 'name_found';
788           }
789         });
790
791         if (!parsed_expectations.length) {
792             if (parsed_modifiers.indexOf('Slow') == -1) {
793                 parsed_modifiers.push('Skip');
794                 parsed_expectations = ['Pass'];
795             }
796         }
797
798         // FIXME: Should we include line number and comment lines here?
799         expectations.push({
800             modifiers: parsed_bugs.concat(parsed_modifiers).join(' '),
801             path: parsed_path,
802             expectations: parsed_expectations.join(' '),
803         });
804     });
805     return expectations;
806 }
807
808
809 function addTestToAllExpectationsForPlatform(test, platformName, expectations, modifiers)
810 {
811     if (!g_allExpectations[test])
812         g_allExpectations[test] = {};
813
814     if (!g_allExpectations[test][platformName])
815         g_allExpectations[test][platformName] = {};
816
817     var allBuildTypes = [];
818     modifiers.split(' ').forEach(function(modifier) {
819         if (modifier in BUILD_TYPES) {
820             allBuildTypes.push(modifier);
821             return;
822         }
823     });
824     if (!allBuildTypes.length)
825         allBuildTypes = Object.keys(BUILD_TYPES);
826
827     allBuildTypes.forEach(function(buildType) {
828         g_allExpectations[test][platformName][buildType] = {modifiers: modifiers, expectations: expectations};
829     });
830 }
831
832 function processExpectationsForPlatform(platformObject, platformName, expectationsArray)
833 {
834     if (!g_allExpectations)
835         g_allExpectations = {};
836
837     if (!expectationsArray)
838         return;
839
840     // Sort the array to hit more specific paths last. More specific
841     // paths (e.g. foo/bar/baz.html) override entries for less-specific ones (e.g. foo/bar).
842     expectationsArray.sort(alphanumericCompare('path'));
843
844     for (var i = 0; i < expectationsArray.length; i++) {
845         var path = expectationsArray[i].path;
846         var modifiers = expectationsArray[i].modifiers;
847         var expectations = expectationsArray[i].expectations;
848
849         var shouldProcessExpectation = false;
850         var hasPlatformModifierUnions = false;
851         if (platformObject.fallbackPlatforms) {
852             platformObject.fallbackPlatforms.forEach(function(fallbackPlatform) {
853                 if (shouldProcessExpectation)
854                     return;
855
856                 var fallbackPlatformObject = platformObjectForName(fallbackPlatform);
857                 if (!fallbackPlatformObject.platformModifierUnions)
858                     return;
859
860                 modifiers.split(' ').forEach(function(modifier) {
861                     if (modifier in fallbackPlatformObject.platformModifierUnions) {
862                         hasPlatformModifierUnions = true;
863                         if (fallbackPlatformObject.platformModifierUnions[modifier].indexOf(platformName) != -1)
864                             shouldProcessExpectation = true;
865                     }
866                 });
867             });
868         }
869
870         if (!hasPlatformModifierUnions)
871             shouldProcessExpectation = true;
872
873         if (!shouldProcessExpectation)
874             continue;
875
876         getAllTestsTrie().forEach(function(triePath) {
877             addTestToAllExpectationsForPlatform(triePath, platformName, expectations, modifiers);
878         }, path);
879     }
880 }
881
882 function processExpectations()
883 {
884     // FIXME: An expectations-by-platform object should be passed into this function rather than checking
885     // for a global object. That way this function can be better tested and meaningful errors can
886     // be reported when expectations for a given platform are not found in that object.
887     if (!g_expectationsByPlatform)
888         return;
889
890     traversePlatformsTree(function(platform, platformName) {
891         if (platform.fallbackPlatforms) {
892             platform.fallbackPlatforms.forEach(function(fallbackPlatform) {
893                 if (fallbackPlatform in g_expectationsByPlatform)
894                     processExpectationsForPlatform(platform, platformName, g_expectationsByPlatform[fallbackPlatform]);
895             });
896         }
897
898         if (platformName in g_expectationsByPlatform)
899             processExpectationsForPlatform(platform, platformName, g_expectationsByPlatform[platformName]);
900     });
901
902     g_expectationsByPlatform = undefined;
903 }
904
905 function processMissingTestsWithExpectations(builder, platform, buildType)
906 {
907     var noFailures = [];
908     var skipped = [];
909
910     var allTestsForPlatformAndBuildType = allTestsWithSamePlatformAndBuildType(platform, buildType);
911     for (var test in g_allExpectations) {
912         var expectations = getExpectations(test, platform, buildType);
913
914         if (!expectations)
915             continue;
916
917         // Test has expectations, but no result in the builders results.
918         // This means it's either SKIP or passes on all builds.
919         if (!allTestsForPlatformAndBuildType[test] && !string.contains(expectations.modifiers, 'WONTFIX')) {
920             if (string.contains(expectations.modifiers, 'SKIP'))
921                 skipped.push(test);
922             else if (!expectations.expectations.match(/^\s*PASS\s*$/)) {
923                 // Don't show tests expected to always pass. This is used in ways like
924                 // the following:
925                 // foo/bar = FAIL
926                 // foo/bar/baz.html = PASS
927                 noFailures.push({test: test, expectations: expectations.expectations, modifiers: expectations.modifiers});
928             }
929         }
930     }
931
932     g_perBuilderSkippedPaths[builder] = skipped.sort();
933     g_perBuilderWithExpectationsButNoFailures[builder] = noFailures.sort();
934 }
935
936 function processTestResultsForBuilderAsync(builder)
937 {
938     setTimeout(function() { processTestRunsForBuilder(builder); }, 0);
939 }
940
941 function processTestRunsForAllBuilders()
942 {
943     for (var builder in currentBuilders())
944         processTestRunsForBuilder(builder);
945 }
946
947 function processTestRunsForBuilder(builderName)
948 {
949     if (g_perBuilderFailures[builderName])
950       return;
951
952     if (!g_resultsByBuilder[builderName]) {
953         console.error('No tests found for ' + builderName);
954         g_perBuilderFailures[builderName] = [];
955         return;
956     }
957
958     processExpectations();
959    
960     var buildInfo = platformAndBuildType(builderName);
961     var platform = buildInfo.platform;
962     var buildType = buildInfo.buildType;
963     processMissingTestsWithExpectations(builderName, platform, buildType);
964
965     var failures = [];
966     var allTestsForThisBuilder = g_resultsByBuilder[builderName].tests;
967
968     for (var test in allTestsForThisBuilder) {
969         var resultsForTest = createResultsObjectForTest(test, builderName);
970         populateExpectationsData(resultsForTest);
971
972         var rawTest = g_resultsByBuilder[builderName].tests[test];
973         resultsForTest.rawTimes = rawTest.times;
974         var rawResults = rawTest.results;
975         resultsForTest.rawResults = rawResults;
976
977         // FIXME: Switch to resultsByBuild
978         var times = resultsForTest.rawTimes;
979         var numTimesSeen = 0;
980         var numResultsSeen = 0;
981         var resultsIndex = 0;
982         var currentResult;
983         for (var i = 0; i < times.length; i++) {
984             numTimesSeen += times[i][RLE.LENGTH];
985
986             while (rawResults[resultsIndex] && numTimesSeen > (numResultsSeen + rawResults[resultsIndex][RLE.LENGTH])) {
987                 numResultsSeen += rawResults[resultsIndex][RLE.LENGTH];
988                 resultsIndex++;
989             }
990
991             if (rawResults && rawResults[resultsIndex])
992                 currentResult = rawResults[resultsIndex][RLE.VALUE];
993
994             var time = times[i][RLE.VALUE]
995
996             // Ignore times for crashing/timeout runs for the sake of seeing if
997             // a test should be marked slow.
998             if (currentResult != 'C' && currentResult != 'T')
999                 resultsForTest.slowestNonTimeoutCrashTime = Math.max(resultsForTest.slowestNonTimeoutCrashTime, time);
1000             resultsForTest.slowestTime = Math.max(resultsForTest.slowestTime, time);
1001         }
1002
1003         processMissingAndExtraExpectations(resultsForTest);
1004         failures.push(resultsForTest);
1005
1006         if (!g_testToResultsMap[test])
1007             g_testToResultsMap[test] = [];
1008         g_testToResultsMap[test].push(resultsForTest);
1009     }
1010
1011     g_perBuilderFailures[builderName] = failures;
1012 }
1013
1014 function processMissingAndExtraExpectations(resultsForTest)
1015 {
1016     // Heuristic for determining whether expectations apply to a given test:
1017     // -If a test result happens < MIN_RUNS_FOR_FLAKE, then consider it a flaky
1018     // result and include it in the list of expected results.
1019     // -Otherwise, grab the first contiguous set of runs with the same result
1020     // for >= MIN_RUNS_FOR_FLAKE and ignore all following runs >=
1021     // MIN_RUNS_FOR_FLAKE.
1022     // This lets us rule out common cases of a test changing expectations for
1023     // a few runs, then being fixed or otherwise modified in a non-flaky way.
1024     var rawResults = resultsForTest.rawResults;
1025
1026     // If the first result is no-data that means the test is skipped or is
1027     // being run on a different builder (e.g. moved from one shard to another).
1028     // Ignore these results since we have no real data about what's going on.
1029     if (rawResults[0][RLE.VALUE] == 'N')
1030         return;
1031
1032     // Only consider flake if it doesn't happen twice in a row.
1033     var MIN_RUNS_FOR_FLAKE = 2;
1034     var resultsMap = {}
1035     var numResultsSeen = 0;
1036     var haveSeenNonFlakeResult = false;
1037     var numRealResults = 0;
1038
1039     var seenResults = {};
1040     for (var i = 0; i < rawResults.length; i++) {
1041         var numResults = rawResults[i][RLE.LENGTH];
1042         numResultsSeen += numResults;
1043
1044         var result = rawResults[i][RLE.VALUE];
1045
1046         var hasMinRuns = numResults >= MIN_RUNS_FOR_FLAKE;
1047         if (haveSeenNonFlakeResult && hasMinRuns)
1048             continue;
1049         else if (hasMinRuns)
1050             haveSeenNonFlakeResult = true;
1051         else if (!seenResults[result]) {
1052             // Only consider a short-lived result if we've seen it more than once.
1053             // Otherwise, we include lots of false-positives due to tests that fail
1054             // for a couple runs and then start passing.
1055             seenResults[result] = true;
1056             continue;
1057         }
1058
1059         var expectation = expectationsFileStringForResult(result);
1060         resultsMap[expectation] = true;
1061         numRealResults++;
1062     }
1063
1064     resultsForTest.flips = i - 1;
1065     resultsForTest.isFlaky = numRealResults > 1;
1066
1067     var missingExpectations = [];
1068     var extraExpectations = [];
1069
1070     if (g_history.isLayoutTestResults()) {
1071         var expectationsArray = resultsForTest.expectations ? resultsForTest.expectations.split(' ') : [];
1072         extraExpectations = expectationsArray.filter(
1073             function(element) {
1074                 // FIXME: Once all the FAIL lines are removed from
1075                 // TestExpectations, delete all the legacyExpectationsSemantics
1076                 // code.
1077                 if (g_history.dashboardSpecificState.legacyExpectationsSemantics) {
1078                     if (element == 'FAIL') {
1079                         for (var i = 0; i < FAIL_RESULTS.length; i++) {
1080                             if (resultsMap[FAIL_RESULTS[i]])
1081                                 return false;
1082                         }
1083                         return true;
1084                     }
1085                 }
1086
1087                 return element && !resultsMap[element] && !string.contains(element, 'BUG');
1088             });
1089
1090         for (var result in resultsMap) {
1091             resultsForTest.actualResults.push(result);
1092             var hasExpectation = false;
1093             for (var i = 0; i < expectationsArray.length; i++) {
1094                 var expectation = expectationsArray[i];
1095                 // FIXME: Once all the FAIL lines are removed from
1096                 // TestExpectations, delete all the legacyExpectationsSemantics
1097                 // code.
1098                 if (g_history.dashboardSpecificState.legacyExpectationsSemantics) {
1099                     if (expectation == 'FAIL') {
1100                         for (var j = 0; j < FAIL_RESULTS.length; j++) {
1101                             if (result == FAIL_RESULTS[j]) {
1102                                 hasExpectation = true;
1103                                 break;
1104                             }
1105                         }
1106                     }
1107                 }
1108
1109                 if (result == expectation)
1110                     hasExpectation = true;
1111
1112                 if (hasExpectation)
1113                     break;
1114             }
1115             // If we have no expectations for a test and it only passes, then don't
1116             // list PASS as a missing expectation. We only want to list PASS if it
1117             // flaky passes, so there would be other expectations.
1118             if (!hasExpectation && !(!expectationsArray.length && result == 'PASS' && numRealResults == 1))
1119                 missingExpectations.push(result);
1120         }
1121
1122         // Only highlight tests that take > 2 seconds as needing to be marked as
1123         // slow. There are too many tests that take ~2 seconds every couple
1124         // hundred runs. It's not worth the manual maintenance effort.
1125         // Also, if a test times out, then it should not be marked as slow.
1126         var minTimeForNeedsSlow = isDebug(resultsForTest.builder) ? 2 : 1;
1127         if (isSlowTest(resultsForTest) && !resultsMap['TIMEOUT'] && (!resultsForTest.modifiers || !string.contains(resultsForTest.modifiers, 'SLOW')))
1128             missingExpectations.push('SLOW');
1129         else if (isFastTest(resultsForTest) && resultsForTest.modifiers && string.contains(resultsForTest.modifiers, 'SLOW'))
1130             extraExpectations.push('SLOW');
1131
1132         // If there are no missing results or modifiers besides build
1133         // type, platform, or bug and the expectations are all extra
1134         // that is, extraExpectations - expectations = PASS,
1135         // include PASS as extra, since that means this line in
1136         // test_expectations can be deleted..
1137         if (!missingExpectations.length && !(resultsForTest.modifiers && realModifiers(resultsForTest.modifiers))) {
1138             var extraPlusPass = extraExpectations.concat(['PASS']);
1139             if (extraPlusPass.sort().toString() == expectationsArray.slice(0).sort().toString())
1140                 extraExpectations.push('PASS');
1141         }
1142
1143     }
1144
1145     resultsForTest.meetsExpectations = !missingExpectations.length && !extraExpectations.length;
1146     resultsForTest.missing = missingExpectations.sort().join(' ');
1147     resultsForTest.extra = extraExpectations.sort().join(' ');
1148 }
1149
1150
1151 var BUG_URL_PREFIX = '<a href="http://';
1152 var BUG_URL_POSTFIX = '/$1">crbug.com/$1</a> ';
1153 var WEBKIT_BUG_URL_POSTFIX = '/$1">webkit.org/b/$1</a> ';
1154 var INTERNAL_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'b' + BUG_URL_POSTFIX;
1155 var EXTERNAL_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'crbug.com' + BUG_URL_POSTFIX;
1156 var WEBKIT_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'webkit.org/b' + WEBKIT_BUG_URL_POSTFIX;
1157
1158 function htmlForBugs(bugs)
1159 {
1160     bugs = bugs.replace(/crbug.com\/(\d+)(\ |$)/g, EXTERNAL_BUG_REPLACE_VALUE);
1161     bugs = bugs.replace(/webkit.org\/b\/(\d+)(\ |$)/g, WEBKIT_BUG_REPLACE_VALUE);
1162     return bugs;
1163 }
1164
1165 function linkHTMLToOpenWindow(url, text)
1166 {
1167     return '<a href="' + url + '" target="_blank">' + text + '</a>';
1168 }
1169
1170 // FIXME: replaced with ui.html.chromiumRevisionLink/ui.html.webKitRevisionLink
1171 function createBlameListHTML(revisions, index, urlBase, separator, repo)
1172 {
1173     var thisRevision = revisions[index];
1174     if (!thisRevision)
1175         return '';
1176
1177     var previousRevision = revisions[index + 1];
1178     if (previousRevision && previousRevision != thisRevision) {
1179         previousRevision++;
1180         return linkHTMLToOpenWindow(urlBase + thisRevision + separator + previousRevision,
1181             repo + ' blamelist r' + previousRevision + ':r' + thisRevision);
1182     } else
1183         return 'At ' + repo + ' revision: ' + thisRevision;
1184 }
1185
1186 // Returns whether the result for index'th result for testName on builder was
1187 // a failure.
1188 function isFailure(builder, testName, index)
1189 {
1190     var currentIndex = 0;
1191     var rawResults = g_resultsByBuilder[builder].tests[testName].results;
1192     for (var i = 0; i < rawResults.length; i++) {
1193         currentIndex += rawResults[i][RLE.LENGTH];
1194         if (currentIndex > index)
1195             return isFailingResult(rawResults[i][RLE.VALUE]);
1196     }
1197     console.error('Index exceeds number of results: ' + index);
1198 }
1199
1200 // Returns an array of indexes for all builds where this test failed.
1201 function indexesForFailures(builder, testName)
1202 {
1203     var rawResults = g_resultsByBuilder[builder].tests[testName].results;
1204     var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
1205     var index = 0;
1206     var failures = [];
1207     for (var i = 0; i < rawResults.length; i++) {
1208         var numResults = rawResults[i][RLE.LENGTH];
1209         if (isFailingResult(rawResults[i][RLE.VALUE])) {
1210             for (var j = 0; j < numResults; j++)
1211                 failures.push(index + j);
1212         }
1213         index += numResults;
1214     }
1215     return failures;
1216 }
1217
1218 // Returns the path to the failure log for this non-webkit test.
1219 function pathToFailureLog(testName)
1220 {
1221     return '/steps/' + g_history.crossDashboardState.testType + '/logs/' + testName.split('.')[1]
1222 }
1223
1224 function showPopupForBuild(e, builder, index, opt_testName)
1225 {
1226     var html = '';
1227
1228     var time = g_resultsByBuilder[builder].secondsSinceEpoch[index];
1229     if (time) {
1230         var date = new Date(time * 1000);
1231         html += date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
1232     }
1233
1234     var buildNumber = g_resultsByBuilder[builder].buildNumbers[index];
1235     var master = builderMaster(builder);
1236     var buildBasePath = master.logPath(builder, buildNumber);
1237
1238     html += '<ul><li>' + linkHTMLToOpenWindow(buildBasePath, 'Build log') +
1239         '</li><li>' +
1240         createBlameListHTML(g_resultsByBuilder[builder].webkitRevision, index,
1241             'http://trac.webkit.org/log/?verbose=on&rev=', '&stop_rev=',
1242             'WebKit') +
1243         '</li>';
1244
1245     if (master.name == WEBKIT_BUILDER_MASTER) {
1246         var revision = g_resultsByBuilder[builder].webkitRevision[index];
1247         html += '<li><span class=link onclick="g_history.setQueryParameter(\'revision\',' +
1248             revision + ')">Show results for WebKit r' + revision +
1249             '</span></li>';
1250     } else {
1251         html += '<li>' +
1252             createBlameListHTML(g_resultsByBuilder[builder].chromeRevision, index,
1253                 'http://build.chromium.org/f/chromium/perf/dashboard/ui/changelog.html?url=/trunk/src&mode=html&range=', ':', 'Chrome') +
1254             '</li>';
1255
1256         var chromeRevision = g_resultsByBuilder[builder].chromeRevision[index];
1257         if (chromeRevision && g_history.isLayoutTestResults()) {
1258             html += '<li><a href="' + TEST_RESULTS_BASE_PATH + currentBuilders()[builder] +
1259                 '/' + chromeRevision + '/layout-test-results.zip">layout-test-results.zip</a></li>';
1260         }
1261     }
1262
1263     if (!g_history.isLayoutTestResults() && opt_testName && isFailure(builder, opt_testName, index))
1264         html += '<li>' + linkHTMLToOpenWindow(buildBasePath + pathToFailureLog(opt_testName), 'Failure log') + '</li>';
1265
1266     html += '</ul>';
1267     ui.popup.show(e.target, html);
1268 }
1269
1270 function htmlForTestResults(test)
1271 {
1272     var html = '';
1273     var results = test.rawResults.concat();
1274     var times = test.rawTimes.concat();
1275     var builder = test.builder;
1276     var master = builderMaster(builder);
1277     var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
1278
1279     var indexToReplaceCurrentResult = -1;
1280     var indexToReplaceCurrentTime = -1;
1281     var currentResultArray, currentTimeArray, currentResult, innerHTML, resultString;
1282     for (var i = 0; i < buildNumbers.length; i++) {
1283         if (i > indexToReplaceCurrentResult) {
1284             currentResultArray = results.shift();
1285             if (currentResultArray) {
1286                 currentResult = currentResultArray[RLE.VALUE];
1287                 // Treat simplified diff failures as just text failures.
1288                 if (currentResult == 'S')
1289                     currentResult = 'F';
1290                 indexToReplaceCurrentResult += currentResultArray[RLE.LENGTH];
1291             } else {
1292                 currentResult = 'N';
1293                 indexToReplaceCurrentResult += buildNumbers.length;
1294             }
1295             resultString = expectationsFileStringForResult(currentResult);
1296         }
1297
1298         if (i > indexToReplaceCurrentTime) {
1299             currentTimeArray = times.shift();
1300             var currentTime = 0;
1301             if (currentResultArray) {
1302               currentTime = currentTimeArray[RLE.VALUE];
1303               indexToReplaceCurrentTime += currentTimeArray[RLE.LENGTH];
1304             } else
1305               indexToReplaceCurrentTime += buildNumbers.length;
1306
1307             innerHTML = currentTime || '&nbsp;';
1308         }
1309
1310         var extraClassNames = '';
1311         var webkitRevision = g_resultsByBuilder[builder].webkitRevision;
1312         var isWebkitMerge = webkitRevision[i + 1] && webkitRevision[i] != webkitRevision[i + 1];
1313         if (isWebkitMerge && master.name != WEBKIT_BUILDER_MASTER)
1314             extraClassNames += ' merge';
1315
1316         html += '<td title="' + (resultString || 'NO DATA') + '. Click for more info." class="results ' + currentResult +
1317           extraClassNames + '" onclick=\'showPopupForBuild(event, "' + builder + '",' + i + ',"' + test.test + '")\'>' + innerHTML;
1318     }
1319     return html;
1320 }
1321
1322 function htmlForTestsWithExpectationsButNoFailures(builder)
1323 {
1324     var tests = g_perBuilderWithExpectationsButNoFailures[builder];
1325     var skippedPaths = g_perBuilderSkippedPaths[builder];
1326     var showUnexpectedPassesLink =  linkHTMLToToggleState('showUnexpectedPasses', 'tests that have not failed in last ' + g_resultsByBuilder[builder].buildNumbers.length + ' runs');
1327     var showSkippedLink = linkHTMLToToggleState('showSkipped', 'skipped tests in TestExpectations');
1328     
1329     var html = '';
1330     if (g_history.isLayoutTestResults() && (tests.length || skippedPaths.length)) {
1331         var buildInfo = platformAndBuildType(builder);
1332         html += '<h2 style="display:inline-block">Expectations for ' + buildInfo.platform + '-' + buildInfo.buildType + '</h2> ';
1333         if (!g_history.dashboardSpecificState.showUnexpectedPasses && tests.length)
1334             html += showUnexpectedPassesLink;
1335         html += ' ';
1336         if (!g_history.dashboardSpecificState.showSkipped && skippedPaths.length)
1337             html += showSkippedLink;
1338     }
1339
1340     var open = '<div onclick="selectContents(this)">';
1341
1342     if (g_history.dashboardSpecificState.showUnexpectedPasses && tests.length) {
1343         html += '<div id="passing-tests">' + showUnexpectedPassesLink;
1344         for (var i = 0; i < tests.length; i++)
1345             html += open + tests[i].test + '</div>';
1346         html += '</div>';
1347     }
1348
1349     if (g_history.dashboardSpecificState.showSkipped && skippedPaths.length)
1350         html += '<div id="skipped-tests">' + showSkippedLink + open + skippedPaths.join('</div>' + open) + '</div></div>';
1351     return html + '<br>';
1352 }
1353
1354 // Returns whether we should exclude test results from the test table.
1355 function shouldHideTest(testResult)
1356 {
1357     // For non-layout tests, we always show everything.
1358     if (!g_history.isLayoutTestResults())
1359         return false;
1360
1361     if (testResult.isWontFixSkip)
1362         return !g_history.dashboardSpecificState.showWontFixSkip;
1363
1364     if (testResult.isFlaky)
1365         return !g_history.dashboardSpecificState.showFlaky;
1366
1367     if (isSlowTest(testResult))
1368         return !g_history.dashboardSpecificState.showSlow;
1369
1370     if (testResult.meetsExpectations)
1371         return !g_history.dashboardSpecificState.showCorrectExpectations;
1372
1373     return !g_history.dashboardSpecificState.showWrongExpectations;
1374 }
1375
1376 // Sets the browser's selection to the element's contents.
1377 function selectContents(element)
1378 {
1379     window.getSelection().selectAllChildren(element);
1380 }
1381
1382 function createBugHTML(test)
1383 {
1384     var symptom = test.isFlaky ? 'flaky' : 'failing';
1385     var title = encodeURIComponent('Layout Test ' + test.test + ' is ' + symptom);
1386     var description = encodeURIComponent('The following layout test is ' + symptom + ' on ' +
1387         '[insert platform]\n\n' + test.test + '\n\nProbable cause:\n\n' +
1388         '[insert probable cause]');
1389     
1390     var component = encodeURIComponent('Tools / Tests');
1391     url = 'https://bugs.webkit.org/enter_bug.cgi?assigned_to=webkit-unassigned%40lists.webkit.org&product=WebKit&form_name=enter_bug&component=' + component + '&short_desc=' + title + '&comment=' + description;
1392     return '<a href="' + url + '" class="file-bug">FILE BUG</a>';
1393 }
1394
1395 function isCrossBuilderView()
1396 {
1397     return g_history.dashboardSpecificState.tests || g_history.dashboardSpecificState.result || g_history.dashboardSpecificState.expectationsUpdate;
1398 }
1399
1400 function tableHeaders(opt_getAll)
1401 {
1402     var headers = [];
1403     if (isCrossBuilderView() || opt_getAll)
1404         headers.push('builder');
1405
1406     if (!isCrossBuilderView() || opt_getAll)
1407         headers.push('test');
1408
1409     if (g_history.isLayoutTestResults() || opt_getAll)
1410         headers.push('bugs', 'modifiers', 'expectations');
1411
1412     headers.push('slowest run', 'flakiness (numbers are runtimes in seconds)');
1413     return headers;
1414 }
1415
1416 function htmlForSingleTestRow(test)
1417 {
1418     if (!isCrossBuilderView() && shouldHideTest(test)) {
1419         // The innerHTML call is considerably faster if we exclude the rows for
1420         // items we're not showing than if we hide them using display:none.
1421         // For the crossBuilderView, we want to show all rows the user is
1422         // explicitly listing tests to view.
1423         return '';
1424     }
1425
1426     var headers = tableHeaders();
1427     var html = '';
1428     for (var i = 0; i < headers.length; i++) {
1429         var header = headers[i];
1430         if (string.startsWith(header, 'test') || string.startsWith(header, 'builder')) {
1431             // If isCrossBuilderView() is true, we're just viewing a single test
1432             // with results for many builders, so the first column is builder names
1433             // instead of test paths.
1434             var testCellClassName = 'test-link' + (isCrossBuilderView() ? ' builder-name' : '');
1435             var testCellHTML = isCrossBuilderView() ? test.builder : '<span class="link" onclick="g_history.setQueryParameter(\'tests\',\'' + test.test +'\');">' + test.test + '</span>';
1436
1437             html += '<tr><td class="' + testCellClassName + '">' + testCellHTML;
1438         } else if (string.startsWith(header, 'bugs'))
1439             html += '<td class=options-container>' + (test.bugs ? htmlForBugs(test.bugs) : createBugHTML(test));
1440         else if (string.startsWith(header, 'modifiers'))
1441             html += '<td class=options-container>' + test.modifiers;
1442         else if (string.startsWith(header, 'expectations'))
1443             html += '<td class=options-container>' + test.expectations;
1444         else if (string.startsWith(header, 'slowest'))
1445             html += '<td>' + (test.slowestTime ? test.slowestTime + 's' : '');
1446         else if (string.startsWith(header, 'flakiness'))
1447             html += htmlForTestResults(test);
1448     }
1449     return html;
1450 }
1451
1452 function sortColumnFromTableHeader(headerText)
1453 {
1454     return headerText.split(' ', 1)[0];
1455 }
1456
1457 function htmlForTableColumnHeader(headerName, opt_fillColSpan)
1458 {
1459     // Use the first word of the header title as the sortkey
1460     var thisSortValue = sortColumnFromTableHeader(headerName);
1461     var arrowHTML = thisSortValue == g_history.dashboardSpecificState.sortColumn ?
1462         '<span class=' + g_history.dashboardSpecificState.sortOrder + '>' + (g_history.dashboardSpecificState.sortOrder == FORWARD ? '&uarr;' : '&darr;' ) + '</span>' : '';
1463     return '<th sortValue=' + thisSortValue +
1464         // Extend last th through all the rest of the columns.
1465         (opt_fillColSpan ? ' colspan=10000' : '') +
1466         // Extra span here is so flex boxing actually centers.
1467         // There's probably a better way to do this with CSS only though.
1468         '><div class=table-header-content><span></span>' + arrowHTML +
1469         '<span class=header-text>' + headerName + '</span>' + arrowHTML + '</div></th>';
1470 }
1471
1472 function htmlForTestTable(rowsHTML, opt_excludeHeaders)
1473 {
1474     var html = '<table class=test-table>';
1475     if (!opt_excludeHeaders) {
1476         html += '<thead><tr>';
1477         var headers = tableHeaders();
1478         for (var i = 0; i < headers.length; i++)
1479             html += htmlForTableColumnHeader(headers[i], i == headers.length - 1);
1480         html += '</tr></thead>';
1481     }
1482     return html + '<tbody>' + rowsHTML + '</tbody></table>';
1483 }
1484
1485 function appendHTML(html)
1486 {
1487     // InnerHTML to a div that's not in the document. This is
1488     // ~300ms faster in Safari 4 and Chrome 4 on mac.
1489     var div = document.createElement('div');
1490     div.innerHTML = html;
1491     document.body.appendChild(div);
1492     postHeightChangedMessage();
1493 }
1494
1495 function alphanumericCompare(column, reverse)
1496 {
1497     return reversibleCompareFunction(function(a, b) {
1498         // Put null entries at the bottom
1499         var a = a[column] ? String(a[column]) : 'z';
1500         var b = b[column] ? String(b[column]) : 'z';
1501
1502         if (a < b)
1503             return -1;
1504         else if (a == b)
1505             return 0;
1506         else
1507             return 1;
1508     }, reverse);
1509 }
1510
1511 function numericSort(column, reverse)
1512 {
1513     return reversibleCompareFunction(function(a, b) {
1514         a = parseFloat(a[column]);
1515         b = parseFloat(b[column]);
1516         return a - b;
1517     }, reverse);
1518 }
1519
1520 function reversibleCompareFunction(compare, reverse)
1521 {
1522     return function(a, b) {
1523         return compare(reverse ? b : a, reverse ? a : b);
1524     };
1525 }
1526
1527 function changeSort(e)
1528 {
1529     var target = e.currentTarget;
1530     e.preventDefault();
1531
1532     var sortValue = target.getAttribute('sortValue');
1533     while (target && target.tagName != 'TABLE')
1534         target = target.parentNode;
1535
1536     var sort = 'sortColumn';
1537     var orderKey = 'sortOrder';
1538     if (sortValue == g_history.dashboardSpecificState[sort] && g_history.dashboardSpecificState[orderKey] == FORWARD)
1539         order = BACKWARD;
1540     else
1541         order = FORWARD;
1542
1543     g_history.setQueryParameter(sort, sortValue, orderKey, order);
1544 }
1545
1546 function sortTests(tests, column, order)
1547 {
1548     var resultsProperty, sortFunctionGetter;
1549     if (column == 'flakiness') {
1550         sortFunctionGetter = numericSort;
1551         resultsProperty = 'flips';
1552     } else if (column == 'slowest') {
1553         sortFunctionGetter = numericSort;
1554         resultsProperty = 'slowestTime';
1555     } else {
1556         sortFunctionGetter = alphanumericCompare;
1557         resultsProperty = column;
1558     }
1559
1560     tests.sort(sortFunctionGetter(resultsProperty, order == BACKWARD));
1561 }
1562
1563 // Sorts a space separated expectations string in alphanumeric order.
1564 // @param {string} str The expectations string.
1565 // @return {string} The sorted string.
1566 function sortExpectationsString(str)
1567 {
1568     return str.split(' ').sort().join(' ');
1569 }
1570
1571 function addUpdate(testsNeedingUpdate, test, builderName, missing, extra)
1572 {
1573     if (!testsNeedingUpdate[test])
1574         testsNeedingUpdate[test] = {};
1575
1576     var buildInfo = platformAndBuildType(builderName);
1577     var builder = buildInfo.platform + ' ' + buildInfo.buildType;
1578     if (!testsNeedingUpdate[test][builder])
1579         testsNeedingUpdate[test][builder] = {};
1580
1581     if (missing)
1582         testsNeedingUpdate[test][builder].missing = sortExpectationsString(missing);
1583
1584     if (extra)
1585         testsNeedingUpdate[test][builder].extra = sortExpectationsString(extra);
1586 }
1587
1588
1589 // From a string of modifiers, returns a string of modifiers that
1590 // are for real result changes, like SLOW, and excludes modifiers
1591 // that specificy things like platform, build_type, bug.
1592 // @param {string} modifierString String containing all modifiers.
1593 // @return {string} String containing only modifiers that effect the results.
1594 function realModifiers(modifierString)
1595 {
1596     var modifiers = modifierString.split(' ');;
1597     return modifiers.filter(function(modifier) {
1598         if (modifier in BUILD_TYPES || string.startsWith(modifier, 'BUG'))
1599             return false;
1600
1601         var matchesPlatformOrUnion = false;
1602         traversePlatformsTree(function(platform, platformName) {
1603             if (matchesPlatformOrUnion)
1604                 return;
1605
1606             if (platform.fallbackPlatforms) {
1607                 platform.fallbackPlatforms.forEach(function(fallbackPlatform) {
1608                     if (matchesPlatformOrUnion)
1609                         return;
1610
1611                     var fallbackPlatformObject = platformObjectForName(fallbackPlatform);
1612                     if (!fallbackPlatformObject.platformModifierUnions)
1613                         return;
1614
1615                     matchesPlatformOrUnion = modifier in fallbackPlatformObject.subPlatforms || modifier in fallbackPlatformObject.platformModifierUnions;
1616                 });
1617             }
1618         });
1619
1620         return !matchesPlatformOrUnion;
1621     }).join(' ');
1622 }
1623
1624 function generatePageForExpectationsUpdate()
1625 {
1626     // Always show all runs when auto-updating expectations.
1627     if (!g_history.crossDashboardState.showAllRuns)
1628         g_history.setQueryParameter('showAllRuns', true);
1629
1630     processTestRunsForAllBuilders();
1631     var testsNeedingUpdate = {};
1632     for (var test in g_testToResultsMap) {
1633         var results = g_testToResultsMap[test];
1634         for (var i = 0; i < results.length; i++) {
1635             var thisResult = results[i];
1636             
1637             if (!thisResult.missing && !thisResult.extra)
1638                 continue;
1639
1640             var allPassesOrNoDatas = thisResult.rawResults.filter(function (x) { return x[1] != "P" && x[1] != "N"; }).length == 0;
1641
1642             if (allPassesOrNoDatas)
1643                 continue;
1644
1645             addUpdate(testsNeedingUpdate, test, thisResult.builder, thisResult.missing, thisResult.extra);
1646         }
1647     }
1648
1649     for (var builder in currentBuilders()) {
1650         var tests = g_perBuilderWithExpectationsButNoFailures[builder]
1651         for (var i = 0; i < tests.length; i++) {
1652             // Anything extra in this case is what is listed in expectations
1653             // plus modifiers other than bug, platform, build type.
1654             var modifiers = realModifiers(tests[i].modifiers);
1655             var extras = tests[i].expectations;
1656             extras += modifiers ? ' ' + modifiers : '';
1657             addUpdate(testsNeedingUpdate, tests[i].test, builder, null, extras);
1658         }
1659     }
1660
1661     // Get the keys in alphabetical order, so it is easy to process groups
1662     // of tests.
1663     var keys = Object.keys(testsNeedingUpdate).sort();
1664     showUpdateInfoForTest(testsNeedingUpdate, keys);
1665 }
1666
1667 // Show the test results and the json for differing expectations, and
1668 // allow the user to include or exclude this update.
1669 //
1670 // @param {Object} testsNeedingUpdate Tests that need updating.
1671 // @param {Array.<string>} keys Keys into the testNeedingUpdate object.
1672 function showUpdateInfoForTest(testsNeedingUpdate, keys)
1673 {
1674     var test = keys[g_history.dashboardSpecificState.updateIndex];
1675     document.body.innerHTML = '';
1676
1677     // FIXME: Make this DOM creation less verbose.
1678     var index = document.createElement('div');
1679     index.style.cssFloat = 'right';
1680     index.textContent = (g_history.dashboardSpecificState.updateIndex + 1) + ' of ' + keys.length + ' tests';
1681     document.body.appendChild(index);
1682
1683     var buttonRegion = document.createElement('div');
1684     var includeBtn = document.createElement('input');
1685     includeBtn.type = 'button';
1686     includeBtn.value = 'include selected';
1687     includeBtn.addEventListener('click', partial(handleUpdate, testsNeedingUpdate, keys), false);
1688     buttonRegion.appendChild(includeBtn);
1689
1690     var previousBtn = document.createElement('input');
1691     previousBtn.type = 'button';
1692     previousBtn.value = 'previous';
1693     previousBtn.addEventListener('click',
1694         function() {
1695           setUpdateIndex(g_history.dashboardSpecificState.updateIndex - 1, testsNeedingUpdate, keys);
1696         },
1697         false);
1698     buttonRegion.appendChild(previousBtn);
1699
1700     var nextBtn = document.createElement('input');
1701     nextBtn.type = 'button';
1702     nextBtn.value = 'next';
1703     nextBtn.addEventListener('click', partial(nextUpdate, testsNeedingUpdate, keys), false);
1704     buttonRegion.appendChild(nextBtn);
1705
1706     var doneBtn = document.createElement('input');
1707     doneBtn.type = 'button';
1708     doneBtn.value = 'done';
1709     doneBtn.addEventListener('click', finishUpdate, false);
1710     buttonRegion.appendChild(doneBtn);
1711
1712     document.body.appendChild(buttonRegion);
1713
1714     var updates = testsNeedingUpdate[test];
1715     var checkboxes = document.createElement('div');
1716     for (var builder in updates) {
1717         // Create a checkbox for each builder.
1718         var checkboxRegion = document.createElement('div');
1719         var checkbox = document.createElement('input');
1720         checkbox.type = 'checkbox';
1721         checkbox.id = builder;
1722         checkbox.checked = true;
1723         checkboxRegion.appendChild(checkbox);
1724         checkboxRegion.appendChild(document.createTextNode(builder + ' : ' + JSON.stringify(updates[builder])));
1725         checkboxes.appendChild(checkboxRegion);
1726     }
1727     document.body.appendChild(checkboxes);
1728
1729     var div = document.createElement('div');
1730     div.innerHTML = htmlForIndividualTestOnAllBuildersWithResultsLinks(test);
1731     document.body.appendChild(div);
1732     appendExpectations();
1733 }
1734
1735
1736 // When the user has finished selecting expectations to update, provide them
1737 // with json to copy over.
1738 function finishUpdate()
1739 {
1740     document.body.innerHTML = 'The next step is to copy the output below ' +
1741         'into a local file and save it.  Then, run<br><code>python ' +
1742         'src/webkit/tools/layout_tests/webkitpy/layout_tests/update_expectat' +
1743         'ions_from_dashboard.py path/to/local/file</code><br>in order to ' +
1744         'update the expectations file.<br><textarea id="results" '+
1745         'style="width:600px;height:600px;"> ' +
1746         JSON.stringify(g_confirmedTests) + '</textarea>';
1747     results.focus();
1748     document.execCommand('SelectAll');
1749 }
1750
1751 // Handle user click on "include selected" button.
1752 // Includes the tests that are selected and exclude the rest.
1753 // @param {Object} testsNeedingUpdate Tests that need updating.
1754 // @param {Array.<string>} keys Keys into the testNeedingUpdate object.
1755 function handleUpdate(testsNeedingUpdate, keys)
1756 {
1757     var test = keys[g_history.dashboardSpecificState.updateIndex];
1758     var updates = testsNeedingUpdate[test];
1759     for (var builder in updates) {
1760         // Add included tests, and delete excluded tests if
1761         // they were previously included.
1762         if ($(builder).checked) {
1763             if (!g_confirmedTests[test])
1764                 g_confirmedTests[test] = {};
1765             g_confirmedTests[test][builder] = testsNeedingUpdate[test][builder];
1766         } else if (g_confirmedTests[test] && g_confirmedTests[test][builder]) {
1767             delete g_confirmedTests[test][builder];
1768             if (!Object.keys(g_confirmedTests[test]).length)
1769                 delete g_confirmedTests[test];
1770         }
1771     }
1772     nextUpdate(testsNeedingUpdate, keys);
1773 }
1774
1775
1776 // Move to the next item to update.
1777 // @param {Object} testsNeedingUpdate Tests that need updating.
1778 // @param {Array.<string>} keys Keys into the testNeedingUpdate object.
1779 function nextUpdate(testsNeedingUpdate, keys)
1780 {
1781     setUpdateIndex(g_history.dashboardSpecificState.updateIndex + 1, testsNeedingUpdate, keys);
1782 }
1783
1784
1785 // Advance the index we are updating at.  If we walk over the end
1786 // or beginning, just loop.
1787 // @param {string} newIndex The index into the keys to move to.
1788 // @param {Object} testsNeedingUpdate Tests that need updating.
1789 // @param {Array.<string>} keys Keys into the testNeedingUpdate object.
1790 function setUpdateIndex(newIndex, testsNeedingUpdate, keys)
1791 {
1792     if (newIndex == -1)
1793         newIndex = keys.length - 1;
1794     else if (newIndex == keys.length)
1795         newIndex = 0;
1796     g_history.setQueryParameter("updateIndex", newIndex);
1797     showUpdateInfoForTest(testsNeedingUpdate, keys);
1798 }
1799
1800 function htmlForIndividualTestOnAllBuilders(test)
1801 {
1802     processTestRunsForAllBuilders();
1803
1804     var testResults = g_testToResultsMap[test];
1805     if (!testResults)
1806         return '<div class="not-found">Test not found. Either it does not exist, is skipped or passes on all platforms.</div>';
1807         
1808     var html = '';
1809     var shownBuilders = [];
1810     for (var j = 0; j < testResults.length; j++) {
1811         shownBuilders.push(testResults[j].builder);
1812         html += htmlForSingleTestRow(testResults[j]);
1813     }
1814
1815     var skippedBuilders = []
1816     for (builder in currentBuilders()) {
1817         if (shownBuilders.indexOf(builder) == -1)
1818             skippedBuilders.push(builder);
1819     }
1820
1821     var skippedBuildersHtml = '';
1822     if (skippedBuilders.length) {
1823         skippedBuildersHtml = '<div>The following builders either don\'t run this test (e.g. it\'s skipped) or all runs passed:</div>' +
1824             '<div class=skipped-builder-list><div class=skipped-builder>' + skippedBuilders.join('</div><div class=skipped-builder>') + '</div></div>';
1825     }
1826
1827     return htmlForTestTable(html) + skippedBuildersHtml;
1828 }
1829
1830 function htmlForIndividualTestOnAllBuildersWithResultsLinks(test)
1831 {
1832     processTestRunsForAllBuilders();
1833
1834     var testResults = g_testToResultsMap[test];
1835     var html = '';
1836     html += htmlForIndividualTestOnAllBuilders(test);
1837
1838     html += '<div class=expectations test=' + test + '><div>' +
1839         linkHTMLToToggleState('showExpectations', 'results')
1840
1841     if (g_history.isLayoutTestResults() || g_history.isGPUTestResults()) {
1842         if (g_history.isLayoutTestResults())
1843             html += ' | ' + linkHTMLToToggleState('showLargeExpectations', 'large thumbnails');
1844         if (testResults && currentBuilderGroup().master().name == WEBKIT_BUILDER_MASTER) {
1845             var revision = g_history.dashboardSpecificState.revision || '';
1846             html += '<form onsubmit="g_history.setQueryParameter(\'revision\', revision.value);' +
1847                 'return false;">Show results for WebKit revision: ' +
1848                 '<input name=revision placeholder="e.g. 65540" value="' + revision +
1849                 '" id=revision-input></form>';
1850         } else
1851             html += ' | <b>Only shows actual results/diffs from the most recent *failure* on each bot.</b>';
1852     } else {
1853       html += ' | <span>Results height:<input ' +
1854           'onchange="g_history.setQueryParameter(\'resultsHeight\',this.value)" value="' +
1855           g_history.dashboardSpecificState.resultsHeight + '" style="width:2.5em">px</span>';
1856     }
1857     html += '</div></div>';
1858     return html;
1859 }
1860
1861 function getExpectationsContainer(expectationsContainers, parentContainer, expectationsType)
1862 {
1863     if (!expectationsContainers[expectationsType]) {
1864         var container = document.createElement('div');
1865         container.className = 'expectations-container';
1866         parentContainer.appendChild(container);
1867         expectationsContainers[expectationsType] = container;
1868     }
1869     return expectationsContainers[expectationsType];
1870 }
1871
1872 function ensureTrailingSlash(path)
1873 {
1874     if (path.match(/\/$/))
1875         return path;
1876     return path + '/';
1877 }
1878
1879 function maybeAddPngChecksum(expectationDiv, pngUrl)
1880 {
1881     // pngUrl gets served from the browser cache since we just loaded it in an
1882     // <img> tag.
1883     loader.request(pngUrl,
1884         function(xhr) {
1885             // Convert the first 2k of the response to a byte string.
1886             var bytes = xhr.responseText.substring(0, 2048);
1887             for (var position = 0; position < bytes.length; ++position)
1888                 bytes[position] = bytes[position] & 0xff;
1889
1890             // Look for the comment.
1891             var commentKey = 'tEXtchecksum\x00';
1892             var checksumPosition = bytes.indexOf(commentKey);
1893             if (checksumPosition == -1)
1894                 return;
1895
1896             var checksum = bytes.substring(checksumPosition + commentKey.length, checksumPosition + commentKey.length + 32);
1897             var checksumContainer = document.createElement('span');
1898             checksumContainer.innerText = 'Embedded checksum: ' + checksum;
1899             checksumContainer.setAttribute('class', 'pngchecksum');
1900             expectationDiv.parentNode.appendChild(checksumContainer);
1901         },
1902         function(xhr) {},
1903         true);
1904 }
1905
1906 // Adds a specific expectation. If it's an image, it's only added on the
1907 // image's onload handler. If it's a text file, then a script tag is appended
1908 // as a hack to see if the file 404s (necessary since it's cross-domain).
1909 // Once all the expectations for a specific type have loaded or errored
1910 // (e.g. all the text results), then we go through and identify which platform
1911 // uses which expectation.
1912 //
1913 // @param {Object} expectationsContainers Map from expectations type to
1914 //     container DIV.
1915 // @param {Element} parentContainer Container element for
1916 //     expectationsContainer divs.
1917 // @param {string} platform Platform string. Empty string for non-platform
1918 //     specific expectations.
1919 // @param {string} path Relative path to the expectation.
1920 // @param {string} base Base path for the expectation URL.
1921 // @param {string} opt_builder Builder whose actual results this expectation
1922 //     points to.
1923 // @param {string} opt_suite "virtual suite" that the test belongs to, if any.
1924 function addExpectationItem(expectationsContainers, parentContainer, platform, path, base, opt_builder, opt_suite)
1925 {
1926     var parts = path.split('.')
1927     var fileExtension = parts[parts.length - 1];
1928     if (fileExtension == 'html')
1929         fileExtension = 'txt';
1930     
1931     var container = getExpectationsContainer(expectationsContainers, parentContainer, fileExtension);
1932     var isImage = path.match(/\.png$/);
1933
1934     // FIXME: Stop using script tags once all the places we pull from support CORS.
1935     var platformPart = platform ? ensureTrailingSlash(platform) : '';
1936     var suitePart = opt_suite ? ensureTrailingSlash(opt_suite) : '';
1937
1938     var childContainer = document.createElement('span');
1939     childContainer.className = 'unloaded';
1940
1941     var appendExpectationsItem = function(item) {
1942         childContainer.appendChild(expectationsTitle(platformPart + suitePart, path, opt_builder));
1943         childContainer.className = 'expectations-item';
1944         item.className = 'expectation ' + fileExtension;
1945         if (g_history.dashboardSpecificState.showLargeExpectations)
1946             item.className += ' large';
1947         childContainer.appendChild(item);
1948         handleFinishedLoadingExpectations(container);
1949     };
1950
1951     var url = base + platformPart + path;
1952     if (isImage || !string.startsWith(base, 'http://svn.webkit.org')) {
1953         var dummyNode = document.createElement(isImage ? 'img' : 'script');
1954         dummyNode.src = url;
1955         dummyNode.onload = function() {
1956             var item;
1957             if (isImage) {
1958                 item = dummyNode;
1959                 if (string.startsWith(base, 'http://svn.webkit.org'))
1960                     maybeAddPngChecksum(item, url);
1961             } else {
1962                 item = document.createElement('iframe');
1963                 item.src = url;
1964             }
1965             appendExpectationsItem(item);
1966         }
1967         dummyNode.onerror = function() {
1968             childContainer.parentNode.removeChild(childContainer);
1969             handleFinishedLoadingExpectations(container);
1970         }
1971
1972         // Append script elements now so that they load. Images load without being
1973         // appended to the DOM.
1974         if (!isImage)
1975             childContainer.appendChild(dummyNode);
1976     } else {
1977         loader.request(url,
1978             function(xhr) {
1979                 var item = document.createElement('pre');
1980                 item.innerText = xhr.responseText;
1981                 appendExpectationsItem(item);
1982             },
1983             function(xhr) {/* Do nothing on errors since they're expected */});
1984     }
1985
1986     container.appendChild(childContainer);
1987 }
1988
1989
1990 // Identifies which expectations are used on which platform once all the
1991 // expectations of a given type have loaded (e.g. the container for png
1992 // expectations for this test had no child elements with the class
1993 // "unloaded").
1994 //
1995 // @param {string} container Element containing the expectations for a given
1996 //     test and a given type (e.g. png).
1997 function handleFinishedLoadingExpectations(container)
1998 {
1999     if (container.getElementsByClassName('unloaded').length)
2000         return;
2001
2002     var titles = container.getElementsByClassName('expectations-title');
2003     for (var platform in g_fallbacksMap) {
2004         var fallbacks = g_fallbacksMap[platform];
2005         var winner = null;
2006         var winningIndex = -1;
2007         for (var i = 0; i < titles.length; i++) {
2008             var title = titles[i];
2009
2010             if (!winner && title.platform == "") {
2011                 winner = title;
2012                 continue;
2013             }
2014
2015             var rawPlatform = title.platform && title.platform.replace('platform/', '');
2016             for (var j = 0; j < fallbacks.length; j++) {
2017                 if ((winningIndex == -1 || winningIndex > j) && rawPlatform == fallbacks[j]) {
2018                     winningIndex = j;
2019                     winner = title;
2020                     break;
2021                 }
2022             }
2023         }
2024         if (winner)
2025             winner.getElementsByClassName('platforms')[0].innerHTML += '<div class=used-platform>' + platform + '</div>';
2026         else {
2027             console.log('No expectations identified for this test. This means ' +
2028                 'there is a logic bug in the dashboard for which expectations a ' +
2029                 'platform uses or trac.webkit.org/src.chromium.org is giving 5XXs.');
2030         }
2031     }
2032
2033     consolidateUsedPlatforms(container);
2034 }
2035
2036 // Consolidate platforms when all sub-platforms for a given platform are represented.
2037 // e.g., if all of the WIN- platforms are there, replace them with just WIN.
2038 function consolidateUsedPlatforms(container)
2039 {
2040     var allPlatforms = Object.keys(g_fallbacksMap);
2041
2042     var platformElements = container.getElementsByClassName('platforms');
2043     for (var i = 0, platformsLength = platformElements.length; i < platformsLength; i++) {
2044         var usedPlatforms = platformElements[i].getElementsByClassName('used-platform');
2045         if (!usedPlatforms.length)
2046             continue;
2047
2048         var platforms = {};
2049         platforms['MAC'] = {};
2050         platforms['WIN'] = {};
2051         platforms['LINUX'] = {};
2052         allPlatforms.forEach(function(platform) {
2053             if (string.startsWith(platform, 'MAC'))
2054                 platforms['MAC'][platform] = 1;
2055             else if (string.startsWith(platform, 'WIN'))
2056                 platforms['WIN'][platform] = 1;
2057             else if (string.startsWith(platform, 'LINUX'))
2058                 platforms['LINUX'][platform] = 1;
2059         });
2060
2061         for (var j = 0, usedPlatformsLength = usedPlatforms.length; j < usedPlatformsLength; j++) {
2062             for (var platform in platforms)
2063                 delete platforms[platform][usedPlatforms[j].textContent];
2064         }
2065
2066         for (var platform in platforms) {
2067             if (!Object.keys(platforms[platform]).length) {
2068                 var nodesToRemove = [];
2069                 for (var j = 0, usedPlatformsLength = usedPlatforms.length; j < usedPlatformsLength; j++) {
2070                     var usedPlatform = usedPlatforms[j];
2071                     if (string.startsWith(usedPlatform.textContent, platform))
2072                         nodesToRemove.push(usedPlatform);
2073                 }
2074
2075                 nodesToRemove.forEach(function(element) { element.parentNode.removeChild(element); });
2076                 platformElements[i].insertAdjacentHTML('afterBegin', '<div class=used-platform>' + platform + '</div>');
2077             }
2078         }
2079     }
2080 }
2081
2082 function addExpectations(expectationsContainers, container, base,
2083     platform, text, png, reftest_html_file, reftest_mismatch_html_file, suite)
2084 {
2085     var builder = '';
2086     addExpectationItem(expectationsContainers, container, platform, text, base, builder, suite);
2087     addExpectationItem(expectationsContainers, container, platform, png, base, builder, suite);
2088     addExpectationItem(expectationsContainers, container, platform, reftest_html_file, base, builder, suite);
2089     addExpectationItem(expectationsContainers, container, platform, reftest_mismatch_html_file, base, builder, suite);
2090 }
2091
2092 function expectationsTitle(platform, path, builder)
2093 {
2094     var header = document.createElement('h3');
2095     header.className = 'expectations-title';
2096
2097     var innerHTML;
2098     if (builder) {
2099         var resultsType;
2100         if (string.endsWith(path, '-crash-log.txt'))
2101             resultsType = 'STACKTRACE';
2102         else if (string.endsWith(path, '-actual.txt') || string.endsWith(path, '-actual.png'))
2103             resultsType = 'ACTUAL RESULTS';
2104         else if (string.endsWith(path, '-wdiff.html'))
2105             resultsType = 'WDIFF';
2106         else
2107             resultsType = 'DIFF';
2108
2109         innerHTML = resultsType + ': ' + builder;
2110     } else if (platform === "") {
2111         var parts = path.split('/');
2112         innerHTML = parts[parts.length - 1];
2113     } else
2114         innerHTML = platform || path;
2115
2116     header.innerHTML = '<div class=title>' + innerHTML +
2117         '</div><div style="float:left">&nbsp;</div>' +
2118         '<div class=platforms style="float:right"></div>';
2119     header.platform = platform;
2120     return header;
2121 }
2122
2123 function loadExpectations(expectationsContainer)
2124 {
2125     var test = expectationsContainer.getAttribute('test');
2126     if (g_history.isLayoutTestResults())
2127         loadExpectationsLayoutTests(test, expectationsContainer);
2128     else {
2129         var results = g_testToResultsMap[test];
2130         for (var i = 0; i < results.length; i++)
2131             if (g_history.isGPUTestResults())
2132                 loadGPUResultsForBuilder(results[i].builder, test, expectationsContainer);
2133             else
2134                 loadNonWebKitResultsForBuilder(results[i].builder, test, expectationsContainer);
2135     }
2136 }
2137
2138 function gpuResultsPath(chromeRevision, builder)
2139 {
2140   return chromeRevision + '_' + builder.replace(/[^A-Za-z0-9]+/g, '_');
2141 }
2142
2143 function loadGPUResultsForBuilder(builder, test, expectationsContainer)
2144 {
2145     var container = document.createElement('div');
2146     container.className = 'expectations-container';
2147     container.innerHTML = '<div><b>' + builder + '</b></div>';
2148     expectationsContainer.appendChild(container);
2149
2150     var failureIndex = indexesForFailures(builder, test)[0];
2151
2152     var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndex];
2153     var pathToLog = builderMaster(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
2154
2155     var chromeRevision = g_resultsByBuilder[builder].chromeRevision[failureIndex];
2156     var resultsUrl = GPU_RESULTS_BASE_PATH + gpuResultsPath(chromeRevision, builder);
2157     var filename = test.split(/\./)[1] + '.png';
2158
2159     appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
2160     appendNonWebKitResults(container, resultsUrl + '/gen/' + filename, 'gpu-test-results', 'Generated');
2161     appendNonWebKitResults(container, resultsUrl + '/ref/' + filename, 'gpu-test-results', 'Reference');
2162     appendNonWebKitResults(container, resultsUrl + '/diff/' + filename, 'gpu-test-results', 'Diff');
2163 }
2164
2165 function loadNonWebKitResultsForBuilder(builder, test, expectationsContainer)
2166 {
2167     var failureIndexes = indexesForFailures(builder, test);
2168     var container = document.createElement('div');
2169     container.innerHTML = '<div><b>' + builder + '</b></div>';
2170     expectationsContainer.appendChild(container);
2171     for (var i = 0; i < failureIndexes.length; i++) {
2172         // FIXME: This doesn't seem to work anymore. Did the paths change?
2173         // Once that's resolved, see if we need to try each GTEST_MODIFIERS prefix as well.
2174         var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndexes[i]];
2175         var pathToLog = builderMaster(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
2176         appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
2177     }
2178 }
2179
2180 function appendNonWebKitResults(container, url, itemClassName, opt_title)
2181 {
2182     // Use a script tag to detect whether the URL 404s.
2183     // Need to use a script tag since the URL is cross-domain.
2184     var dummyNode = document.createElement('script');
2185     dummyNode.src = url;
2186
2187     dummyNode.onload = function() {
2188         var item = document.createElement('iframe');
2189         item.src = dummyNode.src;
2190         item.className = itemClassName;
2191         item.style.height = g_history.dashboardSpecificState.resultsHeight + 'px';
2192
2193         if (opt_title) {
2194             var childContainer = document.createElement('div');
2195             childContainer.style.display = 'inline-block';
2196             var title = document.createElement('div');
2197             title.textContent = opt_title;
2198             childContainer.appendChild(title);
2199             childContainer.appendChild(item);
2200             container.replaceChild(childContainer, dummyNode);
2201         } else
2202             container.replaceChild(item, dummyNode);
2203     }
2204     dummyNode.onerror = function() {
2205         container.removeChild(dummyNode);
2206     }
2207
2208     container.appendChild(dummyNode);
2209 }
2210
2211 function buildInfoForRevision(builder, revision)
2212 {
2213     var revisions = g_resultsByBuilder[builder].webkitRevision;
2214     var revisionStart = 0, revisionEnd = 0, buildNumber = 0;
2215     for (var i = 0; i < revisions.length; i++) {
2216         if (revision > revisions[i]) {
2217             revisionStart = revisions[i - 1];
2218             revisionEnd = revisions[i];
2219             buildNumber = g_resultsByBuilder[builder].buildNumbers[i - 1];
2220             break;
2221         }
2222     }
2223
2224     if (revisionEnd)
2225       revisionEnd++;
2226     else
2227       revisionEnd = '';
2228
2229     return {revisionStart: revisionStart, revisionEnd: revisionEnd, buildNumber: buildNumber};
2230 }
2231
2232 function lookupVirtualTestSuite(test) {
2233     for (var suite in VIRTUAL_SUITES) {
2234         if (test.indexOf(suite) != -1)
2235             return suite;
2236     }
2237     return '';
2238 }
2239
2240 function baseTest(test, suite) {
2241     base = VIRTUAL_SUITES[suite];
2242     return base ? test.replace(suite, base) : test;
2243 }
2244
2245 function loadBaselinesForTest(expectationsContainers, expectationsContainer, test) {
2246     var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
2247     var text = testWithoutSuffix + "-expected.txt";
2248     var png = testWithoutSuffix + "-expected.png";
2249     var reftest_html_file = testWithoutSuffix + "-expected.html";
2250     var reftest_mismatch_html_file = testWithoutSuffix + "-expected-mismatch.html";
2251     var suite = lookupVirtualTestSuite(test);
2252
2253     if (!suite)
2254         addExpectationItem(expectationsContainers, expectationsContainer, null, test, TEST_URL_BASE_PATH);
2255
2256     addExpectations(expectationsContainers, expectationsContainer,
2257         TEST_URL_BASE_PATH, '', text, png, reftest_html_file, reftest_mismatch_html_file, suite);
2258
2259     var fallbacks = allFallbacks();
2260     for (var i = 0; i < fallbacks.length; i++) {
2261       var fallback = 'platform/' + fallbacks[i];
2262       addExpectations(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH, fallback, text, png,
2263           reftest_html_file, reftest_mismatch_html_file, suite);
2264     }
2265
2266     if (suite)
2267         loadBaselinesForTest(expectationsContainers, expectationsContainer, baseTest(test, suite));
2268 }
2269
2270 function loadExpectationsLayoutTests(test, expectationsContainer)
2271 {
2272     // Map from file extension to container div for expectations of that type.
2273     var expectationsContainers = {};
2274
2275     var revisionContainer = document.createElement('div');
2276     revisionContainer.textContent = "Showing results for: "
2277     expectationsContainer.appendChild(revisionContainer);
2278     for (var builder in currentBuilders()) {
2279         if (builderMaster(builder).name == WEBKIT_BUILDER_MASTER) {
2280             var latestRevision = g_history.dashboardSpecificState.revision || g_resultsByBuilder[builder].webkitRevision[0];
2281             var buildInfo = buildInfoForRevision(builder, latestRevision);
2282             var revisionInfo = document.createElement('div');
2283             revisionInfo.style.cssText = 'background:lightgray;margin:0 3px;padding:0 2px;display:inline-block;';
2284             revisionInfo.innerHTML = builder + ' r' + buildInfo.revisionEnd +
2285                 ':r' + buildInfo.revisionStart + ', build ' + buildInfo.buildNumber;
2286             revisionContainer.appendChild(revisionInfo);
2287         }
2288     }
2289
2290     loadBaselinesForTest(expectationsContainers, expectationsContainer, test);
2291         
2292     var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
2293     var actualResultSuffixes = ['-actual.txt', '-actual.png', '-crash-log.txt', '-diff.txt', '-wdiff.html', '-diff.png'];
2294
2295     for (var builder in currentBuilders()) {
2296         var actualResultsBase;
2297         if (builderMaster(builder).name == WEBKIT_BUILDER_MASTER) {
2298             var latestRevision = g_history.dashboardSpecificState.revision || g_resultsByBuilder[builder].webkitRevision[0];
2299             var buildInfo = buildInfoForRevision(builder, latestRevision);
2300             actualResultsBase = 'http://build.webkit.org/results/' + builder +
2301                 '/r' + buildInfo.revisionStart + ' (' + buildInfo.buildNumber + ')/';
2302         } else
2303             actualResultsBase = TEST_RESULTS_BASE_PATH + currentBuilders()[builder] + '/results/layout-test-results/';
2304
2305         for (var i = 0; i < actualResultSuffixes.length; i++) {
2306             addExpectationItem(expectationsContainers, expectationsContainer, null,
2307                 testWithoutSuffix + actualResultSuffixes[i], actualResultsBase, builder);
2308         }
2309     }
2310
2311     // Add a clearing element so floated elements don't bleed out of their
2312     // containing block.
2313     var br = document.createElement('br');
2314     br.style.clear = 'both';
2315     expectationsContainer.appendChild(br);
2316 }
2317
2318 var g_allFallbacks;
2319
2320 // Returns the reverse sorted, deduped list of all platform fallback
2321 // directories.
2322 function allFallbacks()
2323 {
2324     if (!g_allFallbacks) {
2325         var holder = {};
2326         for (var platform in g_fallbacksMap) {
2327             var fallbacks = g_fallbacksMap[platform];
2328             for (var i = 0; i < fallbacks.length; i++)
2329                 holder[fallbacks[i]] = 1;
2330         }
2331
2332         g_allFallbacks = [];
2333         for (var fallback in holder)
2334             g_allFallbacks.push(fallback);
2335
2336         g_allFallbacks.sort(function(a, b) {
2337             if (a == b)
2338                 return 0;
2339             return a < b;
2340         });
2341     }
2342     return g_allFallbacks;
2343 }
2344
2345 function appendExpectations()
2346 {
2347     var expectations = g_history.dashboardSpecificState.showExpectations ? document.getElementsByClassName('expectations') : [];
2348     // Loading expectations is *very* slow. Use a large timeout to avoid
2349     // totally hanging the renderer.
2350     performChunkedAction(expectations, function(chunk) {
2351         for (var i = 0, len = chunk.length; i < len; i++)
2352             loadExpectations(chunk[i]);
2353         postHeightChangedMessage();
2354
2355     }, hideLoadingUI, 10000);
2356 }
2357
2358 function hideLoadingUI()
2359 {
2360     var loadingDiv = $('loading-ui');
2361     if (loadingDiv)
2362         loadingDiv.style.display = 'none';
2363     postHeightChangedMessage();
2364 }
2365
2366 function generatePageForIndividualTests(tests)
2367 {
2368     console.log('Number of tests: ' + tests.length);
2369     if (g_history.dashboardSpecificState.showChrome)
2370         appendHTML(htmlForNavBar());
2371     performChunkedAction(tests, function(chunk) {
2372         appendHTML(htmlForIndividualTests(chunk));
2373     }, appendExpectations, 500);
2374     if (g_history.dashboardSpecificState.showChrome)
2375         $('tests-input').value = g_history.dashboardSpecificState.tests;
2376 }
2377
2378 function performChunkedAction(tests, handleChunk, onComplete, timeout, opt_index) {
2379     var index = opt_index || 0;
2380     setTimeout(function() {
2381         var chunk = Array.prototype.slice.call(tests, index * CHUNK_SIZE, (index + 1) * CHUNK_SIZE);
2382         if (chunk.length) {
2383             handleChunk(chunk);
2384             performChunkedAction(tests, handleChunk, onComplete, timeout, ++index);
2385         } else
2386             onComplete();
2387     // No need for a timeout on the first chunked action.
2388     }, index ? timeout : 0);
2389 }
2390
2391 function htmlForIndividualTests(tests)
2392 {
2393     var testsHTML = [];
2394     for (var i = 0; i < tests.length; i++) {
2395         var test = tests[i];
2396         var testNameHtml = '';
2397         if (g_history.dashboardSpecificState.showChrome || tests.length > 1) {
2398             if (g_history.isLayoutTestResults()) {
2399                 var suite = lookupVirtualTestSuite(test);
2400                 var base = suite ? baseTest(test, suite) : test;
2401                 var tracURL = TEST_URL_BASE_PATH_TRAC + base;
2402                 testNameHtml += '<h2>' + linkHTMLToOpenWindow(tracURL, test) + '</h2>';
2403             } else
2404                 testNameHtml += '<h2>' + test + '</h2>';
2405         }
2406
2407         testsHTML.push(testNameHtml + htmlForIndividualTestOnAllBuildersWithResultsLinks(test));
2408     }
2409     return testsHTML.join('<hr>');
2410 }
2411
2412 function htmlForNavBar()
2413 {
2414     var extraHTML = '';
2415     var html = ui.html.testTypeSwitcher(false, extraHTML, isCrossBuilderView());
2416     html += '<div class=forms><form id=result-form ' +
2417         'onsubmit="g_history.setQueryParameter(\'result\', result.value);' +
2418         'return false;">Show all tests with result: ' +
2419         '<input name=result placeholder="e.g. CRASH" id=result-input>' +
2420         '</form><form id=tests-form ' +
2421         'onsubmit="g_history.setQueryParameter(\'tests\', tests.value);' +
2422         'return false;"><span>Show tests on all platforms: </span>' +
2423         '<input name=tests ' +
2424         'placeholder="Comma or space-separated list of tests or partial ' +
2425         'paths to show test results across all builders, e.g., ' +
2426         'foo/bar.html,foo/baz,domstorage" id=tests-input></form>' +
2427         '<span class=link onclick="showLegend()">Show legend [type ?]</span></div>';
2428     return html;
2429 }
2430
2431 function checkBoxToToggleState(key, text)
2432 {
2433     var stateEnabled = g_history.dashboardSpecificState[key];
2434     return '<label><input type=checkbox ' + (stateEnabled ? 'checked ' : '') + 'onclick="g_history.setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + text + '</label> ';
2435 }
2436
2437 function linkHTMLToToggleState(key, linkText)
2438 {
2439     var stateEnabled = g_history.dashboardSpecificState[key];
2440     return '<span class=link onclick="g_history.setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + (stateEnabled ? 'Hide' : 'Show') + ' ' + linkText + '</span>';
2441 }
2442
2443 function headerForTestTableHtml()
2444 {
2445     return '<h2 style="display:inline-block">Failing tests</h2>' +
2446         checkBoxToToggleState('showWontFixSkip', 'WONTFIX/SKIP') +
2447         checkBoxToToggleState('showCorrectExpectations', 'tests with correct expectations') +
2448         checkBoxToToggleState('showWrongExpectations', 'tests with wrong expectations') +
2449         checkBoxToToggleState('showFlaky', 'flaky') +
2450         checkBoxToToggleState('showSlow', 'slow');
2451 }
2452
2453 function generatePageForBuilder(builderName)
2454 {
2455     processTestRunsForBuilder(builderName);
2456
2457     var results = g_perBuilderFailures[builderName];
2458     sortTests(results, g_history.dashboardSpecificState.sortColumn, g_history.dashboardSpecificState.sortOrder);
2459
2460     var testsHTML = '';
2461     if (results.length) {
2462         var tableRowsHTML = '';
2463         for (var i = 0; i < results.length; i++)
2464             tableRowsHTML += htmlForSingleTestRow(results[i])
2465         testsHTML = htmlForTestTable(tableRowsHTML);
2466     } else {
2467         testsHTML = '<div>No tests found. ';
2468         if (g_history.isLayoutTestResults())
2469             testsHTML += 'Try showing tests with correct expectations.</div>';
2470         else
2471             testsHTML += 'This means no tests have failed!</div>';
2472     }
2473
2474     var html = htmlForNavBar();
2475
2476     if (g_history.isLayoutTestResults())
2477         html += htmlForTestsWithExpectationsButNoFailures(builderName) + headerForTestTableHtml();
2478
2479     html += '<br>' + testsHTML;
2480     appendHTML(html);
2481
2482     var ths = document.getElementsByTagName('th');
2483     for (var i = 0; i < ths.length; i++) {
2484         ths[i].addEventListener('click', changeSort, false);
2485         ths[i].className = "sortable";
2486     }
2487
2488     hideLoadingUI();
2489 }
2490
2491 var VALID_KEYS_FOR_CROSS_BUILDER_VIEW = {
2492     tests: 1,
2493     result: 1,
2494     showChrome: 1,
2495     showExpectations: 1,
2496     showLargeExpectations: 1,
2497     legacyExpectationsSemantics: 1,
2498     resultsHeight: 1,
2499     revision: 1
2500 };
2501
2502 function isInvalidKeyForCrossBuilderView(key)
2503 {
2504     return !(key in VALID_KEYS_FOR_CROSS_BUILDER_VIEW) && !(key in history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES);
2505 }
2506
2507 function hideLegend()
2508 {
2509     var legend = $('legend');
2510     if (legend)
2511         legend.parentNode.removeChild(legend);
2512 }
2513
2514 var g_fallbacksMap = {};
2515 g_fallbacksMap['WIN-XP'] = ['chromium-win-xp', 'chromium-win', 'chromium'];
2516 g_fallbacksMap['WIN-7'] = ['chromium-win', 'chromium'];
2517 g_fallbacksMap['MAC-SNOWLEOPARD'] = ['chromium-mac-snowleopard', 'chromium-mac', 'chromium'];
2518 g_fallbacksMap['MAC-LION'] = ['chromium-mac', 'chromium'];
2519 g_fallbacksMap['LINUX-32'] = ['chromium-linux-x86', 'chromium-linux', 'chromium-win', 'chromium'];
2520 g_fallbacksMap['LINUX-64'] = ['chromium-linux', 'chromium-win', 'chromium'];
2521
2522 function htmlForFallbackHelp(fallbacks)
2523 {
2524     return '<ol class=fallback-list><li>' + fallbacks.join('</li><li>') + '</li></ol>';
2525 }
2526
2527 function showLegend()
2528 {
2529     var legend = $('legend');
2530     if (!legend) {
2531         legend = document.createElement('div');
2532         legend.id = 'legend';
2533         document.body.appendChild(legend);
2534     }
2535
2536     var html = '<div id=legend-toggle onclick="hideLegend()">Hide ' +
2537         'legend [type esc]</div><div id=legend-contents>';
2538     for (var expectation in expectationsMap())
2539         html += '<div class=' + expectation + '>' + expectationsMap()[expectation] + '</div>';
2540
2541     html += '<div class=merge>WEBKIT MERGE</div>';
2542     if (g_history.isLayoutTestResults()) {
2543       html += '</div><br style="clear:both">' +
2544           '</div><h3>Test expectatons fallback order.</h3>';
2545
2546       for (var platform in g_fallbacksMap)
2547           html += '<div class=fallback-header>' + platform + '</div>' + htmlForFallbackHelp(g_fallbacksMap[platform]);
2548
2549       html += '<div>TIMES:</div>' +
2550           htmlForSlowTimes(MIN_SECONDS_FOR_SLOW_TEST) +
2551           '<div>DEBUG TIMES:</div>' +
2552           htmlForSlowTimes(MIN_SECONDS_FOR_SLOW_TEST_DEBUG);
2553     }
2554
2555     legend.innerHTML = html;
2556 }
2557
2558 function htmlForSlowTimes(minTime)
2559 {
2560     return '<ul><li>&lt;1 second == !SLOW</li><li>&gt;1 second && &lt;' +
2561         minTime + ' seconds == SLOW || !SLOW is fine</li><li>&gt;' +
2562         minTime + ' seconds == SLOW</li></ul>';
2563 }
2564
2565 function postHeightChangedMessage()
2566 {
2567     if (window == parent)
2568         return;
2569
2570     var root = document.documentElement;
2571     var height = root.offsetHeight;
2572     if (root.offsetWidth < root.scrollWidth) {
2573         // We have a horizontal scrollbar. Include it in the height.
2574         var dummyNode = document.createElement('div');
2575         dummyNode.style.overflow = 'scroll';
2576         document.body.appendChild(dummyNode);
2577         var scrollbarWidth = dummyNode.offsetHeight - dummyNode.clientHeight;
2578         document.body.removeChild(dummyNode);
2579         height += scrollbarWidth;
2580     }
2581     parent.postMessage({command: 'heightChanged', height: height}, '*')
2582 }
2583
2584 if (window != parent)
2585     window.addEventListener('blur', ui.popup.hide);
2586
2587 document.addEventListener('keydown', function(e) {
2588     if (e.keyIdentifier == 'U+003F' || e.keyIdentifier == 'U+00BF') {
2589         // WebKit MAC retursn 3F. WebKit WIN returns BF. This is a bug!
2590         // ? key
2591         showLegend();
2592     } else if (e.keyIdentifier == 'U+001B') {
2593         // escape key
2594         hideLegend();
2595         ui.popup.hide();
2596     }
2597 }, false);
2598
2599 window.addEventListener('load', function() {
2600     resourceLoader = new loader.Loader();
2601     resourceLoader.load();
2602 }, false);