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