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