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