0eb61241a7b979d7ca164fb066b12ccccf7b41f6
[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     var start = Date.now();
946
947     var buildInfo = platformAndBuildType(builderName);
948     var platform = buildInfo.platform;
949     var buildType = buildInfo.buildType;
950     processMissingTestsWithExpectations(builderName, platform, buildType);
951
952     var failures = [];
953     var allTestsForThisBuilder = g_resultsByBuilder[builderName].tests;
954
955     for (var test in allTestsForThisBuilder) {
956         var resultsForTest = createResultsObjectForTest(test, builderName);
957         populateExpectationsData(resultsForTest);
958
959         var rawTest = g_resultsByBuilder[builderName].tests[test];
960         resultsForTest.rawTimes = rawTest.times;
961         var rawResults = rawTest.results;
962         resultsForTest.rawResults = rawResults;
963
964         // FIXME: Switch to resultsByBuild
965         var times = resultsForTest.rawTimes;
966         var numTimesSeen = 0;
967         var numResultsSeen = 0;
968         var resultsIndex = 0;
969         var currentResult;
970         for (var i = 0; i < times.length; i++) {
971             numTimesSeen += times[i][RLE.LENGTH];
972
973             while (rawResults[resultsIndex] && numTimesSeen > (numResultsSeen + rawResults[resultsIndex][RLE.LENGTH])) {
974                 numResultsSeen += rawResults[resultsIndex][RLE.LENGTH];
975                 resultsIndex++;
976             }
977
978             if (rawResults && rawResults[resultsIndex])
979                 currentResult = rawResults[resultsIndex][RLE.VALUE];
980
981             var time = times[i][RLE.VALUE]
982
983             // Ignore times for crashing/timeout runs for the sake of seeing if
984             // a test should be marked slow.
985             if (currentResult != 'C' && currentResult != 'T')
986                 resultsForTest.slowestNonTimeoutCrashTime = Math.max(resultsForTest.slowestNonTimeoutCrashTime, time);
987             resultsForTest.slowestTime = Math.max(resultsForTest.slowestTime, time);
988         }
989
990         processMissingAndExtraExpectations(resultsForTest);
991         failures.push(resultsForTest);
992
993         if (!g_testToResultsMap[test])
994             g_testToResultsMap[test] = [];
995         g_testToResultsMap[test].push(resultsForTest);
996     }
997
998     g_perBuilderFailures[builderName] = failures;
999     logTime('processTestRunsForBuilder: ' + builderName, start);
1000 }
1001
1002 function processMissingAndExtraExpectations(resultsForTest)
1003 {
1004     // Heuristic for determining whether expectations apply to a given test:
1005     // -If a test result happens < MIN_RUNS_FOR_FLAKE, then consider it a flaky
1006     // result and include it in the list of expected results.
1007     // -Otherwise, grab the first contiguous set of runs with the same result
1008     // for >= MIN_RUNS_FOR_FLAKE and ignore all following runs >=
1009     // MIN_RUNS_FOR_FLAKE.
1010     // This lets us rule out common cases of a test changing expectations for
1011     // a few runs, then being fixed or otherwise modified in a non-flaky way.
1012     var rawResults = resultsForTest.rawResults;
1013
1014     // If the first result is no-data that means the test is skipped or is
1015     // being run on a different builder (e.g. moved from one shard to another).
1016     // Ignore these results since we have no real data about what's going on.
1017     if (rawResults[0][RLE.VALUE] == 'N')
1018         return;
1019
1020     // Only consider flake if it doesn't happen twice in a row.
1021     var MIN_RUNS_FOR_FLAKE = 2;
1022     var resultsMap = {}
1023     var numResultsSeen = 0;
1024     var haveSeenNonFlakeResult = false;
1025     var numRealResults = 0;
1026
1027     var seenResults = {};
1028     for (var i = 0; i < rawResults.length; i++) {
1029         var numResults = rawResults[i][RLE.LENGTH];
1030         numResultsSeen += numResults;
1031
1032         var result = rawResults[i][RLE.VALUE];
1033
1034         var hasMinRuns = numResults >= MIN_RUNS_FOR_FLAKE;
1035         if (haveSeenNonFlakeResult && hasMinRuns)
1036             continue;
1037         else if (hasMinRuns)
1038             haveSeenNonFlakeResult = true;
1039         else if (!seenResults[result]) {
1040             // Only consider a short-lived result if we've seen it more than once.
1041             // Otherwise, we include lots of false-positives due to tests that fail
1042             // for a couple runs and then start passing.
1043             seenResults[result] = true;
1044             continue;
1045         }
1046
1047         var expectation = expectationsFileStringForResult(result);
1048         resultsMap[expectation] = true;
1049         numRealResults++;
1050     }
1051
1052     resultsForTest.flips = i - 1;
1053     resultsForTest.isFlaky = numRealResults > 1;
1054
1055     var missingExpectations = [];
1056     var extraExpectations = [];
1057
1058     if (isLayoutTestResults()) {
1059         var expectationsArray = resultsForTest.expectations ? resultsForTest.expectations.split(' ') : [];
1060         extraExpectations = expectationsArray.filter(
1061             function(element) {
1062                 // FIXME: Once all the FAIL lines are removed from
1063                 // TestExpectations, delete all the legacyExpectationsSemantics
1064                 // code.
1065                 if (g_currentState.legacyExpectationsSemantics) {
1066                     if (element == 'FAIL') {
1067                         for (var i = 0; i < FAIL_RESULTS.length; i++) {
1068                             if (resultsMap[FAIL_RESULTS[i]])
1069                                 return false;
1070                         }
1071                         return true;
1072                     }
1073                 }
1074
1075                 return element && !resultsMap[element] && !string.contains(element, 'BUG');
1076             });
1077
1078         for (var result in resultsMap) {
1079             resultsForTest.actualResults.push(result);
1080             var hasExpectation = false;
1081             for (var i = 0; i < expectationsArray.length; i++) {
1082                 var expectation = expectationsArray[i];
1083                 // FIXME: Once all the FAIL lines are removed from
1084                 // TestExpectations, delete all the legacyExpectationsSemantics
1085                 // code.
1086                 if (g_currentState.legacyExpectationsSemantics) {
1087                     if (expectation == 'FAIL') {
1088                         for (var j = 0; j < FAIL_RESULTS.length; j++) {
1089                             if (result == FAIL_RESULTS[j]) {
1090                                 hasExpectation = true;
1091                                 break;
1092                             }
1093                         }
1094                     }
1095                 }
1096
1097                 if (result == expectation)
1098                     hasExpectation = true;
1099
1100                 if (hasExpectation)
1101                     break;
1102             }
1103             // If we have no expectations for a test and it only passes, then don't
1104             // list PASS as a missing expectation. We only want to list PASS if it
1105             // flaky passes, so there would be other expectations.
1106             if (!hasExpectation && !(!expectationsArray.length && result == 'PASS' && numRealResults == 1))
1107                 missingExpectations.push(result);
1108         }
1109
1110         // Only highlight tests that take > 2 seconds as needing to be marked as
1111         // slow. There are too many tests that take ~2 seconds every couple
1112         // hundred runs. It's not worth the manual maintenance effort.
1113         // Also, if a test times out, then it should not be marked as slow.
1114         var minTimeForNeedsSlow = isDebug(resultsForTest.builder) ? 2 : 1;
1115         if (isSlowTest(resultsForTest) && !resultsMap['TIMEOUT'] && (!resultsForTest.modifiers || !string.contains(resultsForTest.modifiers, 'SLOW')))
1116             missingExpectations.push('SLOW');
1117         else if (isFastTest(resultsForTest) && resultsForTest.modifiers && string.contains(resultsForTest.modifiers, 'SLOW'))
1118             extraExpectations.push('SLOW');
1119
1120         // If there are no missing results or modifiers besides build
1121         // type, platform, or bug and the expectations are all extra
1122         // that is, extraExpectations - expectations = PASS,
1123         // include PASS as extra, since that means this line in
1124         // test_expectations can be deleted..
1125         if (!missingExpectations.length && !(resultsForTest.modifiers && realModifiers(resultsForTest.modifiers))) {
1126             var extraPlusPass = extraExpectations.concat(['PASS']);
1127             if (extraPlusPass.sort().toString() == expectationsArray.slice(0).sort().toString())
1128                 extraExpectations.push('PASS');
1129         }
1130
1131     }
1132
1133     resultsForTest.meetsExpectations = !missingExpectations.length && !extraExpectations.length;
1134     resultsForTest.missing = missingExpectations.sort().join(' ');
1135     resultsForTest.extra = extraExpectations.sort().join(' ');
1136 }
1137
1138
1139 var BUG_URL_PREFIX = '<a href="http://';
1140 var BUG_URL_POSTFIX = '/$1">crbug.com/$1</a> ';
1141 var WEBKIT_BUG_URL_POSTFIX = '/$1">webkit.org/b/$1</a> ';
1142 var INTERNAL_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'b' + BUG_URL_POSTFIX;
1143 var EXTERNAL_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'crbug.com' + BUG_URL_POSTFIX;
1144 var WEBKIT_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'webkit.org/b' + WEBKIT_BUG_URL_POSTFIX;
1145
1146 function htmlForBugs(bugs)
1147 {
1148     bugs = bugs.replace(/crbug.com\/(\d+)(\ |$)/g, EXTERNAL_BUG_REPLACE_VALUE);
1149     bugs = bugs.replace(/webkit.org\/b\/(\d+)(\ |$)/g, WEBKIT_BUG_REPLACE_VALUE);
1150     return bugs;
1151 }
1152
1153 function linkHTMLToOpenWindow(url, text)
1154 {
1155     return '<a href="' + url + '" target="_blank">' + text + '</a>';
1156 }
1157
1158 // FIXME: replaced with chromiumRevisionLink/webKitRevisionLink
1159 function createBlameListHTML(revisions, index, urlBase, separator, repo)
1160 {
1161     var thisRevision = revisions[index];
1162     if (!thisRevision)
1163         return '';
1164
1165     var previousRevision = revisions[index + 1];
1166     if (previousRevision && previousRevision != thisRevision) {
1167         previousRevision++;
1168         return linkHTMLToOpenWindow(urlBase + thisRevision + separator + previousRevision,
1169             repo + ' blamelist r' + previousRevision + ':r' + thisRevision);
1170     } else
1171         return 'At ' + repo + ' revision: ' + thisRevision;
1172 }
1173
1174 // Returns whether the result for index'th result for testName on builder was
1175 // a failure.
1176 function isFailure(builder, testName, index)
1177 {
1178     var currentIndex = 0;
1179     var rawResults = g_resultsByBuilder[builder].tests[testName].results;
1180     for (var i = 0; i < rawResults.length; i++) {
1181         currentIndex += rawResults[i][RLE.LENGTH];
1182         if (currentIndex > index)
1183             return isFailingResult(rawResults[i][RLE.VALUE]);
1184     }
1185     console.error('Index exceeds number of results: ' + index);
1186 }
1187
1188 // Returns an array of indexes for all builds where this test failed.
1189 function indexesForFailures(builder, testName)
1190 {
1191     var rawResults = g_resultsByBuilder[builder].tests[testName].results;
1192     var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
1193     var index = 0;
1194     var failures = [];
1195     for (var i = 0; i < rawResults.length; i++) {
1196         var numResults = rawResults[i][RLE.LENGTH];
1197         if (isFailingResult(rawResults[i][RLE.VALUE])) {
1198             for (var j = 0; j < numResults; j++)
1199                 failures.push(index + j);
1200         }
1201         index += numResults;
1202     }
1203     return failures;
1204 }
1205
1206 // Returns the path to the failure log for this non-webkit test.
1207 function pathToFailureLog(testName)
1208 {
1209     return '/steps/' + g_crossDashboardState.testType + '/logs/' + testName.split('.')[1]
1210 }
1211
1212 function showPopupForBuild(e, builder, index, opt_testName)
1213 {
1214     var html = '';
1215
1216     var time = g_resultsByBuilder[builder].secondsSinceEpoch[index];
1217     if (time) {
1218         var date = new Date(time * 1000);
1219         html += date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
1220     }
1221
1222     var buildNumber = g_resultsByBuilder[builder].buildNumbers[index];
1223     var master = builderMaster(builder);
1224     var buildBasePath = master.logPath(builder, buildNumber);
1225
1226     html += '<ul><li>' + linkHTMLToOpenWindow(buildBasePath, 'Build log') +
1227         '</li><li>' +
1228         createBlameListHTML(g_resultsByBuilder[builder].webkitRevision, index,
1229             'http://trac.webkit.org/log/?verbose=on&rev=', '&stop_rev=',
1230             'WebKit') +
1231         '</li>';
1232
1233     if (master.name == WEBKIT_BUILDER_MASTER) {
1234         var revision = g_resultsByBuilder[builder].webkitRevision[index];
1235         html += '<li><span class=link onclick="setQueryParameter(\'revision\',' +
1236             revision + ')">Show results for WebKit r' + revision +
1237             '</span></li>';
1238     } else {
1239         html += '<li>' +
1240             createBlameListHTML(g_resultsByBuilder[builder].chromeRevision, index,
1241                 'http://build.chromium.org/f/chromium/perf/dashboard/ui/changelog.html?url=/trunk/src&mode=html&range=', ':', 'Chrome') +
1242             '</li>';
1243
1244         var chromeRevision = g_resultsByBuilder[builder].chromeRevision[index];
1245         if (chromeRevision && isLayoutTestResults()) {
1246             html += '<li><a href="' + TEST_RESULTS_BASE_PATH + currentBuilders()[builder] +
1247                 '/' + chromeRevision + '/layout-test-results.zip">layout-test-results.zip</a></li>';
1248         }
1249     }
1250
1251     if (!isLayoutTestResults() && opt_testName && isFailure(builder, opt_testName, index))
1252         html += '<li>' + linkHTMLToOpenWindow(buildBasePath + pathToFailureLog(opt_testName), 'Failure log') + '</li>';
1253
1254     html += '</ul>';
1255     showPopup(e.target, html);
1256 }
1257
1258 function htmlForTestResults(test)
1259 {
1260     var html = '';
1261     var results = test.rawResults.concat();
1262     var times = test.rawTimes.concat();
1263     var builder = test.builder;
1264     var master = builderMaster(builder);
1265     var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
1266
1267     var indexToReplaceCurrentResult = -1;
1268     var indexToReplaceCurrentTime = -1;
1269     var currentResultArray, currentTimeArray, currentResult, innerHTML, resultString;
1270     for (var i = 0; i < buildNumbers.length; i++) {
1271         if (i > indexToReplaceCurrentResult) {
1272             currentResultArray = results.shift();
1273             if (currentResultArray) {
1274                 currentResult = currentResultArray[RLE.VALUE];
1275                 // Treat simplified diff failures as just text failures.
1276                 if (currentResult == 'S')
1277                     currentResult = 'F';
1278                 indexToReplaceCurrentResult += currentResultArray[RLE.LENGTH];
1279             } else {
1280                 currentResult = 'N';
1281                 indexToReplaceCurrentResult += buildNumbers.length;
1282             }
1283             resultString = expectationsFileStringForResult(currentResult);
1284         }
1285
1286         if (i > indexToReplaceCurrentTime) {
1287             currentTimeArray = times.shift();
1288             var currentTime = 0;
1289             if (currentResultArray) {
1290               currentTime = currentTimeArray[RLE.VALUE];
1291               indexToReplaceCurrentTime += currentTimeArray[RLE.LENGTH];
1292             } else
1293               indexToReplaceCurrentTime += buildNumbers.length;
1294
1295             innerHTML = currentTime || '&nbsp;';
1296         }
1297
1298         var extraClassNames = '';
1299         var webkitRevision = g_resultsByBuilder[builder].webkitRevision;
1300         var isWebkitMerge = webkitRevision[i + 1] && webkitRevision[i] != webkitRevision[i + 1];
1301         if (isWebkitMerge && master.name != WEBKIT_BUILDER_MASTER)
1302             extraClassNames += ' merge';
1303
1304         html += '<td title="' + (resultString || 'NO DATA') + '. Click for more info." class="results ' + currentResult +
1305           extraClassNames + '" onclick=\'showPopupForBuild(event, "' + builder + '",' + i + ',"' + test.test + '")\'>' + innerHTML;
1306     }
1307     return html;
1308 }
1309
1310 function htmlForTestsWithExpectationsButNoFailures(builder)
1311 {
1312     var tests = g_perBuilderWithExpectationsButNoFailures[builder];
1313     var skippedPaths = g_perBuilderSkippedPaths[builder];
1314     var showUnexpectedPassesLink =  linkHTMLToToggleState('showUnexpectedPasses', 'tests that have not failed in last ' + g_resultsByBuilder[builder].buildNumbers.length + ' runs');
1315     var showSkippedLink = linkHTMLToToggleState('showSkipped', 'skipped tests in TestExpectations');
1316     
1317
1318     var html = '';
1319     if (tests.length || skippedPaths.length) {
1320         var buildInfo = platformAndBuildType(builder);
1321         html += '<h2 style="display:inline-block">Expectations for ' + buildInfo.platform + '-' + buildInfo.buildType + '</h2> ';
1322         if (!g_currentState.showUnexpectedPasses && tests.length)
1323             html += showUnexpectedPassesLink;
1324         html += ' ';
1325         if (!g_currentState.showSkipped && skippedPaths.length)
1326             html += showSkippedLink;
1327     }
1328
1329     var open = '<div onclick="selectContents(this)">';
1330
1331     if (g_currentState.showUnexpectedPasses && tests.length) {
1332         html += '<div id="passing-tests">' + showUnexpectedPassesLink;
1333         for (var i = 0; i < tests.length; i++)
1334             html += open + tests[i].test + '</div>';
1335         html += '</div>';
1336     }
1337
1338     if (g_currentState.showSkipped && skippedPaths.length)
1339         html += '<div id="skipped-tests">' + showSkippedLink + open + skippedPaths.join('</div>' + open) + '</div></div>';
1340     return html + '<br>';
1341 }
1342
1343 // Returns whether we should exclude test results from the test table.
1344 function shouldHideTest(testResult)
1345 {
1346     if (testResult.isWontFixSkip)
1347         return !g_currentState.showWontFixSkip;
1348
1349     if (testResult.isFlaky)
1350         return !g_currentState.showFlaky;
1351
1352     if (isSlowTest(testResult))
1353         return !g_currentState.showSlow;
1354
1355     if (testResult.meetsExpectations)
1356         return !g_currentState.showCorrectExpectations;
1357
1358     return !g_currentState.showWrongExpectations;
1359 }
1360
1361 // Sets the browser's selection to the element's contents.
1362 function selectContents(element)
1363 {
1364     window.getSelection().selectAllChildren(element);
1365 }
1366
1367 function createBugHTML(test)
1368 {
1369     var symptom = test.isFlaky ? 'flaky' : 'failing';
1370     var title = encodeURIComponent('Layout Test ' + test.test + ' is ' + symptom);
1371     var description = encodeURIComponent('The following layout test is ' + symptom + ' on ' +
1372         '[insert platform]\n\n' + test.test + '\n\nProbable cause:\n\n' +
1373         '[insert probable cause]');
1374     
1375     var component = encodeURIComponent('Tools / Tests');
1376     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;
1377     return '<a href="' + url + '" class="file-bug">FILE BUG</a>';
1378 }
1379
1380 function isCrossBuilderView()
1381 {
1382     return g_currentState.tests || g_currentState.result || g_currentState.expectationsUpdate;
1383 }
1384
1385 function tableHeaders(opt_getAll)
1386 {
1387     var headers = [];
1388     if (isCrossBuilderView() || opt_getAll)
1389         headers.push('builder');
1390
1391     if (!isCrossBuilderView() || opt_getAll)
1392         headers.push('test');
1393
1394     if (isLayoutTestResults() || opt_getAll)
1395         headers.push('bugs', 'modifiers', 'expectations');
1396
1397     headers.push('slowest run', 'flakiness (numbers are runtimes in seconds)');
1398     return headers;
1399 }
1400
1401 function htmlForSingleTestRow(test)
1402 {
1403     if (!isCrossBuilderView() && shouldHideTest(test)) {
1404         // The innerHTML call is considerably faster if we exclude the rows for
1405         // items we're not showing than if we hide them using display:none.
1406         // For the crossBuilderView, we want to show all rows the user is
1407         // explicitly listing tests to view.
1408         return '';
1409     }
1410
1411     var headers = tableHeaders();
1412     var html = '';
1413     for (var i = 0; i < headers.length; i++) {
1414         var header = headers[i];
1415         if (string.startsWith(header, 'test') || string.startsWith(header, 'builder')) {
1416             // If isCrossBuilderView() is true, we're just viewing a single test
1417             // with results for many builders, so the first column is builder names
1418             // instead of test paths.
1419             var testCellClassName = 'test-link' + (isCrossBuilderView() ? ' builder-name' : '');
1420             var testCellHTML = isCrossBuilderView() ? test.builder : '<span class="link" onclick="setQueryParameter(\'tests\',\'' + test.test +'\');">' + test.test + '</span>';
1421
1422             html += '<tr><td class="' + testCellClassName + '">' + testCellHTML;
1423         } else if (string.startsWith(header, 'bugs'))
1424             html += '<td class=options-container>' + (test.bugs ? htmlForBugs(test.bugs) : createBugHTML(test));
1425         else if (string.startsWith(header, 'modifiers'))
1426             html += '<td class=options-container>' + test.modifiers;
1427         else if (string.startsWith(header, 'expectations'))
1428             html += '<td class=options-container>' + test.expectations;
1429         else if (string.startsWith(header, 'slowest'))
1430             html += '<td>' + (test.slowestTime ? test.slowestTime + 's' : '');
1431         else if (string.startsWith(header, 'flakiness'))
1432             html += htmlForTestResults(test);
1433     }
1434     return html;
1435 }
1436
1437 function sortColumnFromTableHeader(headerText)
1438 {
1439     return headerText.split(' ', 1)[0];
1440 }
1441
1442 function htmlForTableColumnHeader(headerName, opt_fillColSpan)
1443 {
1444     // Use the first word of the header title as the sortkey
1445     var thisSortValue = sortColumnFromTableHeader(headerName);
1446     var arrowHTML = thisSortValue == g_currentState.sortColumn ?
1447         '<span class=' + g_currentState.sortOrder + '>' + (g_currentState.sortOrder == FORWARD ? '&uarr;' : '&darr;' ) + '</span>' : '';
1448     return '<th sortValue=' + thisSortValue +
1449         // Extend last th through all the rest of the columns.
1450         (opt_fillColSpan ? ' colspan=10000' : '') +
1451         // Extra span here is so flex boxing actually centers.
1452         // There's probably a better way to do this with CSS only though.
1453         '><div class=table-header-content><span></span>' + arrowHTML +
1454         '<span class=header-text>' + headerName + '</span>' + arrowHTML + '</div></th>';
1455 }
1456
1457 function htmlForTestTable(rowsHTML, opt_excludeHeaders)
1458 {
1459     var html = '<table class=test-table>';
1460     if (!opt_excludeHeaders) {
1461         html += '<thead><tr>';
1462         var headers = tableHeaders();
1463         for (var i = 0; i < headers.length; i++)
1464             html += htmlForTableColumnHeader(headers[i], i == headers.length - 1);
1465         html += '</tr></thead>';
1466     }
1467     return html + '<tbody>' + rowsHTML + '</tbody></table>';
1468 }
1469
1470 function appendHTML(html)
1471 {
1472     var startTime = Date.now();
1473     // InnerHTML to a div that's not in the document. This is
1474     // ~300ms faster in Safari 4 and Chrome 4 on mac.
1475     var div = document.createElement('div');
1476     div.innerHTML = html;
1477     document.body.appendChild(div);
1478     logTime('Time to innerHTML', startTime);
1479     postHeightChangedMessage();
1480 }
1481
1482 function alphanumericCompare(column, reverse)
1483 {
1484     return reversibleCompareFunction(function(a, b) {
1485         // Put null entries at the bottom
1486         var a = a[column] ? String(a[column]) : 'z';
1487         var b = b[column] ? String(b[column]) : 'z';
1488
1489         if (a < b)
1490             return -1;
1491         else if (a == b)
1492             return 0;
1493         else
1494             return 1;
1495     }, reverse);
1496 }
1497
1498 function numericSort(column, reverse)
1499 {
1500     return reversibleCompareFunction(function(a, b) {
1501         a = parseFloat(a[column]);
1502         b = parseFloat(b[column]);
1503         return a - b;
1504     }, reverse);
1505 }
1506
1507 function reversibleCompareFunction(compare, reverse)
1508 {
1509     return function(a, b) {
1510         return compare(reverse ? b : a, reverse ? a : b);
1511     };
1512 }
1513
1514 function changeSort(e)
1515 {
1516     var target = e.currentTarget;
1517     e.preventDefault();
1518
1519     var sortValue = target.getAttribute('sortValue');
1520     while (target && target.tagName != 'TABLE')
1521         target = target.parentNode;
1522
1523     var sort = 'sortColumn';
1524     var orderKey = 'sortOrder';
1525     if (sortValue == g_currentState[sort] && g_currentState[orderKey] == FORWARD)
1526         order = BACKWARD;
1527     else
1528         order = FORWARD;
1529
1530     setQueryParameter(sort, sortValue, orderKey, order);
1531 }
1532
1533 function sortTests(tests, column, order)
1534 {
1535     var resultsProperty, sortFunctionGetter;
1536     if (column == 'flakiness') {
1537         sortFunctionGetter = numericSort;
1538         resultsProperty = 'flips';
1539     } else if (column == 'slowest') {
1540         sortFunctionGetter = numericSort;
1541         resultsProperty = 'slowestTime';
1542     } else {
1543         sortFunctionGetter = alphanumericCompare;
1544         resultsProperty = column;
1545     }
1546
1547     tests.sort(sortFunctionGetter(resultsProperty, order == BACKWARD));
1548 }
1549
1550 // Sorts a space separated expectations string in alphanumeric order.
1551 // @param {string} str The expectations string.
1552 // @return {string} The sorted string.
1553 function sortExpectationsString(str)
1554 {
1555     return str.split(' ').sort().join(' ');
1556 }
1557
1558 function addUpdate(testsNeedingUpdate, test, builderName, missing, extra)
1559 {
1560     if (!testsNeedingUpdate[test])
1561         testsNeedingUpdate[test] = {};
1562
1563     var buildInfo = platformAndBuildType(builderName);
1564     var builder = buildInfo.platform + ' ' + buildInfo.buildType;
1565     if (!testsNeedingUpdate[test][builder])
1566         testsNeedingUpdate[test][builder] = {};
1567
1568     if (missing)
1569         testsNeedingUpdate[test][builder].missing = sortExpectationsString(missing);
1570
1571     if (extra)
1572         testsNeedingUpdate[test][builder].extra = sortExpectationsString(extra);
1573 }
1574
1575
1576 // From a string of modifiers, returns a string of modifiers that
1577 // are for real result changes, like SLOW, and excludes modifiers
1578 // that specificy things like platform, build_type, bug.
1579 // @param {string} modifierString String containing all modifiers.
1580 // @return {string} String containing only modifiers that effect the results.
1581 function realModifiers(modifierString)
1582 {
1583     var modifiers = modifierString.split(' ');;
1584     return modifiers.filter(function(modifier) {
1585         if (modifier in BUILD_TYPES || string.startsWith(modifier, 'BUG'))
1586             return false;
1587
1588         var matchesPlatformOrUnion = false;
1589         traversePlatformsTree(function(platform, platformName) {
1590             if (matchesPlatformOrUnion)
1591                 return;
1592
1593             if (platform.fallbackPlatforms) {
1594                 platform.fallbackPlatforms.forEach(function(fallbackPlatform) {
1595                     if (matchesPlatformOrUnion)
1596                         return;
1597
1598                     var fallbackPlatformObject = platformObjectForName(fallbackPlatform);
1599                     if (!fallbackPlatformObject.platformModifierUnions)
1600                         return;
1601
1602                     matchesPlatformOrUnion = modifier in fallbackPlatformObject.subPlatforms || modifier in fallbackPlatformObject.platformModifierUnions;
1603                 });
1604             }
1605         });
1606
1607         return !matchesPlatformOrUnion;
1608     }).join(' ');
1609 }
1610
1611 function generatePageForExpectationsUpdate()
1612 {
1613     // Always show all runs when auto-updating expectations.
1614     if (!g_crossDashboardState.showAllRuns)
1615         setQueryParameter('showAllRuns', true);
1616
1617     processTestRunsForAllBuilders();
1618     var testsNeedingUpdate = {};
1619     for (var test in g_testToResultsMap) {
1620         var results = g_testToResultsMap[test];
1621         for (var i = 0; i < results.length; i++) {
1622             var thisResult = results[i];
1623             
1624             if (!thisResult.missing && !thisResult.extra)
1625                 continue;
1626
1627             var allPassesOrNoDatas = thisResult.rawResults.filter(function (x) { return x[1] != "P" && x[1] != "N"; }).length == 0;
1628
1629             if (allPassesOrNoDatas)
1630                 continue;
1631
1632             addUpdate(testsNeedingUpdate, test, thisResult.builder, thisResult.missing, thisResult.extra);
1633         }
1634     }
1635
1636     for (var builder in currentBuilders()) {
1637         var tests = g_perBuilderWithExpectationsButNoFailures[builder]
1638         for (var i = 0; i < tests.length; i++) {
1639             // Anything extra in this case is what is listed in expectations
1640             // plus modifiers other than bug, platform, build type.
1641             var modifiers = realModifiers(tests[i].modifiers);
1642             var extras = tests[i].expectations;
1643             extras += modifiers ? ' ' + modifiers : '';
1644             addUpdate(testsNeedingUpdate, tests[i].test, builder, null, extras);
1645         }
1646     }
1647
1648     // Get the keys in alphabetical order, so it is easy to process groups
1649     // of tests.
1650     var keys = Object.keys(testsNeedingUpdate).sort();
1651     showUpdateInfoForTest(testsNeedingUpdate, keys);
1652 }
1653
1654 // Show the test results and the json for differing expectations, and
1655 // allow the user to include or exclude this update.
1656 //
1657 // @param {Object} testsNeedingUpdate Tests that need updating.
1658 // @param {Array.<string>} keys Keys into the testNeedingUpdate object.
1659 function showUpdateInfoForTest(testsNeedingUpdate, keys)
1660 {
1661     var test = keys[g_currentState.updateIndex];
1662     document.body.innerHTML = '';
1663
1664     // FIXME: Make this DOM creation less verbose.
1665     var index = document.createElement('div');
1666     index.style.cssFloat = 'right';
1667     index.textContent = (g_currentState.updateIndex + 1) + ' of ' + keys.length + ' tests';
1668     document.body.appendChild(index);
1669
1670     var buttonRegion = document.createElement('div');
1671     var includeBtn = document.createElement('input');
1672     includeBtn.type = 'button';
1673     includeBtn.value = 'include selected';
1674     includeBtn.addEventListener('click', partial(handleUpdate, testsNeedingUpdate, keys), false);
1675     buttonRegion.appendChild(includeBtn);
1676
1677     var previousBtn = document.createElement('input');
1678     previousBtn.type = 'button';
1679     previousBtn.value = 'previous';
1680     previousBtn.addEventListener('click',
1681         function() {
1682           setUpdateIndex(g_currentState.updateIndex - 1, testsNeedingUpdate, keys);
1683         },
1684         false);
1685     buttonRegion.appendChild(previousBtn);
1686
1687     var nextBtn = document.createElement('input');
1688     nextBtn.type = 'button';
1689     nextBtn.value = 'next';
1690     nextBtn.addEventListener('click', partial(nextUpdate, testsNeedingUpdate, keys), false);
1691     buttonRegion.appendChild(nextBtn);
1692
1693     var doneBtn = document.createElement('input');
1694     doneBtn.type = 'button';
1695     doneBtn.value = 'done';
1696     doneBtn.addEventListener('click', finishUpdate, false);
1697     buttonRegion.appendChild(doneBtn);
1698
1699     document.body.appendChild(buttonRegion);
1700
1701     var updates = testsNeedingUpdate[test];
1702     var checkboxes = document.createElement('div');
1703     for (var builder in updates) {
1704         // Create a checkbox for each builder.
1705         var checkboxRegion = document.createElement('div');
1706         var checkbox = document.createElement('input');
1707         checkbox.type = 'checkbox';
1708         checkbox.id = builder;
1709         checkbox.checked = true;
1710         checkboxRegion.appendChild(checkbox);
1711         checkboxRegion.appendChild(document.createTextNode(builder + ' : ' + JSON.stringify(updates[builder])));
1712         checkboxes.appendChild(checkboxRegion);
1713     }
1714     document.body.appendChild(checkboxes);
1715
1716     var div = document.createElement('div');
1717     div.innerHTML = htmlForIndividualTestOnAllBuildersWithResultsLinks(test);
1718     document.body.appendChild(div);
1719     appendExpectations();
1720 }
1721
1722
1723 // When the user has finished selecting expectations to update, provide them
1724 // with json to copy over.
1725 function finishUpdate()
1726 {
1727     document.body.innerHTML = 'The next step is to copy the output below ' +
1728         'into a local file and save it.  Then, run<br><code>python ' +
1729         'src/webkit/tools/layout_tests/webkitpy/layout_tests/update_expectat' +
1730         'ions_from_dashboard.py path/to/local/file</code><br>in order to ' +
1731         'update the expectations file.<br><textarea id="results" '+
1732         'style="width:600px;height:600px;"> ' +
1733         JSON.stringify(g_confirmedTests) + '</textarea>';
1734     results.focus();
1735     document.execCommand('SelectAll');
1736 }
1737
1738 // Handle user click on "include selected" button.
1739 // Includes the tests that are selected and exclude the rest.
1740 // @param {Object} testsNeedingUpdate Tests that need updating.
1741 // @param {Array.<string>} keys Keys into the testNeedingUpdate object.
1742 function handleUpdate(testsNeedingUpdate, keys)
1743 {
1744     var test = keys[g_currentState.updateIndex];
1745     var updates = testsNeedingUpdate[test];
1746     for (var builder in updates) {
1747         // Add included tests, and delete excluded tests if
1748         // they were previously included.
1749         if ($(builder).checked) {
1750             if (!g_confirmedTests[test])
1751                 g_confirmedTests[test] = {};
1752             g_confirmedTests[test][builder] = testsNeedingUpdate[test][builder];
1753         } else if (g_confirmedTests[test] && g_confirmedTests[test][builder]) {
1754             delete g_confirmedTests[test][builder];
1755             if (!Object.keys(g_confirmedTests[test]).length)
1756                 delete g_confirmedTests[test];
1757         }
1758     }
1759     nextUpdate(testsNeedingUpdate, keys);
1760 }
1761
1762
1763 // Move to the next item to update.
1764 // @param {Object} testsNeedingUpdate Tests that need updating.
1765 // @param {Array.<string>} keys Keys into the testNeedingUpdate object.
1766 function nextUpdate(testsNeedingUpdate, keys)
1767 {
1768     setUpdateIndex(g_currentState.updateIndex + 1, testsNeedingUpdate, keys);
1769 }
1770
1771
1772 // Advance the index we are updating at.  If we walk over the end
1773 // or beginning, just loop.
1774 // @param {string} newIndex The index into the keys to move to.
1775 // @param {Object} testsNeedingUpdate Tests that need updating.
1776 // @param {Array.<string>} keys Keys into the testNeedingUpdate object.
1777 function setUpdateIndex(newIndex, testsNeedingUpdate, keys)
1778 {
1779     if (newIndex == -1)
1780         newIndex = keys.length - 1;
1781     else if (newIndex == keys.length)
1782         newIndex = 0;
1783     setQueryParameter("updateIndex", newIndex);
1784     showUpdateInfoForTest(testsNeedingUpdate, keys);
1785 }
1786
1787 function htmlForIndividualTestOnAllBuilders(test)
1788 {
1789     processTestRunsForAllBuilders();
1790
1791     var testResults = g_testToResultsMap[test];
1792     if (!testResults)
1793         return '<div class="not-found">Test not found. Either it does not exist, is skipped or passes on all platforms.</div>';
1794         
1795     var html = '';
1796     var shownBuilders = [];
1797     for (var j = 0; j < testResults.length; j++) {
1798         shownBuilders.push(testResults[j].builder);
1799         html += htmlForSingleTestRow(testResults[j]);
1800     }
1801
1802     var skippedBuilders = []
1803     for (builder in currentBuilders()) {
1804         if (shownBuilders.indexOf(builder) == -1)
1805             skippedBuilders.push(builder);
1806     }
1807
1808     var skippedBuildersHtml = '';
1809     if (skippedBuilders.length) {
1810         skippedBuildersHtml = '<div>The following builders either don\'t run this test (e.g. it\'s skipped) or all runs passed:</div>' +
1811             '<div class=skipped-builder-list><div class=skipped-builder>' + skippedBuilders.join('</div><div class=skipped-builder>') + '</div></div>';
1812     }
1813
1814     return htmlForTestTable(html) + skippedBuildersHtml;
1815 }
1816
1817 function htmlForIndividualTestOnAllBuildersWithResultsLinks(test)
1818 {
1819     processTestRunsForAllBuilders();
1820
1821     var testResults = g_testToResultsMap[test];
1822     var html = '';
1823     html += htmlForIndividualTestOnAllBuilders(test);
1824
1825     html += '<div class=expectations test=' + test + '><div>' +
1826         linkHTMLToToggleState('showExpectations', 'results')
1827
1828     if (isLayoutTestResults() || isGPUTestResults()) {
1829         if (isLayoutTestResults())
1830             html += ' | ' + linkHTMLToToggleState('showLargeExpectations', 'large thumbnails');
1831         if (testResults && currentBuilderGroup().master().name == WEBKIT_BUILDER_MASTER) {
1832             var revision = g_currentState.revision || '';
1833             html += '<form onsubmit="setQueryParameter(\'revision\', revision.value);' +
1834                 'return false;">Show results for WebKit revision: ' +
1835                 '<input name=revision placeholder="e.g. 65540" value="' + revision +
1836                 '" id=revision-input></form>';
1837         } else
1838             html += ' | <b>Only shows actual results/diffs from the most recent *failure* on each bot.</b>';
1839     } else {
1840       html += ' | <span>Results height:<input ' +
1841           'onchange="setQueryParameter(\'resultsHeight\',this.value)" value="' +
1842           g_currentState.resultsHeight + '" style="width:2.5em">px</span>';
1843     }
1844     html += '</div></div>';
1845     return html;
1846 }
1847
1848 function getExpectationsContainer(expectationsContainers, parentContainer, expectationsType)
1849 {
1850     if (!expectationsContainers[expectationsType]) {
1851         var container = document.createElement('div');
1852         container.className = 'expectations-container';
1853         parentContainer.appendChild(container);
1854         expectationsContainers[expectationsType] = container;
1855     }
1856     return expectationsContainers[expectationsType];
1857 }
1858
1859 function ensureTrailingSlash(path)
1860 {
1861     if (path.match(/\/$/))
1862         return path;
1863     return path + '/';
1864 }
1865
1866 function maybeAddPngChecksum(expectationDiv, pngUrl)
1867 {
1868     // pngUrl gets served from the browser cache since we just loaded it in an
1869     // <img> tag.
1870     loader.request(pngUrl,
1871         function(xhr) {
1872             // Convert the first 2k of the response to a byte string.
1873             var bytes = xhr.responseText.substring(0, 2048);
1874             for (var position = 0; position < bytes.length; ++position)
1875                 bytes[position] = bytes[position] & 0xff;
1876
1877             // Look for the comment.
1878             var commentKey = 'tEXtchecksum\x00';
1879             var checksumPosition = bytes.indexOf(commentKey);
1880             if (checksumPosition == -1)
1881                 return;
1882
1883             var checksum = bytes.substring(checksumPosition + commentKey.length, checksumPosition + commentKey.length + 32);
1884             var checksumContainer = document.createElement('span');
1885             checksumContainer.innerText = 'Embedded checksum: ' + checksum;
1886             checksumContainer.setAttribute('class', 'pngchecksum');
1887             expectationDiv.parentNode.appendChild(checksumContainer);
1888         },
1889         function(xhr) {},
1890         true);
1891 }
1892
1893 // Adds a specific expectation. If it's an image, it's only added on the
1894 // image's onload handler. If it's a text file, then a script tag is appended
1895 // as a hack to see if the file 404s (necessary since it's cross-domain).
1896 // Once all the expectations for a specific type have loaded or errored
1897 // (e.g. all the text results), then we go through and identify which platform
1898 // uses which expectation.
1899 //
1900 // @param {Object} expectationsContainers Map from expectations type to
1901 //     container DIV.
1902 // @param {Element} parentContainer Container element for
1903 //     expectationsContainer divs.
1904 // @param {string} platform Platform string. Empty string for non-platform
1905 //     specific expectations.
1906 // @param {string} path Relative path to the expectation.
1907 // @param {string} base Base path for the expectation URL.
1908 // @param {string} opt_builder Builder whose actual results this expectation
1909 //     points to.
1910 // @param {string} opt_suite "virtual suite" that the test belongs to, if any.
1911 function addExpectationItem(expectationsContainers, parentContainer, platform, path, base, opt_builder, opt_suite)
1912 {
1913     var parts = path.split('.')
1914     var fileExtension = parts[parts.length - 1];
1915     if (fileExtension == 'html')
1916         fileExtension = 'txt';
1917     
1918     var container = getExpectationsContainer(expectationsContainers, parentContainer, fileExtension);
1919     var isImage = path.match(/\.png$/);
1920
1921     // FIXME: Stop using script tags once all the places we pull from support CORS.
1922     var platformPart = platform ? ensureTrailingSlash(platform) : '';
1923     var suitePart = opt_suite ? ensureTrailingSlash(opt_suite) : '';
1924
1925     var childContainer = document.createElement('span');
1926     childContainer.className = 'unloaded';
1927
1928     var appendExpectationsItem = function(item) {
1929         childContainer.appendChild(expectationsTitle(platformPart + suitePart, path, opt_builder));
1930         childContainer.className = 'expectations-item';
1931         item.className = 'expectation ' + fileExtension;
1932         if (g_currentState.showLargeExpectations)
1933             item.className += ' large';
1934         childContainer.appendChild(item);
1935         handleFinishedLoadingExpectations(container);
1936     };
1937
1938     var url = base + platformPart + path;
1939     if (isImage || !string.startsWith(base, 'http://svn.webkit.org')) {
1940         var dummyNode = document.createElement(isImage ? 'img' : 'script');
1941         dummyNode.src = url;
1942         dummyNode.onload = function() {
1943             var item;
1944             if (isImage) {
1945                 item = dummyNode;
1946                 if (string.startsWith(base, 'http://svn.webkit.org'))
1947                     maybeAddPngChecksum(item, url);
1948             } else {
1949                 item = document.createElement('iframe');
1950                 item.src = url;
1951             }
1952             appendExpectationsItem(item);
1953         }
1954         dummyNode.onerror = function() {
1955             childContainer.parentNode.removeChild(childContainer);
1956             handleFinishedLoadingExpectations(container);
1957         }
1958
1959         // Append script elements now so that they load. Images load without being
1960         // appended to the DOM.
1961         if (!isImage)
1962             childContainer.appendChild(dummyNode);
1963     } else {
1964         loader.request(url,
1965             function(xhr) {
1966                 var item = document.createElement('pre');
1967                 item.innerText = xhr.responseText;
1968                 appendExpectationsItem(item);
1969             },
1970             function(xhr) {/* Do nothing on errors since they're expected */});
1971     }
1972
1973     container.appendChild(childContainer);
1974 }
1975
1976
1977 // Identifies which expectations are used on which platform once all the
1978 // expectations of a given type have loaded (e.g. the container for png
1979 // expectations for this test had no child elements with the class
1980 // "unloaded").
1981 //
1982 // @param {string} container Element containing the expectations for a given
1983 //     test and a given type (e.g. png).
1984 function handleFinishedLoadingExpectations(container)
1985 {
1986     if (container.getElementsByClassName('unloaded').length)
1987         return;
1988
1989     var titles = container.getElementsByClassName('expectations-title');
1990     for (var platform in g_fallbacksMap) {
1991         var fallbacks = g_fallbacksMap[platform];
1992         var winner = null;
1993         var winningIndex = -1;
1994         for (var i = 0; i < titles.length; i++) {
1995             var title = titles[i];
1996
1997             if (!winner && title.platform == "") {
1998                 winner = title;
1999                 continue;
2000             }
2001
2002             var rawPlatform = title.platform && title.platform.replace('platform/', '');
2003             for (var j = 0; j < fallbacks.length; j++) {
2004                 if ((winningIndex == -1 || winningIndex > j) && rawPlatform == fallbacks[j]) {
2005                     winningIndex = j;
2006                     winner = title;
2007                     break;
2008                 }
2009             }
2010         }
2011         if (winner)
2012             winner.getElementsByClassName('platforms')[0].innerHTML += '<div class=used-platform>' + platform + '</div>';
2013         else {
2014             console.log('No expectations identified for this test. This means ' +
2015                 'there is a logic bug in the dashboard for which expectations a ' +
2016                 'platform uses or trac.webkit.org/src.chromium.org is giving 5XXs.');
2017         }
2018     }
2019
2020     consolidateUsedPlatforms(container);
2021 }
2022
2023 // Consolidate platforms when all sub-platforms for a given platform are represented.
2024 // e.g., if all of the WIN- platforms are there, replace them with just WIN.
2025 function consolidateUsedPlatforms(container)
2026 {
2027     var allPlatforms = Object.keys(g_fallbacksMap);
2028
2029     var platformElements = container.getElementsByClassName('platforms');
2030     for (var i = 0, platformsLength = platformElements.length; i < platformsLength; i++) {
2031         var usedPlatforms = platformElements[i].getElementsByClassName('used-platform');
2032         if (!usedPlatforms.length)
2033             continue;
2034
2035         var platforms = {};
2036         platforms['MAC'] = {};
2037         platforms['WIN'] = {};
2038         platforms['LINUX'] = {};
2039         allPlatforms.forEach(function(platform) {
2040             if (string.startsWith(platform, 'MAC'))
2041                 platforms['MAC'][platform] = 1;
2042             else if (string.startsWith(platform, 'WIN'))
2043                 platforms['WIN'][platform] = 1;
2044             else if (string.startsWith(platform, 'LINUX'))
2045                 platforms['LINUX'][platform] = 1;
2046         });
2047
2048         for (var j = 0, usedPlatformsLength = usedPlatforms.length; j < usedPlatformsLength; j++) {
2049             for (var platform in platforms)
2050                 delete platforms[platform][usedPlatforms[j].textContent];
2051         }
2052
2053         for (var platform in platforms) {
2054             if (!Object.keys(platforms[platform]).length) {
2055                 var nodesToRemove = [];
2056                 for (var j = 0, usedPlatformsLength = usedPlatforms.length; j < usedPlatformsLength; j++) {
2057                     var usedPlatform = usedPlatforms[j];
2058                     if (string.startsWith(usedPlatform.textContent, platform))
2059                         nodesToRemove.push(usedPlatform);
2060                 }
2061
2062                 nodesToRemove.forEach(function(element) { element.parentNode.removeChild(element); });
2063                 platformElements[i].insertAdjacentHTML('afterBegin', '<div class=used-platform>' + platform + '</div>');
2064             }
2065         }
2066     }
2067 }
2068
2069 function addExpectations(expectationsContainers, container, base,
2070     platform, text, png, reftest_html_file, reftest_mismatch_html_file, suite)
2071 {
2072     var builder = '';
2073     addExpectationItem(expectationsContainers, container, platform, text, base, builder, suite);
2074     addExpectationItem(expectationsContainers, container, platform, png, base, builder, suite);
2075     addExpectationItem(expectationsContainers, container, platform, reftest_html_file, base, builder, suite);
2076     addExpectationItem(expectationsContainers, container, platform, reftest_mismatch_html_file, base, builder, suite);
2077 }
2078
2079 function expectationsTitle(platform, path, builder)
2080 {
2081     var header = document.createElement('h3');
2082     header.className = 'expectations-title';
2083
2084     var innerHTML;
2085     if (builder) {
2086         var resultsType;
2087         if (string.endsWith(path, '-crash-log.txt'))
2088             resultsType = 'STACKTRACE';
2089         else if (string.endsWith(path, '-actual.txt') || string.endsWith(path, '-actual.png'))
2090             resultsType = 'ACTUAL RESULTS';
2091         else if (string.endsWith(path, '-wdiff.html'))
2092             resultsType = 'WDIFF';
2093         else
2094             resultsType = 'DIFF';
2095
2096         innerHTML = resultsType + ': ' + builder;
2097     } else if (platform === "") {
2098         var parts = path.split('/');
2099         innerHTML = parts[parts.length - 1];
2100     } else
2101         innerHTML = platform || path;
2102
2103     header.innerHTML = '<div class=title>' + innerHTML +
2104         '</div><div style="float:left">&nbsp;</div>' +
2105         '<div class=platforms style="float:right"></div>';
2106     header.platform = platform;
2107     return header;
2108 }
2109
2110 function loadExpectations(expectationsContainer)
2111 {
2112     var test = expectationsContainer.getAttribute('test');
2113     if (isLayoutTestResults())
2114         loadExpectationsLayoutTests(test, expectationsContainer);
2115     else {
2116         var results = g_testToResultsMap[test];
2117         for (var i = 0; i < results.length; i++)
2118             if (isGPUTestResults())
2119                 loadGPUResultsForBuilder(results[i].builder, test, expectationsContainer);
2120             else
2121                 loadNonWebKitResultsForBuilder(results[i].builder, test, expectationsContainer);
2122     }
2123 }
2124
2125 function gpuResultsPath(chromeRevision, builder)
2126 {
2127   return chromeRevision + '_' + builder.replace(/[^A-Za-z0-9]+/g, '_');
2128 }
2129
2130 function loadGPUResultsForBuilder(builder, test, expectationsContainer)
2131 {
2132     var container = document.createElement('div');
2133     container.className = 'expectations-container';
2134     container.innerHTML = '<div><b>' + builder + '</b></div>';
2135     expectationsContainer.appendChild(container);
2136
2137     var failureIndex = indexesForFailures(builder, test)[0];
2138
2139     var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndex];
2140     var pathToLog = builderMaster(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
2141
2142     var chromeRevision = g_resultsByBuilder[builder].chromeRevision[failureIndex];
2143     var resultsUrl = GPU_RESULTS_BASE_PATH + gpuResultsPath(chromeRevision, builder);
2144     var filename = test.split(/\./)[1] + '.png';
2145
2146     appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
2147     appendNonWebKitResults(container, resultsUrl + '/gen/' + filename, 'gpu-test-results', 'Generated');
2148     appendNonWebKitResults(container, resultsUrl + '/ref/' + filename, 'gpu-test-results', 'Reference');
2149     appendNonWebKitResults(container, resultsUrl + '/diff/' + filename, 'gpu-test-results', 'Diff');
2150 }
2151
2152 function loadNonWebKitResultsForBuilder(builder, test, expectationsContainer)
2153 {
2154     var failureIndexes = indexesForFailures(builder, test);
2155     var container = document.createElement('div');
2156     container.innerHTML = '<div><b>' + builder + '</b></div>';
2157     expectationsContainer.appendChild(container);
2158     for (var i = 0; i < failureIndexes.length; i++) {
2159         // FIXME: This doesn't seem to work anymore. Did the paths change?
2160         // Once that's resolved, see if we need to try each GTEST_MODIFIERS prefix as well.
2161         var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndexes[i]];
2162         var pathToLog = builderMaster(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
2163         appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
2164     }
2165 }
2166
2167 function appendNonWebKitResults(container, url, itemClassName, opt_title)
2168 {
2169     // Use a script tag to detect whether the URL 404s.
2170     // Need to use a script tag since the URL is cross-domain.
2171     var dummyNode = document.createElement('script');
2172     dummyNode.src = url;
2173
2174     dummyNode.onload = function() {
2175         var item = document.createElement('iframe');
2176         item.src = dummyNode.src;
2177         item.className = itemClassName;
2178         item.style.height = g_currentState.resultsHeight + 'px';
2179
2180         if (opt_title) {
2181             var childContainer = document.createElement('div');
2182             childContainer.style.display = 'inline-block';
2183             var title = document.createElement('div');
2184             title.textContent = opt_title;
2185             childContainer.appendChild(title);
2186             childContainer.appendChild(item);
2187             container.replaceChild(childContainer, dummyNode);
2188         } else
2189             container.replaceChild(item, dummyNode);
2190     }
2191     dummyNode.onerror = function() {
2192         container.removeChild(dummyNode);
2193     }
2194
2195     container.appendChild(dummyNode);
2196 }
2197
2198 function buildInfoForRevision(builder, revision)
2199 {
2200     var revisions = g_resultsByBuilder[builder].webkitRevision;
2201     var revisionStart = 0, revisionEnd = 0, buildNumber = 0;
2202     for (var i = 0; i < revisions.length; i++) {
2203         if (revision > revisions[i]) {
2204             revisionStart = revisions[i - 1];
2205             revisionEnd = revisions[i];
2206             buildNumber = g_resultsByBuilder[builder].buildNumbers[i - 1];
2207             break;
2208         }
2209     }
2210
2211     if (revisionEnd)
2212       revisionEnd++;
2213     else
2214       revisionEnd = '';
2215
2216     return {revisionStart: revisionStart, revisionEnd: revisionEnd, buildNumber: buildNumber};
2217 }
2218
2219 function lookupVirtualTestSuite(test) {
2220     for (var suite in VIRTUAL_SUITES) {
2221         if (test.indexOf(suite) != -1)
2222             return suite;
2223     }
2224     return '';
2225 }
2226
2227 function baseTest(test, suite) {
2228     base = VIRTUAL_SUITES[suite];
2229     return base ? test.replace(suite, base) : test;
2230 }
2231
2232 function loadBaselinesForTest(expectationsContainers, expectationsContainer, test) {
2233     var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
2234     var text = testWithoutSuffix + "-expected.txt";
2235     var png = testWithoutSuffix + "-expected.png";
2236     var reftest_html_file = testWithoutSuffix + "-expected.html";
2237     var reftest_mismatch_html_file = testWithoutSuffix + "-expected-mismatch.html";
2238     var suite = lookupVirtualTestSuite(test);
2239
2240     if (!suite)
2241         addExpectationItem(expectationsContainers, expectationsContainer, null, test, TEST_URL_BASE_PATH);
2242
2243     addExpectations(expectationsContainers, expectationsContainer,
2244         TEST_URL_BASE_PATH, '', text, png, reftest_html_file, reftest_mismatch_html_file, suite);
2245
2246     var fallbacks = allFallbacks();
2247     for (var i = 0; i < fallbacks.length; i++) {
2248       var fallback = 'platform/' + fallbacks[i];
2249       addExpectations(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH, fallback, text, png,
2250           reftest_html_file, reftest_mismatch_html_file, suite);
2251     }
2252
2253     if (suite)
2254         loadBaselinesForTest(expectationsContainers, expectationsContainer, baseTest(test, suite));
2255 }
2256
2257 function loadExpectationsLayoutTests(test, expectationsContainer)
2258 {
2259     // Map from file extension to container div for expectations of that type.
2260     var expectationsContainers = {};
2261
2262     var revisionContainer = document.createElement('div');
2263     revisionContainer.textContent = "Showing results for: "
2264     expectationsContainer.appendChild(revisionContainer);
2265     for (var builder in currentBuilders()) {
2266         if (builderMaster(builder).name == WEBKIT_BUILDER_MASTER) {
2267             var latestRevision = g_currentState.revision || g_resultsByBuilder[builder].webkitRevision[0];
2268             var buildInfo = buildInfoForRevision(builder, latestRevision);
2269             var revisionInfo = document.createElement('div');
2270             revisionInfo.style.cssText = 'background:lightgray;margin:0 3px;padding:0 2px;display:inline-block;';
2271             revisionInfo.innerHTML = builder + ' r' + buildInfo.revisionEnd +
2272                 ':r' + buildInfo.revisionStart + ', build ' + buildInfo.buildNumber;
2273             revisionContainer.appendChild(revisionInfo);
2274         }
2275     }
2276
2277     loadBaselinesForTest(expectationsContainers, expectationsContainer, test);
2278         
2279     var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
2280     var actualResultSuffixes = ['-actual.txt', '-actual.png', '-crash-log.txt', '-diff.txt', '-wdiff.html', '-diff.png'];
2281
2282     for (var builder in currentBuilders()) {
2283         var actualResultsBase;
2284         if (builderMaster(builder).name == WEBKIT_BUILDER_MASTER) {
2285             var latestRevision = g_currentState.revision || g_resultsByBuilder[builder].webkitRevision[0];
2286             var buildInfo = buildInfoForRevision(builder, latestRevision);
2287             actualResultsBase = 'http://build.webkit.org/results/' + builder +
2288                 '/r' + buildInfo.revisionStart + ' (' + buildInfo.buildNumber + ')/';
2289         } else
2290             actualResultsBase = TEST_RESULTS_BASE_PATH + currentBuilders()[builder] + '/results/layout-test-results/';
2291
2292         for (var i = 0; i < actualResultSuffixes.length; i++) {
2293             addExpectationItem(expectationsContainers, expectationsContainer, null,
2294                 testWithoutSuffix + actualResultSuffixes[i], actualResultsBase, builder);
2295         }
2296     }
2297
2298     // Add a clearing element so floated elements don't bleed out of their
2299     // containing block.
2300     var br = document.createElement('br');
2301     br.style.clear = 'both';
2302     expectationsContainer.appendChild(br);
2303 }
2304
2305 var g_allFallbacks;
2306
2307 // Returns the reverse sorted, deduped list of all platform fallback
2308 // directories.
2309 function allFallbacks()
2310 {
2311     if (!g_allFallbacks) {
2312         var holder = {};
2313         for (var platform in g_fallbacksMap) {
2314             var fallbacks = g_fallbacksMap[platform];
2315             for (var i = 0; i < fallbacks.length; i++)
2316                 holder[fallbacks[i]] = 1;
2317         }
2318
2319         g_allFallbacks = [];
2320         for (var fallback in holder)
2321             g_allFallbacks.push(fallback);
2322
2323         g_allFallbacks.sort(function(a, b) {
2324             if (a == b)
2325                 return 0;
2326             return a < b;
2327         });
2328     }
2329     return g_allFallbacks;
2330 }
2331
2332 function appendExpectations()
2333 {
2334     var expectations = g_currentState.showExpectations ? document.getElementsByClassName('expectations') : [];
2335     // Loading expectations is *very* slow. Use a large timeout to avoid
2336     // totally hanging the renderer.
2337     performChunkedAction(expectations, function(chunk) {
2338         for (var i = 0, len = chunk.length; i < len; i++)
2339             loadExpectations(chunk[i]);
2340         postHeightChangedMessage();
2341
2342     }, hideLoadingUI, 10000);
2343 }
2344
2345 function hideLoadingUI()
2346 {
2347     var loadingDiv = $('loading-ui');
2348     if (loadingDiv)
2349         loadingDiv.style.display = 'none';
2350     postHeightChangedMessage();
2351 }
2352
2353 function generatePageForIndividualTests(tests)
2354 {
2355     console.log('Number of tests: ' + tests.length);
2356     if (g_currentState.showChrome)
2357         appendHTML(htmlForNavBar());
2358     performChunkedAction(tests, function(chunk) {
2359         appendHTML(htmlForIndividualTests(chunk));
2360     }, appendExpectations, 500);
2361     if (g_currentState.showChrome)
2362         $('tests-input').value = g_currentState.tests;
2363 }
2364
2365 function performChunkedAction(tests, handleChunk, onComplete, timeout, opt_index) {
2366     var index = opt_index || 0;
2367     setTimeout(function() {
2368         var chunk = Array.prototype.slice.call(tests, index * CHUNK_SIZE, (index + 1) * CHUNK_SIZE);
2369         if (chunk.length) {
2370             handleChunk(chunk);
2371             performChunkedAction(tests, handleChunk, onComplete, timeout, ++index);
2372         } else
2373             onComplete();
2374     // No need for a timeout on the first chunked action.
2375     }, index ? timeout : 0);
2376 }
2377
2378 function htmlForIndividualTests(tests)
2379 {
2380     var testsHTML = [];
2381     for (var i = 0; i < tests.length; i++) {
2382         var test = tests[i];
2383         var testNameHtml = '';
2384         if (g_currentState.showChrome || tests.length > 1) {
2385             if (isLayoutTestResults()) {
2386                 var suite = lookupVirtualTestSuite(test);
2387                 var base = suite ? baseTest(test, suite) : test;
2388                 var tracURL = TEST_URL_BASE_PATH_TRAC + base;
2389                 testNameHtml += '<h2>' + linkHTMLToOpenWindow(tracURL, test) + '</h2>';
2390             } else
2391                 testNameHtml += '<h2>' + test + '</h2>';
2392         }
2393
2394         testsHTML.push(testNameHtml + htmlForIndividualTestOnAllBuildersWithResultsLinks(test));
2395     }
2396     return testsHTML.join('<hr>');
2397 }
2398
2399 function htmlForNavBar()
2400 {
2401     var extraHTML = '';
2402     var html = htmlForTestTypeSwitcher(false, extraHTML, isCrossBuilderView());
2403     html += '<div class=forms><form id=result-form ' +
2404         'onsubmit="setQueryParameter(\'result\', result.value);' +
2405         'return false;">Show all tests with result: ' +
2406         '<input name=result placeholder="e.g. CRASH" id=result-input>' +
2407         '</form><form id=tests-form ' +
2408         'onsubmit="setQueryParameter(\'tests\', tests.value);' +
2409         'return false;"><span>Show tests on all platforms: </span>' +
2410         '<input name=tests ' +
2411         'placeholder="Comma or space-separated list of tests or partial ' +
2412         'paths to show test results across all builders, e.g., ' +
2413         'foo/bar.html,foo/baz,domstorage" id=tests-input></form>' +
2414         '<span class=link onclick="showLegend()">Show legend [type ?]</span></div>';
2415     return html;
2416 }
2417
2418 function checkBoxToToggleState(key, text)
2419 {
2420     var stateEnabled = g_currentState[key];
2421     return '<label><input type=checkbox ' + (stateEnabled ? 'checked ' : '') + 'onclick="setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + text + '</label> ';
2422 }
2423
2424 function linkHTMLToToggleState(key, linkText)
2425 {
2426     var stateEnabled = g_currentState[key];
2427     return '<span class=link onclick="setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + (stateEnabled ? 'Hide' : 'Show') + ' ' + linkText + '</span>';
2428 }
2429
2430 function headerForTestTableHtml()
2431 {
2432     return '<h2 style="display:inline-block">Failing tests</h2>' +
2433         checkBoxToToggleState('showWontFixSkip', 'WONTFIX/SKIP') +
2434         checkBoxToToggleState('showCorrectExpectations', 'tests with correct expectations') +
2435         checkBoxToToggleState('showWrongExpectations', 'tests with wrong expectations') +
2436         checkBoxToToggleState('showFlaky', 'flaky') +
2437         checkBoxToToggleState('showSlow', 'slow');
2438 }
2439
2440 function generatePageForBuilder(builderName)
2441 {
2442     processTestRunsForBuilder(builderName);
2443
2444     var results = g_perBuilderFailures[builderName];
2445     sortTests(results, g_currentState.sortColumn, g_currentState.sortOrder);
2446
2447     var testsHTML = '';
2448     if (results.length) {
2449         var tableRowsHTML = '';
2450         for (var i = 0; i < results.length; i++)
2451             tableRowsHTML += htmlForSingleTestRow(results[i])
2452         testsHTML = htmlForTestTable(tableRowsHTML);
2453     } else {
2454         testsHTML = '<div>No tests found. ';
2455         if (isLayoutTestResults())
2456             testsHTML += 'Try showing tests with correct expectations.</div>';
2457         else
2458             testsHTML += 'This means no tests have failed!</div>';
2459     }
2460
2461     var html = htmlForNavBar();
2462
2463     if (isLayoutTestResults())
2464         html += htmlForTestsWithExpectationsButNoFailures(builderName) + headerForTestTableHtml();
2465
2466     html += '<br>' + testsHTML;
2467     appendHTML(html);
2468
2469     var ths = document.getElementsByTagName('th');
2470     for (var i = 0; i < ths.length; i++) {
2471         ths[i].addEventListener('click', changeSort, false);
2472         ths[i].className = "sortable";
2473     }
2474
2475     hideLoadingUI();
2476 }
2477
2478 var VALID_KEYS_FOR_CROSS_BUILDER_VIEW = {
2479     tests: 1,
2480     result: 1,
2481     showChrome: 1,
2482     showExpectations: 1,
2483     showLargeExpectations: 1,
2484     legacyExpectationsSemantics: 1,
2485     resultsHeight: 1,
2486     revision: 1
2487 };
2488
2489 function isInvalidKeyForCrossBuilderView(key)
2490 {
2491     return !(key in VALID_KEYS_FOR_CROSS_BUILDER_VIEW) && !(key in g_defaultCrossDashboardStateValues);
2492 }
2493
2494 // Sets the page state to regenerate the page.
2495 // @param {Object} params New or modified query parameters as key: value.
2496 function handleQueryParameterChange(params)
2497 {
2498     for (key in params) {
2499         if (key == 'tests') {
2500             // Entering cross-builder view, only keep valid keys for that view.
2501             for (var currentKey in g_currentState) {
2502               if (isInvalidKeyForCrossBuilderView(currentKey)) {
2503                 delete g_currentState[currentKey];
2504               }
2505             }
2506         } else if (isInvalidKeyForCrossBuilderView(key)) {
2507             delete g_currentState.tests;
2508             delete g_currentState.result;
2509         }
2510     }
2511
2512     return true;
2513 }
2514
2515 function hideLegend()
2516 {
2517     var legend = $('legend');
2518     if (legend)
2519         legend.parentNode.removeChild(legend);
2520 }
2521
2522 var g_fallbacksMap = {};
2523 g_fallbacksMap['WIN-XP'] = ['chromium-win-xp', 'chromium-win', 'chromium'];
2524 g_fallbacksMap['WIN-7'] = ['chromium-win', 'chromium'];
2525 g_fallbacksMap['MAC-SNOWLEOPARD'] = ['chromium-mac-snowleopard', 'chromium-mac', 'chromium'];
2526 g_fallbacksMap['MAC-LION'] = ['chromium-mac', 'chromium'];
2527 g_fallbacksMap['LINUX-32'] = ['chromium-linux-x86', 'chromium-linux', 'chromium-win', 'chromium'];
2528 g_fallbacksMap['LINUX-64'] = ['chromium-linux', 'chromium-win', 'chromium'];
2529
2530 function htmlForFallbackHelp(fallbacks)
2531 {
2532     return '<ol class=fallback-list><li>' + fallbacks.join('</li><li>') + '</li></ol>';
2533 }
2534
2535 function showLegend()
2536 {
2537     var legend = $('legend');
2538     if (!legend) {
2539         legend = document.createElement('div');
2540         legend.id = 'legend';
2541         document.body.appendChild(legend);
2542     }
2543
2544     var html = '<div id=legend-toggle onclick="hideLegend()">Hide ' +
2545         'legend [type esc]</div><div id=legend-contents>';
2546     for (var expectation in expectationsMap())
2547         html += '<div class=' + expectation + '>' + expectationsMap()[expectation] + '</div>';
2548
2549     html += '<div class=merge>WEBKIT MERGE</div>';
2550     if (isLayoutTestResults()) {
2551       html += '</div><br style="clear:both">' +
2552           '</div><h3>Test expectatons fallback order.</h3>';
2553
2554       for (var platform in g_fallbacksMap)
2555           html += '<div class=fallback-header>' + platform + '</div>' + htmlForFallbackHelp(g_fallbacksMap[platform]);
2556
2557       html += '<div>TIMES:</div>' +
2558           htmlForSlowTimes(MIN_SECONDS_FOR_SLOW_TEST) +
2559           '<div>DEBUG TIMES:</div>' +
2560           htmlForSlowTimes(MIN_SECONDS_FOR_SLOW_TEST_DEBUG);
2561     }
2562
2563     legend.innerHTML = html;
2564 }
2565
2566 function htmlForSlowTimes(minTime)
2567 {
2568     return '<ul><li>&lt;1 second == !SLOW</li><li>&gt;1 second && &lt;' +
2569         minTime + ' seconds == SLOW || !SLOW is fine</li><li>&gt;' +
2570         minTime + ' seconds == SLOW</li></ul>';
2571 }
2572
2573 function postHeightChangedMessage()
2574 {
2575     if (window == parent)
2576         return;
2577
2578     var root = document.documentElement;
2579     var height = root.offsetHeight;
2580     if (root.offsetWidth < root.scrollWidth) {
2581         // We have a horizontal scrollbar. Include it in the height.
2582         var dummyNode = document.createElement('div');
2583         dummyNode.style.overflow = 'scroll';
2584         document.body.appendChild(dummyNode);
2585         var scrollbarWidth = dummyNode.offsetHeight - dummyNode.clientHeight;
2586         document.body.removeChild(dummyNode);
2587         height += scrollbarWidth;
2588     }
2589     parent.postMessage({command: 'heightChanged', height: height}, '*')
2590 }
2591
2592 if (window != parent)
2593     window.addEventListener('blur', hidePopup);
2594
2595 document.addEventListener('keydown', function(e) {
2596     if (e.keyIdentifier == 'U+003F' || e.keyIdentifier == 'U+00BF') {
2597         // WebKit MAC retursn 3F. WebKit WIN returns BF. This is a bug!
2598         // ? key
2599         showLegend();
2600     } else if (e.keyIdentifier == 'U+001B') {
2601         // escape key
2602         hideLegend();
2603         hidePopup();
2604     }
2605 }, false);