• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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//////////////////////////////////////////////////////////////////////////////
32var FORWARD = 'forward';
33var BACKWARD = 'backward';
34var TEST_URL_BASE_PATH_FOR_BROWSING = 'http://src.chromium.org/viewvc/blink/trunk/LayoutTests/';
35var TEST_URL_BASE_PATH_FOR_XHR = 'http://src.chromium.org/blink/trunk/LayoutTests/';
36var TEST_RESULTS_BASE_PATH = 'https://storage.googleapis.com/chromium-layout-test-archives/';
37var GPU_RESULTS_BASE_PATH = 'http://chromium-browser-gpu-tests.commondatastorage.googleapis.com/runs/'
38
39var RELEASE_TIMEOUT = 6;
40var DEBUG_TIMEOUT = 12;
41var SLOW_MULTIPLIER = 5;
42
43// FIXME: Figure out how to make this not be hard-coded.
44// Probably just include in the results.json files and get it from there.
45var VIRTUAL_SUITES = {
46    'virtual/gpu/fast/canvas': 'fast/canvas',
47    'virtual/gpu/canvas/philip': 'canvas/philip',
48    'virtual/threaded/compositing/visibility': 'compositing/visibility',
49    'virtual/threaded/compositing/webgl': 'compositing/webgl',
50    'virtual/gpu/fast/hidpi': 'fast/hidpi',
51    'virtual/softwarecompositing': 'compositing',
52    'virtual/deferred/fast/images': 'fast/images',
53    'virtual/gpu/compositedscrolling/overflow': 'compositing/overflow',
54    'virtual/gpu/compositedscrolling/scrollbars': 'scrollbars',
55};
56
57var ACTUAL_RESULT_SUFFIXES = ['expected.txt', 'expected.png', 'actual.txt', 'actual.png', 'diff.txt', 'diff.png', 'wdiff.html', 'crash-log.txt'];
58
59var EXPECTATIONS_ORDER = ACTUAL_RESULT_SUFFIXES.filter(function(suffix) {
60    return !string.endsWith(suffix, 'png');
61}).map(function(suffix) {
62    return suffix.split('.')[0]
63});
64
65var resourceLoader;
66
67function generatePage(historyInstance)
68{
69    if (historyInstance.crossDashboardState.useTestData)
70        return;
71
72    document.body.innerHTML = '<div id="loading-ui">LOADING...</div>';
73    resourceLoader.showErrors();
74
75    // tests expands to all tests that match the CSV list.
76    // result expands to all tests that ever have the given result
77    if (historyInstance.dashboardSpecificState.tests || historyInstance.dashboardSpecificState.result)
78        generatePageForIndividualTests(individualTests());
79    else
80        generatePageForBuilder(historyInstance.dashboardSpecificState.builder || currentBuilderGroup().defaultBuilder());
81
82    for (var builder in currentBuilders())
83        processTestResultsForBuilderAsync(builder);
84
85    postHeightChangedMessage();
86}
87
88function handleValidHashParameter(historyInstance, key, value)
89{
90    switch(key) {
91    case 'result':
92    case 'tests':
93        history.validateParameter(historyInstance.dashboardSpecificState, key, value,
94            function() {
95                return string.isValidName(value);
96            });
97        return true;
98
99    case 'builder':
100        history.validateParameter(historyInstance.dashboardSpecificState, key, value,
101            function() {
102                return value in currentBuilders();
103            });
104
105        return true;
106
107    case 'sortColumn':
108        history.validateParameter(historyInstance.dashboardSpecificState, key, value,
109            function() {
110                // Get all possible headers since the actual used set of headers
111                // depends on the values in historyInstance.dashboardSpecificState, which are currently being set.
112                var getAllTableHeaders = true;
113                var headers = tableHeaders(getAllTableHeaders);
114                for (var i = 0; i < headers.length; i++) {
115                    if (value == sortColumnFromTableHeader(headers[i]))
116                        return true;
117                }
118                return value == 'test' || value == 'builder';
119            });
120        return true;
121
122    case 'sortOrder':
123        history.validateParameter(historyInstance.dashboardSpecificState, key, value,
124            function() {
125                return value == FORWARD || value == BACKWARD;
126            });
127        return true;
128
129    case 'resultsHeight':
130    case 'revision':
131        history.validateParameter(historyInstance.dashboardSpecificState, key, Number(value),
132            function() {
133                return value.match(/^\d+$/);
134            });
135        return true;
136
137    case 'showChrome':
138    case 'showExpectations':
139    case 'showFlaky':
140    case 'showLargeExpectations':
141    case 'showNonFlaky':
142    case 'showSlow':
143    case 'showSkip':
144    case 'showUnexpectedPasses':
145    case 'showWontFix':
146        historyInstance.dashboardSpecificState[key] = value == 'true';
147        return true;
148
149    default:
150        return false;
151    }
152}
153
154// @param {Object} params New or modified query parameters as key: value.
155function handleQueryParameterChange(historyInstance, params)
156{
157    for (key in params) {
158        if (key == 'tests') {
159            // Entering cross-builder view, only keep valid keys for that view.
160            for (var currentKey in historyInstance.dashboardSpecificState) {
161              if (isInvalidKeyForCrossBuilderView(currentKey)) {
162                delete historyInstance.dashboardSpecificState[currentKey];
163              }
164            }
165        } else if (isInvalidKeyForCrossBuilderView(key)) {
166            delete historyInstance.dashboardSpecificState.tests;
167            delete historyInstance.dashboardSpecificState.result;
168        }
169    }
170
171    return true;
172}
173
174var defaultDashboardSpecificStateValues = {
175    sortOrder: BACKWARD,
176    sortColumn: 'flakiness',
177    showExpectations: false,
178    // FIXME: Show flaky tests by default if you have a builder picked.
179    // Ideally, we'd fix the dashboard to not pick a default builder and have
180    // you pick one. In the interim, this is a good way to make the default
181    // page load faster since we don't need to generate/layout a large table.
182    showFlaky: false,
183    showLargeExpectations: false,
184    showChrome: true,
185    showWontFix: false,
186    showNonFlaky: false,
187    showSkip: false,
188    showUnexpectedPasses: false,
189    resultsHeight: 300,
190    revision: null,
191    tests: '',
192    result: '',
193    builder: null
194};
195
196var DB_SPECIFIC_INVALIDATING_PARAMETERS = {
197    'tests' : 'builder',
198    'testType': 'builder',
199    'group': 'builder'
200};
201
202var flakinessConfig = {
203    defaultStateValues: defaultDashboardSpecificStateValues,
204    generatePage: generatePage,
205    handleValidHashParameter: handleValidHashParameter,
206    handleQueryParameterChange: handleQueryParameterChange,
207    invalidatingHashParameters: DB_SPECIFIC_INVALIDATING_PARAMETERS
208};
209
210// FIXME(jparent): Eventually remove all usage of global history object.
211var g_history = new history.History(flakinessConfig);
212g_history.parseCrossDashboardParameters();
213
214//////////////////////////////////////////////////////////////////////////////
215// GLOBALS
216//////////////////////////////////////////////////////////////////////////////
217
218var g_perBuilderFailures = {};
219// Maps test path to an array of {builder, testResults} objects.
220var g_testToResultsMap = {};
221
222function createResultsObjectForTest(test, builder)
223{
224    return {
225        test: test,
226        builder: builder,
227        // HTML for display of the results in the flakiness column
228        html: '',
229        flipCount: 0,
230        slowestTime: 0,
231        isFlaky: false,
232        bugs: [],
233        expectations : '',
234        rawResults: '',
235        // List of all the results the test actually has.
236        actualResults: []
237    };
238}
239
240var TestTrie = function(builders, resultsByBuilder)
241{
242    this._trie = {};
243
244    for (var builder in builders) {
245        if (!resultsByBuilder[builder]) {
246            console.warn("No results for builder: ", builder)
247            continue;
248        }
249        var testsForBuilder = resultsByBuilder[builder].tests;
250        for (var test in testsForBuilder)
251            this._addTest(test.split('/'), this._trie);
252    }
253}
254
255TestTrie.prototype.forEach = function(callback, startingTriePath)
256{
257    var testsTrie = this._trie;
258    if (startingTriePath) {
259        var splitPath = startingTriePath.split('/');
260        while (splitPath.length && testsTrie)
261            testsTrie = testsTrie[splitPath.shift()];
262    }
263
264    if (!testsTrie)
265        return;
266
267    function traverse(trie, triePath) {
268        if (trie == true)
269            callback(triePath);
270        else {
271            for (var member in trie)
272                traverse(trie[member], triePath ? triePath + '/' + member : member);
273        }
274    }
275    traverse(testsTrie, startingTriePath);
276}
277
278TestTrie.prototype._addTest = function(test, trie)
279{
280    var rootComponent = test.shift();
281    if (!test.length) {
282        if (!trie[rootComponent])
283            trie[rootComponent] = true;
284        return;
285    }
286
287    if (!trie[rootComponent] || trie[rootComponent] == true)
288        trie[rootComponent] = {};
289    this._addTest(test, trie[rootComponent]);
290}
291
292// Map of all tests to true values. This is just so we can have the list of
293// all tests across all the builders.
294var g_allTestsTrie;
295
296function getAllTestsTrie()
297{
298    if (!g_allTestsTrie)
299        g_allTestsTrie = new TestTrie(currentBuilders(), g_resultsByBuilder);
300
301    return g_allTestsTrie;
302}
303
304// Returns an array of tests to be displayed in the individual tests view.
305// Note that a directory can be listed as a test, so we expand that into all
306// tests in the directory.
307function individualTests()
308{
309    if (g_history.dashboardSpecificState.result)
310        return allTestsWithResult(g_history.dashboardSpecificState.result);
311
312    if (!g_history.dashboardSpecificState.tests)
313        return [];
314
315    return individualTestsForSubstringList();
316}
317
318function splitTestList()
319{
320    // Convert windows slashes to unix slashes and spaces/newlines to commas.
321    var tests = g_history.dashboardSpecificState.tests.replace(/\\/g, '/').replace('\n', ' ').replace(/\s+/g, ',');
322    return tests.split(',');
323}
324
325function individualTestsForSubstringList()
326{
327    var testList = splitTestList();
328    // If listing a lot of tests, assume you've passed in an explicit list of tests
329    // instead of patterns to match against. The matching code below is super slow.
330    //
331    // Also, when showChrome is false, we're embedding the dashboard elsewhere and
332    // an explicit test list is passed in. In that case, we don't want
333    // a search for compositing/foo.html to also show virtual/softwarecompositing/foo.html.
334    if (testList.length > 10 || !g_history.dashboardSpecificState.showChrome)
335        return testList;
336
337    // Put the tests into an object first and then move them into an array
338    // as a way of deduping.
339    var testsMap = {};
340    for (var i = 0; i < testList.length; i++) {
341        var path = testList[i];
342
343        // Ignore whitespace entries as they'd match every test.
344        if (path.match(/^\s*$/))
345            continue;
346
347        var hasAnyMatches = false;
348        getAllTestsTrie().forEach(function(triePath) {
349            if (string.caseInsensitiveContains(triePath, path)) {
350                testsMap[triePath] = 1;
351                hasAnyMatches = true;
352            }
353        });
354
355        // If a path doesn't match any tests, then assume it's a full path
356        // to a test that passes on all builders.
357        if (!hasAnyMatches)
358            testsMap[path] = 1;
359    }
360
361    var testsArray = [];
362    for (var test in testsMap)
363        testsArray.push(test);
364
365    return testsArray;
366}
367
368function allTestsWithResult(result)
369{
370    processTestRunsForAllBuilders();
371    var retVal = [];
372
373    getAllTestsTrie().forEach(function(triePath) {
374        for (var i = 0; i < g_testToResultsMap[triePath].length; i++) {
375            if (g_testToResultsMap[triePath][i].actualResults.indexOf(result.toUpperCase()) != -1) {
376                retVal.push(triePath);
377                break;
378            }
379        }
380    });
381
382    return retVal;
383}
384
385function processTestResultsForBuilderAsync(builder)
386{
387    setTimeout(function() { processTestRunsForBuilder(builder); }, 0);
388}
389
390function processTestRunsForAllBuilders()
391{
392    for (var builder in currentBuilders())
393        processTestRunsForBuilder(builder);
394}
395
396function processTestRunsForBuilder(builderName)
397{
398    if (g_perBuilderFailures[builderName])
399      return;
400
401    if (!g_resultsByBuilder[builderName]) {
402        console.error('No tests found for ' + builderName);
403        g_perBuilderFailures[builderName] = [];
404        return;
405    }
406
407    var failures = [];
408    var allTestsForThisBuilder = g_resultsByBuilder[builderName].tests;
409
410    for (var test in allTestsForThisBuilder) {
411        var resultsForTest = createResultsObjectForTest(test, builderName);
412
413        var rawTest = g_resultsByBuilder[builderName].tests[test];
414        resultsForTest.rawTimes = rawTest.times;
415        var rawResults = rawTest.results;
416        resultsForTest.rawResults = rawResults;
417
418        if (rawTest.expected)
419            resultsForTest.expectations = rawTest.expected;
420
421        if (rawTest.bugs)
422            resultsForTest.bugs = rawTest.bugs;
423
424        var failureMap = g_resultsByBuilder[builderName][results.FAILURE_MAP];
425        // FIXME: Switch to resultsByBuild
426        var times = resultsForTest.rawTimes;
427        var numTimesSeen = 0;
428        var numResultsSeen = 0;
429        var resultsIndex = 0;
430        var resultsMap = {}
431
432        for (var i = 0; i < times.length; i++) {
433            numTimesSeen += times[i][results.RLE.LENGTH];
434
435            while (rawResults[resultsIndex] && numTimesSeen > (numResultsSeen + rawResults[resultsIndex][results.RLE.LENGTH])) {
436                numResultsSeen += rawResults[resultsIndex][results.RLE.LENGTH];
437                resultsIndex++;
438            }
439
440            if (rawResults && rawResults[resultsIndex]) {
441                var result = rawResults[resultsIndex][results.RLE.VALUE];
442                resultsMap[failureMap[result]] = true;
443            }
444
445            resultsForTest.slowestTime = Math.max(resultsForTest.slowestTime, times[i][results.RLE.VALUE]);
446        }
447
448        resultsForTest.actualResults = Object.keys(resultsMap);
449
450        results.determineFlakiness(failureMap, rawResults, resultsForTest);
451        failures.push(resultsForTest);
452
453        if (!g_testToResultsMap[test])
454            g_testToResultsMap[test] = [];
455        g_testToResultsMap[test].push(resultsForTest);
456    }
457
458    g_perBuilderFailures[builderName] = failures;
459}
460
461function linkHTMLToOpenWindow(url, text)
462{
463    return '<a href="' + url + '" target="_blank">' + text + '</a>';
464}
465
466// Returns whether the result for index'th result for testName on builder was
467// a failure.
468function isFailure(builder, testName, index)
469{
470    var currentIndex = 0;
471    var rawResults = g_resultsByBuilder[builder].tests[testName].results;
472    var failureMap = g_resultsByBuilder[builder][results.FAILURE_MAP];
473    for (var i = 0; i < rawResults.length; i++) {
474        currentIndex += rawResults[i][results.RLE.LENGTH];
475        if (currentIndex > index)
476            return results.isFailingResult(failureMap, rawResults[i][results.RLE.VALUE]);
477    }
478    console.error('Index exceeds number of results: ' + index);
479}
480
481// Returns an array of indexes for all builds where this test failed.
482function indexesForFailures(builder, testName)
483{
484    var rawResults = g_resultsByBuilder[builder].tests[testName].results;
485    var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
486    var failureMap = g_resultsByBuilder[builder][results.FAILURE_MAP];
487    var index = 0;
488    var failures = [];
489    for (var i = 0; i < rawResults.length; i++) {
490        var numResults = rawResults[i][results.RLE.LENGTH];
491        if (results.isFailingResult(failureMap, rawResults[i][results.RLE.VALUE])) {
492            for (var j = 0; j < numResults; j++)
493                failures.push(index + j);
494        }
495        index += numResults;
496    }
497    return failures;
498}
499
500// Returns the path to the failure log for this non-webkit test.
501function pathToFailureLog(testName)
502{
503    return '/steps/' + g_history.crossDashboardState.testType + '/logs/' + testName.split('.')[1]
504}
505
506function showPopupForBuild(e, builder, index, opt_testName)
507{
508    var html = '';
509
510    var time = g_resultsByBuilder[builder].secondsSinceEpoch[index];
511    if (time) {
512        var date = new Date(time * 1000);
513        html += date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
514    }
515
516    var buildNumber = g_resultsByBuilder[builder].buildNumbers[index];
517    var master = builders.master(builder);
518    var buildBasePath = master.logPath(builder, buildNumber);
519
520    html += '<ul><li>' + linkHTMLToOpenWindow(buildBasePath, 'Build log');
521
522    if (g_resultsByBuilder[builder][results.BLINK_REVISIONS])
523        html += '</li><li>Blink: ' + ui.html.blinkRevisionLink(g_resultsByBuilder[builder], index) + '</li>';
524
525    html += '</li><li>Chromium: ' + ui.html.chromiumRevisionLink(g_resultsByBuilder[builder], index) + '</li>';
526
527    var chromeRevision = g_resultsByBuilder[builder].chromeRevision[index];
528    if (chromeRevision && g_history.isLayoutTestResults()) {
529        html += '<li><a href="' + TEST_RESULTS_BASE_PATH + currentBuilders()[builder] +
530            '/' + buildNumber + '/layout-test-results.zip">layout-test-results.zip</a></li>';
531    }
532
533    if (!g_history.isLayoutTestResults() && opt_testName && isFailure(builder, opt_testName, index))
534        html += '<li>' + linkHTMLToOpenWindow(buildBasePath + pathToFailureLog(opt_testName), 'Failure log') + '</li>';
535
536    html += '</ul>';
537    ui.popup.show(e.target, html);
538}
539
540function classNameForFailureString(failure)
541{
542    return failure.replace(/(\+|\ )/, '');
543}
544
545function htmlForTestResults(test)
546{
547    var html = '';
548    var testResults = test.rawResults.concat();
549    var times = test.rawTimes.concat();
550    var builder = test.builder;
551    var master = builders.master(builder);
552    var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
553
554    var indexToReplaceCurrentResult = -1;
555    var indexToReplaceCurrentTime = -1;
556    for (var i = 0; i < buildNumbers.length; i++) {
557        var currentResultArray, currentTimeArray, innerHTML, resultString;
558
559        if (i > indexToReplaceCurrentResult) {
560            currentResultArray = testResults.shift();
561            if (currentResultArray) {
562                resultString = g_resultsByBuilder[builder][results.FAILURE_MAP][currentResultArray[results.RLE.VALUE]];
563                indexToReplaceCurrentResult += currentResultArray[results.RLE.LENGTH];
564            } else {
565                resultString = results.NO_DATA;
566                indexToReplaceCurrentResult += buildNumbers.length;
567            }
568        }
569
570        if (i > indexToReplaceCurrentTime) {
571            currentTimeArray = times.shift();
572            var currentTime = 0;
573            if (currentResultArray) {
574              currentTime = currentTimeArray[results.RLE.VALUE];
575              indexToReplaceCurrentTime += currentTimeArray[results.RLE.LENGTH];
576            } else
577              indexToReplaceCurrentTime += buildNumbers.length;
578
579            innerHTML = currentTime || '&nbsp;';
580        }
581
582        html += '<td title="' + resultString + '. Click for more info." class="results ' + classNameForFailureString(resultString) +
583          '" onclick=\'showPopupForBuild(event, "' + builder + '",' + i + ',"' + test.test + '")\'>' + innerHTML;
584    }
585    return html;
586}
587
588function shouldShowTest(testResult)
589{
590    if (!g_history.isLayoutTestResults())
591        return true;
592
593    if (testResult.expectations == 'WONTFIX')
594        return g_history.dashboardSpecificState.showWontFix;
595
596    if (testResult.expectations == results.SKIP)
597        return g_history.dashboardSpecificState.showSkip;
598
599    if (testResult.isFlaky)
600        return g_history.dashboardSpecificState.showFlaky;
601
602    return g_history.dashboardSpecificState.showNonFlaky;
603}
604
605function createBugHTML(test)
606{
607    var symptom = test.isFlaky ? 'flaky' : 'failing';
608    var title = encodeURIComponent('Layout Test ' + test.test + ' is ' + symptom);
609    var description = encodeURIComponent('The following layout test is ' + symptom + ' on ' +
610        '[insert platform]\n\n' + test.test + '\n\nProbable cause:\n\n' +
611        '[insert probable cause]');
612
613    url = 'https://code.google.com/p/chromium/issues/entry?template=Layout%20Test%20Failure&summary=' + title + '&comment=' + description;
614    return '<a href="' + url + '">File new bug</a>';
615}
616
617function isCrossBuilderView()
618{
619    return g_history.dashboardSpecificState.tests || g_history.dashboardSpecificState.result;
620}
621
622function tableHeaders(opt_getAll)
623{
624    var headers = [];
625    if (isCrossBuilderView() || opt_getAll)
626        headers.push('builder');
627
628    if (!isCrossBuilderView() || opt_getAll)
629        headers.push('test');
630
631    if (g_history.isLayoutTestResults() || opt_getAll)
632        headers.push('bugs', 'expectations');
633
634    headers.push('slowest run', 'flakiness (numbers are runtimes in seconds)');
635    return headers;
636}
637
638function linkifyBugs(bugs)
639{
640    var html = '';
641    bugs.forEach(function(bug) {
642        var bugHtml;
643        if (string.startsWith(bug, 'Bug('))
644            bugHtml = bug;
645        else
646            bugHtml = '<a href="http://' + bug + '">' + bug + '</a>';
647        html += '<div>' + bugHtml + '</div>'
648    });
649    return html;
650}
651
652function htmlForSingleTestRow(test, showBuilderNames)
653{
654    var headers = tableHeaders();
655    var html = '';
656    for (var i = 0; i < headers.length; i++) {
657        var header = headers[i];
658        if (string.startsWith(header, 'test') || string.startsWith(header, 'builder')) {
659            var testCellClassName = 'test-link' + (showBuilderNames ? ' builder-name' : '');
660            var testCellHTML = showBuilderNames ? test.builder : '<span class="link" onclick="g_history.setQueryParameter(\'tests\',\'' + test.test +'\');">' + test.test + '</span>';
661            html += '<tr><td class="' + testCellClassName + '">' + testCellHTML;
662        } else if (string.startsWith(header, 'bugs'))
663            // FIXME: linkify bugs.
664            html += '<td class=options-container>' + (linkifyBugs(test.bugs) || createBugHTML(test));
665        else if (string.startsWith(header, 'expectations'))
666            html += '<td class=options-container>' + test.expectations;
667        else if (string.startsWith(header, 'slowest'))
668            html += '<td>' + (test.slowestTime ? test.slowestTime + 's' : '');
669        else if (string.startsWith(header, 'flakiness'))
670            html += htmlForTestResults(test);
671    }
672    return html;
673}
674
675function sortColumnFromTableHeader(headerText)
676{
677    return headerText.split(' ', 1)[0];
678}
679
680function htmlForTableColumnHeader(headerName, opt_fillColSpan)
681{
682    // Use the first word of the header title as the sortkey
683    var thisSortValue = sortColumnFromTableHeader(headerName);
684    var arrowHTML = thisSortValue == g_history.dashboardSpecificState.sortColumn ?
685        '<span class=' + g_history.dashboardSpecificState.sortOrder + '>' + (g_history.dashboardSpecificState.sortOrder == FORWARD ? '&uarr;' : '&darr;' ) + '</span>' : '';
686    return '<th sortValue=' + thisSortValue +
687        // Extend last th through all the rest of the columns.
688        (opt_fillColSpan ? ' colspan=10000' : '') +
689        // Extra span here is so flex boxing actually centers.
690        // There's probably a better way to do this with CSS only though.
691        '><div class=table-header-content><span></span>' + arrowHTML +
692        '<span class=header-text>' + headerName + '</span>' + arrowHTML + '</div></th>';
693}
694
695function htmlForTestTable(rowsHTML, opt_excludeHeaders)
696{
697    var html = '<table class=test-table>';
698    if (!opt_excludeHeaders) {
699        html += '<thead><tr>';
700        var headers = tableHeaders();
701        for (var i = 0; i < headers.length; i++)
702            html += htmlForTableColumnHeader(headers[i], i == headers.length - 1);
703        html += '</tr></thead>';
704    }
705    return html + '<tbody>' + rowsHTML + '</tbody></table>';
706}
707
708function appendHTML(html)
709{
710    // InnerHTML to a div that's not in the document. This is
711    // ~300ms faster in Safari 4 and Chrome 4 on mac.
712    var div = document.createElement('div');
713    div.innerHTML = html;
714    document.body.appendChild(div);
715    postHeightChangedMessage();
716}
717
718function alphanumericCompare(column, reverse)
719{
720    return reversibleCompareFunction(function(a, b) {
721        // Put null entries at the bottom
722        var a = a[column] ? String(a[column]) : 'z';
723        var b = b[column] ? String(b[column]) : 'z';
724
725        if (a < b)
726            return -1;
727        else if (a == b)
728            return 0;
729        else
730            return 1;
731    }, reverse);
732}
733
734function numericSort(column, reverse)
735{
736    return reversibleCompareFunction(function(a, b) {
737        a = parseFloat(a[column]);
738        b = parseFloat(b[column]);
739        return a - b;
740    }, reverse);
741}
742
743function reversibleCompareFunction(compare, reverse)
744{
745    return function(a, b) {
746        return compare(reverse ? b : a, reverse ? a : b);
747    };
748}
749
750function changeSort(e)
751{
752    var target = e.currentTarget;
753    e.preventDefault();
754
755    var sortValue = target.getAttribute('sortValue');
756    while (target && target.tagName != 'TABLE')
757        target = target.parentNode;
758
759    var sort = 'sortColumn';
760    var orderKey = 'sortOrder';
761    if (sortValue == g_history.dashboardSpecificState[sort] && g_history.dashboardSpecificState[orderKey] == FORWARD)
762        order = BACKWARD;
763    else
764        order = FORWARD;
765
766    g_history.setQueryParameter(sort, sortValue, orderKey, order);
767}
768
769function sortTests(tests, column, order)
770{
771    var resultsProperty, sortFunctionGetter;
772    if (column == 'flakiness') {
773        sortFunctionGetter = numericSort;
774        resultsProperty = 'flipCount';
775    } else if (column == 'slowest') {
776        sortFunctionGetter = numericSort;
777        resultsProperty = 'slowestTime';
778    } else {
779        sortFunctionGetter = alphanumericCompare;
780        resultsProperty = column;
781    }
782
783    tests.sort(sortFunctionGetter(resultsProperty, order == BACKWARD));
784}
785
786function htmlForIndividualTestOnAllBuilders(test)
787{
788    processTestRunsForAllBuilders();
789
790    var testResults = g_testToResultsMap[test];
791    if (!testResults)
792        return '<div class="not-found">Test not found. Either it does not exist, is skipped or passes on all recorded runs.</div>';
793
794    var html = '';
795    var shownBuilders = [];
796    for (var j = 0; j < testResults.length; j++) {
797        shownBuilders.push(testResults[j].builder);
798        var showBuilderNames = true;
799        html += htmlForSingleTestRow(testResults[j], showBuilderNames);
800    }
801
802    var skippedBuilders = []
803    for (builder in currentBuilders()) {
804        if (shownBuilders.indexOf(builder) == -1)
805            skippedBuilders.push(builder);
806    }
807
808    var skippedBuildersHtml = '';
809    if (skippedBuilders.length) {
810        skippedBuildersHtml = '<div>The following builders either don\'t run this test (e.g. it\'s skipped) or all recorded runs passed:</div>' +
811            '<div class=skipped-builder-list><div class=skipped-builder>' + skippedBuilders.join('</div><div class=skipped-builder>') + '</div></div>';
812    }
813
814    return htmlForTestTable(html) + skippedBuildersHtml;
815}
816
817function htmlForIndividualTestOnAllBuildersWithResultsLinks(test)
818{
819    processTestRunsForAllBuilders();
820
821    var testResults = g_testToResultsMap[test];
822    var html = '';
823    html += htmlForIndividualTestOnAllBuilders(test);
824
825    html += '<div class=expectations test=' + test + '><div>' +
826        linkHTMLToToggleState('showExpectations', 'results')
827
828    if (g_history.isLayoutTestResults() || g_history.isGPUTestResults()) {
829        if (g_history.isLayoutTestResults())
830            html += ' | ' + linkHTMLToToggleState('showLargeExpectations', 'large thumbnails');
831            html += ' | <b>Only shows actual results/diffs from the most recent *failure* on each bot.</b>';
832    } else {
833      html += ' | <span>Results height:<input ' +
834          'onchange="g_history.setQueryParameter(\'resultsHeight\',this.value)" value="' +
835          g_history.dashboardSpecificState.resultsHeight + '" style="width:2.5em">px</span>';
836    }
837    html += '</div></div>';
838    return html;
839}
840
841function maybeAddPngChecksum(expectationDiv, pngUrl)
842{
843    // pngUrl gets served from the browser cache since we just loaded it in an
844    // <img> tag.
845    loader.request(pngUrl,
846        function(xhr) {
847            // Convert the first 2k of the response to a byte string.
848            var bytes = xhr.responseText.substring(0, 2048);
849            for (var position = 0; position < bytes.length; ++position)
850                bytes[position] = bytes[position] & 0xff;
851
852            // Look for the comment.
853            var commentKey = 'tEXtchecksum\x00';
854            var checksumPosition = bytes.indexOf(commentKey);
855            if (checksumPosition == -1)
856                return;
857
858            var checksum = bytes.substring(checksumPosition + commentKey.length, checksumPosition + commentKey.length + 32);
859            var checksumContainer = document.createElement('span');
860            checksumContainer.innerText = 'Embedded checksum: ' + checksum;
861            checksumContainer.setAttribute('class', 'pngchecksum');
862            expectationDiv.parentNode.appendChild(checksumContainer);
863        },
864        function(xhr) {},
865        true);
866}
867
868function getOrCreate(className, parent)
869{
870    var element = parent.querySelector('.' + className);
871    if (!element) {
872        element = document.createElement('div');
873        element.className = className;
874        parent.appendChild(element);
875    }
876    return element;
877}
878
879function handleExpectationsItemLoad(title, item, itemType, parent)
880{
881    item.className = 'expectation';
882    if (g_history.dashboardSpecificState.showLargeExpectations)
883        item.className += ' large';
884
885    var titleContainer = document.createElement('h3');
886    titleContainer.className = 'expectations-title';
887    titleContainer.textContent = title;
888
889    var itemContainer = document.createElement('span');
890    itemContainer.appendChild(titleContainer);
891    itemContainer.className = 'expectations-item ' + title;
892    itemContainer.appendChild(item);
893
894    // Separate text and image results into separate divs..
895    var typeContainer = getOrCreate(itemType, parent);
896
897    // Insert results in a consistent order.
898    var index = EXPECTATIONS_ORDER.indexOf(title);
899    while (index < EXPECTATIONS_ORDER.length) {
900        index++;
901        var elementAfter = typeContainer.querySelector('.' + EXPECTATIONS_ORDER[index]);
902        if (elementAfter) {
903            typeContainer.insertBefore(itemContainer, elementAfter);
904            break;
905        }
906    }
907    if (!itemContainer.parentNode)
908        typeContainer.appendChild(itemContainer);
909
910    handleFinishedLoadingExpectations(parent);
911}
912
913function addExpectationItem(expectationsContainers, parentContainer, url, opt_builder)
914{
915    // Group expectations by builder, putting test and reference files first.
916    var builder = opt_builder || "Test and reference files";
917    var container = expectationsContainers[builder];
918
919    if (!container) {
920        container = document.createElement('div');
921        container.className = 'expectations-container';
922        container.setAttribute('data-builder', builder);
923        parentContainer.appendChild(container);
924        expectationsContainers[builder] = container;
925    }
926
927    var numUnloaded = container.getAttribute('data-unloaded') || 0;
928    container.setAttribute('data-unloaded', ++numUnloaded);
929
930    var isImage = url.match(/\.png$/);
931
932    var appendExpectationsItem = function(item) {
933        var itemType = isImage ? 'image' : 'text';
934        handleExpectationsItemLoad(expectationsTitle(url), item, itemType, container);
935    };
936
937    var handleLoadError = function() {
938        handleFinishedLoadingExpectations(container);
939    };
940
941    if (isImage) {
942        var dummyNode = document.createElement('img');
943        dummyNode.onload = function() {
944            var item = dummyNode;
945            maybeAddPngChecksum(item, url);
946            appendExpectationsItem(item);
947        }
948        dummyNode.onerror = handleLoadError;
949        dummyNode.src = url;
950    } else {
951        loader.request(url,
952            function(xhr) {
953                var item = document.createElement('pre');
954                if (string.endsWith(url, '-wdiff.html'))
955                    item.innerHTML = xhr.responseText;
956                else
957                    item.textContent = xhr.responseText;
958                appendExpectationsItem(item);
959            },
960            handleLoadError);
961    }
962}
963
964function handleFinishedLoadingExpectations(container)
965{
966    var numUnloaded = container.getAttribute('data-unloaded') - 1;
967    container.setAttribute('data-unloaded', numUnloaded);
968    if (numUnloaded)
969        return;
970
971    if (!container.firstChild) {
972        container.remove();
973        return;
974    }
975
976    var builder = container.getAttribute('data-builder');
977    if (!builder)
978        return;
979
980    var header = document.createElement('h2');
981    header.textContent = builder;
982    container.insertBefore(header, container.firstChild);
983}
984
985function expectationsTitle(url)
986{
987    var matchingSuffixes = ACTUAL_RESULT_SUFFIXES.filter(function(suffix) {
988        return string.endsWith(url, suffix);
989    });
990
991    if (matchingSuffixes.length)
992        return matchingSuffixes[0].split('.')[0];
993
994    var parts = url.split('/');
995    return parts[parts.length - 1];
996}
997
998function loadExpectations(expectationsContainer)
999{
1000    var test = expectationsContainer.getAttribute('test');
1001    if (g_history.isLayoutTestResults())
1002        loadExpectationsLayoutTests(test, expectationsContainer);
1003    else {
1004        var testResults = g_testToResultsMap[test];
1005        for (var i = 0; i < testResults.length; i++)
1006            if (g_history.isGPUTestResults())
1007                loadGPUResultsForBuilder(testResults[i].builder, test, expectationsContainer);
1008            else
1009                loadNonWebKitResultsForBuilder(testResults[i].builder, test, expectationsContainer);
1010    }
1011}
1012
1013function gpuResultsPath(chromeRevision, builder)
1014{
1015  return chromeRevision + '_' + builder.replace(/[^A-Za-z0-9]+/g, '_');
1016}
1017
1018function loadGPUResultsForBuilder(builder, test, expectationsContainer)
1019{
1020    var container = document.createElement('div');
1021    container.className = 'expectations-container';
1022    container.innerHTML = '<div><b>' + builder + '</b></div>';
1023    expectationsContainer.appendChild(container);
1024
1025    var failureIndex = indexesForFailures(builder, test)[0];
1026
1027    var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndex];
1028    var pathToLog = builders.master(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
1029
1030    var chromeRevision = g_resultsByBuilder[builder].chromeRevision[failureIndex];
1031    var resultsUrl = GPU_RESULTS_BASE_PATH + gpuResultsPath(chromeRevision, builder);
1032    var filename = test.split(/\./)[1] + '.png';
1033
1034    appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
1035    appendNonWebKitResults(container, resultsUrl + '/gen/' + filename, 'gpu-test-results', 'Generated');
1036    appendNonWebKitResults(container, resultsUrl + '/ref/' + filename, 'gpu-test-results', 'Reference');
1037    appendNonWebKitResults(container, resultsUrl + '/diff/' + filename, 'gpu-test-results', 'Diff');
1038}
1039
1040function loadNonWebKitResultsForBuilder(builder, test, expectationsContainer)
1041{
1042    var failureIndexes = indexesForFailures(builder, test);
1043    var container = document.createElement('div');
1044    container.innerHTML = '<div><b>' + builder + '</b></div>';
1045    expectationsContainer.appendChild(container);
1046    for (var i = 0; i < failureIndexes.length; i++) {
1047        // FIXME: This doesn't seem to work anymore. Did the paths change?
1048        // Once that's resolved, see if we need to try each gtest modifier prefix as well.
1049        var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndexes[i]];
1050        var pathToLog = builders.master(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
1051        appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
1052    }
1053}
1054
1055function appendNonWebKitResults(container, url, itemClassName, opt_title)
1056{
1057    // Use a script tag to detect whether the URL 404s.
1058    // Need to use a script tag since the URL is cross-domain.
1059    var dummyNode = document.createElement('script');
1060    dummyNode.src = url;
1061
1062    dummyNode.onload = function() {
1063        var item = document.createElement('iframe');
1064        item.src = dummyNode.src;
1065        item.className = itemClassName;
1066        item.style.height = g_history.dashboardSpecificState.resultsHeight + 'px';
1067
1068        if (opt_title) {
1069            var childContainer = document.createElement('div');
1070            childContainer.style.display = 'inline-block';
1071            var title = document.createElement('div');
1072            title.textContent = opt_title;
1073            childContainer.appendChild(title);
1074            childContainer.appendChild(item);
1075            container.replaceChild(childContainer, dummyNode);
1076        } else
1077            container.replaceChild(item, dummyNode);
1078    }
1079    dummyNode.onerror = function() {
1080        container.removeChild(dummyNode);
1081    }
1082
1083    container.appendChild(dummyNode);
1084}
1085
1086function lookupVirtualTestSuite(test) {
1087    for (var suite in VIRTUAL_SUITES) {
1088        if (test.indexOf(suite) != -1)
1089            return suite;
1090    }
1091    return '';
1092}
1093
1094function baseTest(test, suite) {
1095    base = VIRTUAL_SUITES[suite];
1096    return base ? test.replace(suite, base) : test;
1097}
1098
1099function loadTestAndReferenceFiles(expectationsContainers, expectationsContainer, test) {
1100    var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
1101    var reftest_html_file = testWithoutSuffix + "-expected.html";
1102    var reftest_mismatch_html_file = testWithoutSuffix + "-expected-mismatch.html";
1103
1104    var suite = lookupVirtualTestSuite(test);
1105    if (suite) {
1106        loadTestAndReferenceFiles(expectationsContainers, expectationsContainer, baseTest(test, suite));
1107        return;
1108    }
1109
1110    addExpectationItem(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH_FOR_XHR + test);
1111    addExpectationItem(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH_FOR_XHR + reftest_html_file);
1112    addExpectationItem(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH_FOR_XHR + reftest_mismatch_html_file);
1113}
1114
1115function loadExpectationsLayoutTests(test, expectationsContainer)
1116{
1117    // Map from file extension to container div for expectations of that type.
1118    var expectationsContainers = {};
1119    loadTestAndReferenceFiles(expectationsContainers, expectationsContainer, test);
1120
1121    var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
1122
1123    for (var builder in currentBuilders()) {
1124        var actualResultsBase = TEST_RESULTS_BASE_PATH + currentBuilders()[builder] + '/results/layout-test-results/';
1125        ACTUAL_RESULT_SUFFIXES.forEach(function(suffix) {{
1126            addExpectationItem(expectationsContainers, expectationsContainer, actualResultsBase + testWithoutSuffix + '-' + suffix, builder);
1127        }})
1128    }
1129
1130    // Add a clearing element so floated elements don't bleed out of their
1131    // containing block.
1132    var br = document.createElement('br');
1133    br.style.clear = 'both';
1134    expectationsContainer.appendChild(br);
1135}
1136
1137function appendExpectations()
1138{
1139    var expectations = g_history.dashboardSpecificState.showExpectations ? document.getElementsByClassName('expectations') : [];
1140    g_chunkedActionState = {
1141        items: expectations,
1142        index: 0
1143    }
1144    performChunkedAction(function(expectation) {
1145            loadExpectations(expectation);
1146            postHeightChangedMessage();
1147        },
1148        hideLoadingUI,
1149        expectations);
1150}
1151
1152function hideLoadingUI()
1153{
1154    var loadingDiv = $('loading-ui');
1155    if (loadingDiv)
1156        loadingDiv.style.display = 'none';
1157    postHeightChangedMessage();
1158}
1159
1160function generatePageForIndividualTests(tests)
1161{
1162    console.log('Number of tests: ' + tests.length);
1163    if (g_history.dashboardSpecificState.showChrome)
1164        appendHTML(htmlForNavBar());
1165    performChunkedAction(function(test) {
1166            appendHTML(htmlForIndividualTest(test));
1167        },
1168        appendExpectations,
1169        tests);
1170    if (g_history.dashboardSpecificState.showChrome) {
1171        $('tests-input').value = g_history.dashboardSpecificState.tests;
1172        $('result-input').value = g_history.dashboardSpecificState.result;
1173    }
1174}
1175
1176var g_chunkedActionRequestId;
1177function performChunkedAction(action, onComplete, items, opt_index) {
1178    if (g_chunkedActionRequestId)
1179        cancelAnimationFrame(g_chunkedActionRequestId);
1180
1181    var index = opt_index || 0;
1182    g_chunkedActionRequestId = requestAnimationFrame(function() {
1183        if (index < items.length) {
1184            action(items[index]);
1185            performChunkedAction(action, onComplete, items, ++index);
1186        } else {
1187            onComplete();
1188        }
1189    });
1190}
1191
1192function htmlForIndividualTest(test)
1193{
1194    var testNameHtml = '';
1195    if (g_history.dashboardSpecificState.showChrome) {
1196        if (g_history.isLayoutTestResults()) {
1197            var suite = lookupVirtualTestSuite(test);
1198            var base = suite ? baseTest(test, suite) : test;
1199            var versionControlUrl = TEST_URL_BASE_PATH_FOR_BROWSING + base;
1200            testNameHtml += '<h2>' + linkHTMLToOpenWindow(versionControlUrl, test) + '</h2>';
1201        } else
1202            testNameHtml += '<h2>' + test + '</h2>';
1203    }
1204
1205    return testNameHtml + htmlForIndividualTestOnAllBuildersWithResultsLinks(test);
1206}
1207
1208function setTestsParameter(input)
1209{
1210    g_history.setQueryParameter('tests', input.value);
1211}
1212
1213function htmlForNavBar()
1214{
1215    var extraHTML = '';
1216    var html = ui.html.testTypeSwitcher(false, extraHTML, isCrossBuilderView());
1217    html += '<div class=forms><form id=result-form ' +
1218        'onsubmit="g_history.setQueryParameter(\'result\', result.value);' +
1219        'return false;">Show all tests with result: ' +
1220        '<input name=result placeholder="e.g. CRASH" id=result-input>' +
1221        '</form><span>Show tests on all platforms: </span>' +
1222        // Use a textarea to avoid the 32k limit on the length of inputs.
1223        '<textarea name=tests ' +
1224        'placeholder="Comma or space-separated list of tests or partial ' +
1225        'paths to show test results across all builders, e.g., ' +
1226        'foo/bar.html,foo/baz,domstorage" id=tests-input onchange="setTestsParameter(this)" ' +
1227        'onkeydown="if (event.keyCode == 13) { setTestsParameter(this); return false; }"></textarea>' +
1228        '<span class=link onclick="showLegend()">Show legend [type ?]</span></div>';
1229    return html;
1230}
1231
1232function checkBoxToToggleState(key, text)
1233{
1234    var stateEnabled = g_history.dashboardSpecificState[key];
1235    return '<label><input type=checkbox ' + (stateEnabled ? 'checked ' : '') + 'onclick="g_history.setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + text + '</label> ';
1236}
1237
1238function linkHTMLToToggleState(key, linkText)
1239{
1240    var stateEnabled = g_history.dashboardSpecificState[key];
1241    return '<span class=link onclick="g_history.setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + (stateEnabled ? 'Hide' : 'Show') + ' ' + linkText + '</span>';
1242}
1243
1244function headerForTestTableHtml()
1245{
1246    return '<h2 style="display:inline-block">Failing tests</h2>' +
1247        checkBoxToToggleState('showFlaky', 'Show flaky') +
1248        checkBoxToToggleState('showNonFlaky', 'Show non-flaky') +
1249        checkBoxToToggleState('showSkip', 'Show Skip') +
1250        checkBoxToToggleState('showWontFix', 'Show WontFix');
1251}
1252
1253function generatePageForBuilder(builderName)
1254{
1255    processTestRunsForBuilder(builderName);
1256
1257    var filteredResults = g_perBuilderFailures[builderName].filter(shouldShowTest);
1258    sortTests(filteredResults, g_history.dashboardSpecificState.sortColumn, g_history.dashboardSpecificState.sortOrder);
1259
1260    var testsHTML = '';
1261    if (filteredResults.length) {
1262        var tableRowsHTML = '';
1263        var showBuilderNames = false;
1264        for (var i = 0; i < filteredResults.length; i++)
1265            tableRowsHTML += htmlForSingleTestRow(filteredResults[i], showBuilderNames)
1266        testsHTML = htmlForTestTable(tableRowsHTML);
1267    } else {
1268        if (g_history.isLayoutTestResults())
1269            testsHTML += '<div>Fill in one of the text inputs or checkboxes above to show failures.</div>';
1270        else
1271            testsHTML += '<div>No tests have failed!</div>';
1272    }
1273
1274    var html = htmlForNavBar();
1275
1276    if (g_history.isLayoutTestResults())
1277        html += headerForTestTableHtml();
1278
1279    html += '<br>' + testsHTML;
1280    appendHTML(html);
1281
1282    var ths = document.getElementsByTagName('th');
1283    for (var i = 0; i < ths.length; i++) {
1284        ths[i].addEventListener('click', changeSort, false);
1285        ths[i].className = "sortable";
1286    }
1287
1288    hideLoadingUI();
1289}
1290
1291var VALID_KEYS_FOR_CROSS_BUILDER_VIEW = {
1292    tests: 1,
1293    result: 1,
1294    showChrome: 1,
1295    showExpectations: 1,
1296    showLargeExpectations: 1,
1297    resultsHeight: 1,
1298    revision: 1
1299};
1300
1301function isInvalidKeyForCrossBuilderView(key)
1302{
1303    return !(key in VALID_KEYS_FOR_CROSS_BUILDER_VIEW) && !(key in history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES);
1304}
1305
1306function hideLegend()
1307{
1308    var legend = $('legend');
1309    if (legend)
1310        legend.parentNode.removeChild(legend);
1311}
1312
1313function showLegend()
1314{
1315    var legend = $('legend');
1316    if (!legend) {
1317        legend = document.createElement('div');
1318        legend.id = 'legend';
1319        document.body.appendChild(legend);
1320    }
1321
1322    var html = '<div id=legend-toggle onclick="hideLegend()">Hide ' +
1323        'legend [type esc]</div><div id=legend-contents>';
1324
1325    // Just grab the first failureMap. Technically, different builders can have different maps if they
1326    // haven't all cycled after the map was changed, but meh.
1327    var failureMap = g_resultsByBuilder[Object.keys(g_resultsByBuilder)[0]][results.FAILURE_MAP];
1328    for (var expectation in failureMap) {
1329        var failureString = failureMap[expectation];
1330        html += '<div class=' + classNameForFailureString(failureString) + '>' + failureString + '</div>';
1331    }
1332
1333    if (g_history.isLayoutTestResults()) {
1334      html += '</div><br style="clear:both">' +
1335          '</div>';
1336
1337      html += '<div>RELEASE TIMEOUTS:</div>' +
1338          htmlForSlowTimes(RELEASE_TIMEOUT) +
1339          '<div>DEBUG TIMEOUTS:</div>' +
1340          htmlForSlowTimes(DEBUG_TIMEOUT);
1341    }
1342
1343    legend.innerHTML = html;
1344}
1345
1346function htmlForSlowTimes(minTime)
1347{
1348    return '<ul><li>' + minTime + ' seconds</li><li>' +
1349        SLOW_MULTIPLIER * minTime + ' seconds if marked Slow in TestExpectations</li></ul>';
1350}
1351
1352function postHeightChangedMessage()
1353{
1354    if (window == parent)
1355        return;
1356
1357    var root = document.documentElement;
1358    var height = root.offsetHeight;
1359    if (root.offsetWidth < root.scrollWidth) {
1360        // We have a horizontal scrollbar. Include it in the height.
1361        var dummyNode = document.createElement('div');
1362        dummyNode.style.overflow = 'scroll';
1363        document.body.appendChild(dummyNode);
1364        var scrollbarWidth = dummyNode.offsetHeight - dummyNode.clientHeight;
1365        document.body.removeChild(dummyNode);
1366        height += scrollbarWidth;
1367    }
1368    parent.postMessage({command: 'heightChanged', height: height}, '*')
1369}
1370
1371if (window != parent)
1372    window.addEventListener('blur', ui.popup.hide);
1373
1374document.addEventListener('keydown', function(e) {
1375    if (e.keyIdentifier == 'U+003F' || e.keyIdentifier == 'U+00BF') {
1376        // WebKit MAC retursn 3F. WebKit WIN returns BF. This is a bug!
1377        // ? key
1378        showLegend();
1379    } else if (e.keyIdentifier == 'U+001B') {
1380        // escape key
1381        hideLegend();
1382        ui.popup.hide();
1383    }
1384}, false);
1385
1386window.addEventListener('load', function() {
1387    resourceLoader = new loader.Loader();
1388    resourceLoader.load();
1389}, false);
1390