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