blob: 750cd3a75eea6dc16a01eb2e3f2a57b000bbcf9b [file] [log] [blame]
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001// 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 ALL = 'ALL';
33var FORWARD = 'forward';
34var BACKWARD = 'backward';
35var GTEST_MODIFIERS = ['FLAKY', 'FAILS', 'MAYBE', 'DISABLED'];
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +010036var TEST_URL_BASE_PATH_IN_VERSION_CONTROL = 'http://src.chromium.org/viewvc/blink/trunk/LayoutTests/';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +000037var TEST_URL_BASE_PATH = "http://svn.webkit.org/repository/webkit/trunk/LayoutTests/";
38var EXPECTATIONS_URL_BASE_PATH = TEST_URL_BASE_PATH + "platform/";
39var TEST_RESULTS_BASE_PATH = 'http://build.chromium.org/f/chromium/layout_test_results/';
40var GPU_RESULTS_BASE_PATH = 'http://chromium-browser-gpu-tests.commondatastorage.googleapis.com/runs/'
41
42var PLATFORMS = {
43 'CHROMIUM': {
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +010044 expectationsDirectory: null, /* FIXME: cleanup post blink split 'chromium', */
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +000045 subPlatforms: {
46 'LION': { fallbackPlatforms: ['CHROMIUM'] },
47 'SNOWLEOPARD': { fallbackPlatforms: ['CHROMIUM'] },
48 'XP': { fallbackPlatforms: ['CHROMIUM'] },
49 'VISTA': { fallbackPlatforms: ['CHROMIUM'] },
50 'WIN7': { fallbackPlatforms: ['CHROMIUM'] },
51 'LUCID': { fallbackPlatforms: ['CHROMIUM'] },
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +010052 'ANDROID': { fallbackPlatforms: ['CHROMIUM'], expectationsDirectory: null /* 'chromium-android' */ }
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +000053 },
54 platformModifierUnions: {
55 'MAC': ['CHROMIUM_LION', 'CHROMIUM_SNOWLEOPARD'],
56 'WIN': ['CHROMIUM_XP', 'CHROMIUM_VISTA', 'CHROMIUM_WIN7'],
57 'LINUX': ['CHROMIUM_LUCID']
58 }
59 },
60 'APPLE': {
61 subPlatforms: {
62 'MAC': {
63 expectationsDirectory: 'mac',
64 subPlatforms: {
65 'LION': {
66 expectationsDirectory: 'mac-lion',
67 subPlatforms: {
68 'WK1': { fallbackPlatforms: ['APPLE_MAC_LION', 'APPLE_MAC'] },
69 'WK2': { fallbackPlatforms: ['APPLE_MAC_LION', 'APPLE_MAC', 'WK2'] }
70 }
71 },
72 'SNOWLEOPARD': {
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +010073 expectationsDirectory: null,
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +000074 subPlatforms: {
75 'WK1': { fallbackPlatforms: ['APPLE_MAC_SNOWLEOPARD', 'APPLE_MAC'] },
76 'WK2': { fallbackPlatforms: ['APPLE_MAC_SNOWLEOPARD', 'APPLE_MAC', 'WK2'] }
77 }
78 }
79 }
80 },
81 'WIN': {
82 expectationsDirectory: 'win',
83 subPlatforms: {
84 'XP': { fallbackPlatforms: ['APPLE_WIN'] },
85 'WIN7': { fallbackPlatforms: ['APPLE_WIN'] }
86 }
87 }
88 }
89 },
90 'GTK': {
91 expectationsDirectory: 'gtk',
92 subPlatforms: {
93 'LINUX': {
94 subPlatforms: {
95 'WK1': { fallbackPlatforms: ['GTK'] },
96 'WK2': { fallbackPlatforms: ['GTK', 'WK2'], expectationsDirectory: 'gtk-wk2' }
97 }
98 }
99 }
100 },
101 'QT': {
102 expectationsDirectory: 'qt',
103 subPlatforms: {
104 'LINUX': { fallbackPlatforms: ['QT'] }
105 }
106 },
107 'EFL': {
108 expectationsDirectory: 'efl',
109 subPlatforms: {
110 'LINUX': {
111 subPlatforms: {
112 'WK1': { fallbackPlatforms: ['EFL'], expectationsDirectory: 'efl-wk1' },
113 'WK2': { fallbackPlatforms: ['EFL', 'WK2'], expectationsDirectory: 'efl-wk2' }
114 }
115 }
116 }
117 },
118 'WK2': {
119 basePlatform: true,
120 expectationsDirectory: 'wk2'
121 }
122};
123
124var BUILD_TYPES = {'DEBUG': 'DBG', 'RELEASE': 'RELEASE'};
125var MIN_SECONDS_FOR_SLOW_TEST = 4;
126var MIN_SECONDS_FOR_SLOW_TEST_DEBUG = 2 * MIN_SECONDS_FOR_SLOW_TEST;
127var FAIL_RESULTS = ['IMAGE', 'IMAGE+TEXT', 'TEXT', 'MISSING'];
128var CHUNK_SIZE = 25;
129var MAX_RESULTS = 1500;
130
131// FIXME: Figure out how to make this not be hard-coded.
132var VIRTUAL_SUITES = {
133 'platform/chromium/virtual/gpu/fast/canvas': 'fast/canvas',
134 'platform/chromium/virtual/gpu/canvas/philip': 'canvas/philip'
135};
136
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000137var resourceLoader;
138
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100139function generatePage(historyInstance)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000140{
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100141 if (historyInstance.crossDashboardState.useTestData)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000142 return;
143
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000144 document.body.innerHTML = '<div id="loading-ui">LOADING...</div>';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000145 resourceLoader.showErrors();
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000146
147 // tests expands to all tests that match the CSV list.
148 // result expands to all tests that ever have the given result
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100149 if (historyInstance.dashboardSpecificState.tests || historyInstance.dashboardSpecificState.result)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000150 generatePageForIndividualTests(individualTests());
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100151 else if (historyInstance.dashboardSpecificState.expectationsUpdate)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000152 generatePageForExpectationsUpdate();
153 else
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100154 generatePageForBuilder(historyInstance.dashboardSpecificState.builder || currentBuilderGroup().defaultBuilder());
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000155
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000156 for (var builder in currentBuilders())
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000157 processTestResultsForBuilderAsync(builder);
158
159 postHeightChangedMessage();
160}
161
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100162function handleValidHashParameter(historyInstance, key, value)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000163{
164 switch(key) {
165 case 'tests':
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100166 history.validateParameter(historyInstance.dashboardSpecificState, key, value,
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000167 function() {
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000168 return string.isValidName(value);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000169 });
170 return true;
171
172 case 'result':
173 value = value.toUpperCase();
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100174 history.validateParameter(historyInstance.dashboardSpecificState, key, value,
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000175 function() {
176 for (var result in LAYOUT_TEST_EXPECTATIONS_MAP_) {
177 if (value == LAYOUT_TEST_EXPECTATIONS_MAP_[result])
178 return true;
179 }
180 return false;
181 });
182 return true;
183
184 case 'builder':
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100185 history.validateParameter(historyInstance.dashboardSpecificState, key, value,
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000186 function() {
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000187 return value in currentBuilders();
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000188 });
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000189
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000190 return true;
191
192 case 'sortColumn':
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100193 history.validateParameter(historyInstance.dashboardSpecificState, key, value,
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000194 function() {
195 // Get all possible headers since the actual used set of headers
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100196 // depends on the values in historyInstance.dashboardSpecificState, which are currently being set.
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000197 var headers = tableHeaders(true);
198 for (var i = 0; i < headers.length; i++) {
199 if (value == sortColumnFromTableHeader(headers[i]))
200 return true;
201 }
202 return value == 'test' || value == 'builder';
203 });
204 return true;
205
206 case 'sortOrder':
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100207 history.validateParameter(historyInstance.dashboardSpecificState, key, value,
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000208 function() {
209 return value == FORWARD || value == BACKWARD;
210 });
211 return true;
212
213 case 'resultsHeight':
214 case 'updateIndex':
215 case 'revision':
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100216 history.validateParameter(historyInstance.dashboardSpecificState, key, Number(value),
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000217 function() {
218 return value.match(/^\d+$/);
219 });
220 return true;
221
222 case 'showChrome':
223 case 'showCorrectExpectations':
224 case 'showWrongExpectations':
225 case 'showExpectations':
226 case 'showFlaky':
227 case 'showLargeExpectations':
228 case 'legacyExpectationsSemantics':
229 case 'showSkipped':
230 case 'showSlow':
231 case 'showUnexpectedPasses':
232 case 'showWontFixSkip':
233 case 'expectationsUpdate':
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100234 historyInstance.dashboardSpecificState[key] = value == 'true';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000235 return true;
236
237 default:
238 return false;
239 }
240}
241
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100242// @param {Object} params New or modified query parameters as key: value.
243function handleQueryParameterChange(historyInstance, params)
244{
245 for (key in params) {
246 if (key == 'tests') {
247 // Entering cross-builder view, only keep valid keys for that view.
248 for (var currentKey in historyInstance.dashboardSpecificState) {
249 if (isInvalidKeyForCrossBuilderView(currentKey)) {
250 delete historyInstance.dashboardSpecificState[currentKey];
251 }
252 }
253 } else if (isInvalidKeyForCrossBuilderView(key)) {
254 delete historyInstance.dashboardSpecificState.tests;
255 delete historyInstance.dashboardSpecificState.result;
256 }
257 }
258
259 return true;
260}
261
262var defaultDashboardSpecificStateValues = {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000263 sortOrder: BACKWARD,
264 sortColumn: 'flakiness',
265 showExpectations: false,
266 showFlaky: true,
267 showLargeExpectations: false,
268 legacyExpectationsSemantics: true,
269 showChrome: true,
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100270 showCorrectExpectations: false,
271 showWrongExpectations: false,
272 showWontFixSkip: false,
273 showSlow: false,
274 showSkipped: false,
275 showUnexpectedPasses: false,
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000276 expectationsUpdate: false,
277 updateIndex: 0,
278 resultsHeight: 300,
279 revision: null,
280 tests: '',
281 result: '',
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000282 builder: null
283};
284
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100285var DB_SPECIFIC_INVALIDATING_PARAMETERS = {
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000286 'tests' : 'builder',
287 'testType': 'builder',
288 'group': 'builder'
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000289};
290
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100291
292var flakinessConfig = {
293 defaultStateValues: defaultDashboardSpecificStateValues,
294 generatePage: generatePage,
295 handleValidHashParameter: handleValidHashParameter,
296 handleQueryParameterChange: handleQueryParameterChange,
297 invalidatingHashParameters: DB_SPECIFIC_INVALIDATING_PARAMETERS
298};
299
300// FIXME(jparent): Eventually remove all usage of global history object.
301var g_history = new history.History(flakinessConfig);
302g_history.parseCrossDashboardParameters();
303
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000304//////////////////////////////////////////////////////////////////////////////
305// GLOBALS
306//////////////////////////////////////////////////////////////////////////////
307
308var g_perBuilderPlatformAndBuildType = {};
309var g_perBuilderFailures = {};
310// Map of builder to arrays of tests that are listed in the expectations file
311// but have for that builder.
312var g_perBuilderWithExpectationsButNoFailures = {};
313// Map of builder to arrays of paths that are skipped. This shows the raw
314// path used in TestExpectations rather than the test path since we
315// don't actually have any data here for skipped tests.
316var g_perBuilderSkippedPaths = {};
317// Maps test path to an array of {builder, testResults} objects.
318var g_testToResultsMap = {};
319// Tests that the user wants to update expectations for.
320var g_confirmedTests = {};
321
322function traversePlatformsTree(callback)
323{
324 function traverse(platformObject, parentPlatform) {
325 Object.keys(platformObject).forEach(function(platformName) {
326 var platform = platformObject[platformName];
327 platformName = parentPlatform ? parentPlatform + platformName : platformName;
328
329 if (platform.subPlatforms)
330 traverse(platform.subPlatforms, platformName + '_');
331 else if (!platform.basePlatform)
332 callback(platform, platformName);
333 });
334 }
335 traverse(PLATFORMS, null);
336}
337
338function createResultsObjectForTest(test, builder)
339{
340 return {
341 test: test,
342 builder: builder,
343 // HTML for display of the results in the flakiness column
344 html: '',
345 flips: 0,
346 slowestTime: 0,
347 slowestNonTimeoutCrashTime: 0,
348 meetsExpectations: true,
349 isWontFixSkip: false,
350 isFlaky: false,
351 // Sorted string of missing expectations
352 missing: '',
353 // String of extra expectations (i.e. expectations that never occur).
354 extra: '',
355 modifiers: '',
356 bugs: '',
357 expectations : '',
358 rawResults: '',
359 // List of all the results the test actually has.
360 actualResults: []
361 };
362}
363
364function matchingElement(stringToMatch, elementsMap)
365{
366 for (var element in elementsMap) {
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000367 if (string.contains(stringToMatch, elementsMap[element]))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000368 return element;
369 }
370}
371
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000372function chromiumPlatform(builderNameUpperCase)
373{
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000374 if (string.contains(builderNameUpperCase, 'MAC')) {
375 if (string.contains(builderNameUpperCase, '10.7'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000376 return 'CHROMIUM_LION';
377 // The webkit.org 'Chromium Mac Release (Tests)' bot runs SnowLeopard.
378 return 'CHROMIUM_SNOWLEOPARD';
379 }
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000380 if (string.contains(builderNameUpperCase, 'WIN7'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000381 return 'CHROMIUM_WIN7';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000382 if (string.contains(builderNameUpperCase, 'VISTA'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000383 return 'CHROMIUM_VISTA';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000384 if (string.contains(builderNameUpperCase, 'WIN') || string.contains(builderNameUpperCase, 'XP'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000385 return 'CHROMIUM_XP';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000386 if (string.contains(builderNameUpperCase, 'LINUX'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000387 return 'CHROMIUM_LUCID';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000388 if (string.contains(builderNameUpperCase, 'ANDROID'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000389 return 'CHROMIUM_ANDROID';
390 // The interactive bot is XP, but doesn't have an OS in it's name.
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000391 if (string.contains(builderNameUpperCase, 'INTERACTIVE'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000392 return 'CHROMIUM_XP';
393}
394
395
396function platformAndBuildType(builderName)
397{
398 if (!g_perBuilderPlatformAndBuildType[builderName]) {
399 var builderNameUpperCase = builderName.toUpperCase();
400
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +0100401 var platform = chromiumPlatform(builderNameUpperCase);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000402
403 if (!platform)
404 console.error('Could not resolve platform for builder: ' + builderName);
405
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000406 var buildType = string.contains(builderNameUpperCase, 'DBG') || string.contains(builderNameUpperCase, 'DEBUG') ? 'DEBUG' : 'RELEASE';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000407 g_perBuilderPlatformAndBuildType[builderName] = {platform: platform, buildType: buildType};
408 }
409 return g_perBuilderPlatformAndBuildType[builderName];
410}
411
412function isDebug(builderName)
413{
414 return platformAndBuildType(builderName).buildType == 'DEBUG';
415}
416
417// Returns the expectation string for the given single character result.
418// This string should match the expectations that are put into
419// test_expectations.py.
420//
421// For example, if we start explicitly listing IMAGE result failures,
422// this function should start returning 'IMAGE'.
423function expectationsFileStringForResult(result)
424{
425 // For the purposes of comparing against the expecations of a test,
426 // consider simplified diff failures as just text failures since
427 // the test_expectations file doesn't treat them specially.
428 if (result == 'S')
429 return 'TEXT';
430
431 if (result == 'N')
432 return '';
433
434 return expectationsMap()[result];
435}
436
437var TestTrie = function(builders, resultsByBuilder)
438{
439 this._trie = {};
440
441 for (var builder in builders) {
442 var testsForBuilder = resultsByBuilder[builder].tests;
443 for (var test in testsForBuilder)
444 this._addTest(test.split('/'), this._trie);
445 }
446}
447
448TestTrie.prototype.forEach = function(callback, startingTriePath)
449{
450 var testsTrie = this._trie;
451 if (startingTriePath) {
452 var splitPath = startingTriePath.split('/');
453 while (splitPath.length && testsTrie)
454 testsTrie = testsTrie[splitPath.shift()];
455 }
456
457 if (!testsTrie)
458 return;
459
460 function traverse(trie, triePath) {
461 if (trie == true)
462 callback(triePath);
463 else {
464 for (var member in trie)
465 traverse(trie[member], triePath ? triePath + '/' + member : member);
466 }
467 }
468 traverse(testsTrie, startingTriePath);
469}
470
471TestTrie.prototype._addTest = function(test, trie)
472{
473 var rootComponent = test.shift();
474 if (!test.length) {
475 if (!trie[rootComponent])
476 trie[rootComponent] = true;
477 return;
478 }
479
480 if (!trie[rootComponent] || trie[rootComponent] == true)
481 trie[rootComponent] = {};
482 this._addTest(test, trie[rootComponent]);
483}
484
485// Map of all tests to true values. This is just so we can have the list of
486// all tests across all the builders.
487var g_allTestsTrie;
488
489function getAllTestsTrie()
490{
491 if (!g_allTestsTrie)
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000492 g_allTestsTrie = new TestTrie(currentBuilders(), g_resultsByBuilder);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000493
494 return g_allTestsTrie;
495}
496
497// Returns an array of tests to be displayed in the individual tests view.
498// Note that a directory can be listed as a test, so we expand that into all
499// tests in the directory.
500function individualTests()
501{
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000502 if (g_history.dashboardSpecificState.result)
503 return allTestsWithResult(g_history.dashboardSpecificState.result);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000504
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000505 if (!g_history.dashboardSpecificState.tests)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000506 return [];
507
508 return individualTestsForSubstringList();
509}
510
511function substringList()
512{
513 // Convert windows slashes to unix slashes.
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000514 var tests = g_history.dashboardSpecificState.tests.replace(/\\/g, '/');
515 var separator = string.contains(tests, ' ') ? ' ' : ',';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000516 var testList = tests.split(separator);
517
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000518 if (g_history.isLayoutTestResults())
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000519 return testList;
520
521 var testListWithoutModifiers = [];
522 testList.forEach(function(path) {
523 GTEST_MODIFIERS.forEach(function(modifier) {
524 path = path.replace('.' + modifier + '_', '.');
525 });
526 testListWithoutModifiers.push(path);
527 });
528 return testListWithoutModifiers;
529}
530
531function individualTestsForSubstringList()
532{
533 var testList = substringList();
534
535 // Put the tests into an object first and then move them into an array
536 // as a way of deduping.
537 var testsMap = {};
538 for (var i = 0; i < testList.length; i++) {
539 var path = testList[i];
540
541 // Ignore whitespace entries as they'd match every test.
542 if (path.match(/^\s*$/))
543 continue;
544
545 var hasAnyMatches = false;
546 getAllTestsTrie().forEach(function(triePath) {
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000547 if (string.caseInsensitiveContains(triePath, path)) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000548 testsMap[triePath] = 1;
549 hasAnyMatches = true;
550 }
551 });
552
553 // If a path doesn't match any tests, then assume it's a full path
554 // to a test that passes on all builders.
555 if (!hasAnyMatches)
556 testsMap[path] = 1;
557 }
558
559 var testsArray = [];
560 for (var test in testsMap)
561 testsArray.push(test);
562 return testsArray;
563}
564
565// Returns whether this test's slowest time is above the cutoff for
566// being a slow test.
567function isSlowTest(resultsForTest)
568{
569 var maxTime = isDebug(resultsForTest.builder) ? MIN_SECONDS_FOR_SLOW_TEST_DEBUG : MIN_SECONDS_FOR_SLOW_TEST;
570 return resultsForTest.slowestNonTimeoutCrashTime > maxTime;
571}
572
573// Returns whether this test's slowest time is *well* below the cutoff for
574// being a slow test.
575function isFastTest(resultsForTest)
576{
577 var maxTime = isDebug(resultsForTest.builder) ? MIN_SECONDS_FOR_SLOW_TEST_DEBUG : MIN_SECONDS_FOR_SLOW_TEST;
578 return resultsForTest.slowestNonTimeoutCrashTime < maxTime / 2;
579}
580
581function allTestsWithResult(result)
582{
583 processTestRunsForAllBuilders();
584 var retVal = [];
585
586 getAllTestsTrie().forEach(function(triePath) {
587 for (var i = 0; i < g_testToResultsMap[triePath].length; i++) {
588 if (g_testToResultsMap[triePath][i].actualResults.indexOf(result) != -1) {
589 retVal.push(triePath);
590 break;
591 }
592 }
593 });
594
595 return retVal;
596}
597
598
599// Adds all the tests for the given builder to the testMapToPopulate.
600function addTestsForBuilder(builder, testMapToPopulate)
601{
602 var tests = g_resultsByBuilder[builder].tests;
603 for (var test in tests) {
604 testMapToPopulate[test] = true;
605 }
606}
607
608// Map of all tests to true values by platform and build type.
609// e.g. g_allTestsByPlatformAndBuildType['XP']['DEBUG'] will have the union
610// of all tests run on the xp-debug builders.
611var g_allTestsByPlatformAndBuildType = {};
612traversePlatformsTree(function(platform, platformName) {
613 g_allTestsByPlatformAndBuildType[platformName] = {};
614});
615
616// Map of all tests to true values by platform and build type.
617// e.g. g_allTestsByPlatformAndBuildType['WIN']['DEBUG'] will have the union
618// of all tests run on the win-debug builders.
619function allTestsWithSamePlatformAndBuildType(platform, buildType)
620{
621 if (!g_allTestsByPlatformAndBuildType[platform][buildType]) {
622 var tests = {};
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000623 for (var thisBuilder in currentBuilders()) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000624 var thisBuilderBuildInfo = platformAndBuildType(thisBuilder);
625 if (thisBuilderBuildInfo.buildType == buildType && thisBuilderBuildInfo.platform == platform) {
626 addTestsForBuilder(thisBuilder, tests);
627 }
628 }
629 g_allTestsByPlatformAndBuildType[platform][buildType] = tests;
630 }
631
632 return g_allTestsByPlatformAndBuildType[platform][buildType];
633}
634
635function getExpectations(test, platform, buildType)
636{
637 var testObject = g_allExpectations[test];
638 if (!testObject)
639 return null;
640
641 var platformObject = testObject[platform];
642 if (!platformObject)
643 return null;
644
645 return platformObject[buildType];
646}
647
648function filterBugs(modifiers)
649{
650 var bugs = modifiers.match(/\b(Bug|webkit.org|crbug.com|code.google.com)\S*/g);
651 if (!bugs)
652 return {bugs: '', modifiers: modifiers};
653 for (var j = 0; j < bugs.length; j++)
654 modifiers = modifiers.replace(bugs[j], '');
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000655 return {bugs: bugs.join(' '), modifiers: string.collapseWhitespace(string.trimString(modifiers))};
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000656}
657
658function populateExpectationsData(resultsObject)
659{
660 var buildInfo = platformAndBuildType(resultsObject.builder);
661 var expectations = getExpectations(resultsObject.test, buildInfo.platform, buildInfo.buildType);
662 if (!expectations)
663 return;
664
665 resultsObject.expectations = expectations.expectations;
666 var filteredModifiers = filterBugs(expectations.modifiers);
667 resultsObject.modifiers = filteredModifiers.modifiers;
668 resultsObject.bugs = filteredModifiers.bugs;
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000669 resultsObject.isWontFixSkip = string.contains(expectations.modifiers, 'WONTFIX') || string.contains(expectations.modifiers, 'SKIP');
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000670}
671
672function platformObjectForName(platformName)
673{
674 var platformsList = platformName.split("_");
675 var platformObject = PLATFORMS[platformsList.shift()];
676 platformsList.forEach(function(platformName) {
677 platformObject = platformObject.subPlatforms[platformName];
678 });
679 return platformObject;
680}
681
682// Data structure to hold the processed expectations.
683// g_allExpectations[testPath][platform][buildType] gets the object that has
684// expectations and modifiers properties for this platform/buildType.
685//
686// platform and buildType both go through fallback sets of keys from most
687// specific key to least specific. For example, on Windows XP, we first
688// check the platform WIN-XP, if there's no such object, we check WIN,
689// then finally we check ALL. For build types, we check the current
690// buildType, then ALL.
691var g_allExpectations;
692
693function getParsedExpectations(data)
694{
695 var expectations = [];
696 var lines = data.split('\n');
697 lines.forEach(function(line) {
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000698 line = string.trimString(line);
699 if (!line || string.startsWith(line, '#'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000700 return;
701
702 // This code mimics _tokenize_line_using_new_format() in
703 // Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py
704 //
705 // FIXME: consider doing more error checking here.
706 //
707 // FIXME: Clean this all up once we've fully cut over to the new syntax.
708 var tokens = line.split(/\s+/)
709 var parsed_bugs = [];
710 var parsed_modifiers = [];
711 var parsed_path;
712 var parsed_expectations = [];
713 var state = 'start';
714
Torne (Richard Coles)e5249552013-05-15 11:35:13 +0100715 // This clones _modifier_tokens_list in test_expectations.py.
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000716 // FIXME: unify with the platforms constants at the top of the file.
Torne (Richard Coles)e5249552013-05-15 11:35:13 +0100717 var modifier_tokens = {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000718 'Release': 'RELEASE',
719 'Debug': 'DEBUG',
720 'Mac': 'MAC',
721 'Win': 'WIN',
722 'Linux': 'LINUX',
723 'SnowLeopard': 'SNOWLEOPARD',
724 'Lion': 'LION',
725 'MountainLion': 'MOUNTAINLION',
726 'Win7': 'WIN7',
727 'XP': 'XP',
728 'Vista': 'VISTA',
729 'Android': 'ANDROID',
730 };
731
732 var expectation_tokens = {
733 'Crash': 'CRASH',
734 'Failure': 'FAIL',
735 'ImageOnlyFailure': 'IMAGE',
736 'Missing': 'MISSING',
737 'Pass': 'PASS',
738 'Rebaseline': 'REBASELINE',
739 'Skip': 'SKIP',
740 'Slow': 'SLOW',
741 'Timeout': 'TIMEOUT',
742 'WontFix': 'WONTFIX',
743 };
744
Torne (Richard Coles)e5249552013-05-15 11:35:13 +0100745 var reachedEol = false;
746
747 // States
748 // - start: Next tokens are bugs or a path.
749 // - modifier: Parsed bugs and a '['. Next token is a modifier.
750 // - path: Parsed modifiers and a ']'. Next token is a path.
751 // - path_found: Parsed a path. Next token is '[' or EOL.
752 // - expectations: Parsed a path and a '['. Next tokens are
753 // expectations.
754 // - done: Parsed expectations and a ']'. Next is EOL.
755 // - error: Error occurred. Ignore this line.
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000756 tokens.forEach(function(token) {
Torne (Richard Coles)e5249552013-05-15 11:35:13 +0100757 if (reachedEol)
758 return;
759
760 if (state == 'start' &&
761 (token.indexOf('Bug') == 0 ||
762 token.indexOf('webkit.org') == 0 ||
763 token.indexOf('crbug.com') == 0 ||
764 token.indexOf('code.google.com') == 0)) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000765 parsed_bugs.push(token);
766 } else if (token == '[') {
767 if (state == 'start') {
Torne (Richard Coles)e5249552013-05-15 11:35:13 +0100768 state = 'modifier';
769 } else if (state == 'path_found') {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000770 state = 'expectations';
Torne (Richard Coles)e5249552013-05-15 11:35:13 +0100771 } else {
772 console.error('Unexpected \'[\' (state = ' + state + '): ' + line);
773 state = 'error';
774 return;
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000775 }
776 } else if (token == ']') {
Torne (Richard Coles)e5249552013-05-15 11:35:13 +0100777 if (state == 'modifier') {
778 state = 'path';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000779 } else if (state == 'expectations') {
780 state = 'done';
Torne (Richard Coles)e5249552013-05-15 11:35:13 +0100781 } else {
782 state = 'error';
783 return;
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000784 }
Torne (Richard Coles)e5249552013-05-15 11:35:13 +0100785 } else if (state == 'modifier') {
786 var modifier = modifier_tokens[token];
787 if (!modifier) {
788 console.error('Unknown modifier: ' + modifier);
789 state = 'error';
790 return;
791 }
792 parsed_modifiers.push(modifier);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000793 } else if (state == 'expectations') {
794 if (token == 'Rebaseline' || token == 'Skip' || token == 'Slow' || token == 'WontFix') {
795 parsed_modifiers.push(token.toUpperCase());
796 } else {
Torne (Richard Coles)e5249552013-05-15 11:35:13 +0100797 var expectation = expectation_tokens[token];
798 if (!expectation) {
799 console.error('Unknown expectation: ' + expectation);
800 state = 'error';
801 return;
802 }
803 parsed_expectations.push(expectation);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000804 }
805 } else if (token == '#') {
Torne (Richard Coles)e5249552013-05-15 11:35:13 +0100806 reachedEol = true;
807 } else if (state == 'path' || state == 'start') {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000808 parsed_path = token;
Torne (Richard Coles)e5249552013-05-15 11:35:13 +0100809 state = 'path_found';
810 } else {
811 console.error('Unexpected token (state = ' + state + '): ' + token);
812 state = 'error';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000813 }
814 });
815
Torne (Richard Coles)e5249552013-05-15 11:35:13 +0100816 if (state != 'path_found' && state != 'done')
817 return;
818
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000819 if (!parsed_expectations.length) {
820 if (parsed_modifiers.indexOf('Slow') == -1) {
821 parsed_modifiers.push('Skip');
822 parsed_expectations = ['Pass'];
823 }
824 }
825
826 // FIXME: Should we include line number and comment lines here?
827 expectations.push({
828 modifiers: parsed_bugs.concat(parsed_modifiers).join(' '),
829 path: parsed_path,
830 expectations: parsed_expectations.join(' '),
831 });
832 });
833 return expectations;
834}
835
836
837function addTestToAllExpectationsForPlatform(test, platformName, expectations, modifiers)
838{
839 if (!g_allExpectations[test])
840 g_allExpectations[test] = {};
841
842 if (!g_allExpectations[test][platformName])
843 g_allExpectations[test][platformName] = {};
844
845 var allBuildTypes = [];
846 modifiers.split(' ').forEach(function(modifier) {
847 if (modifier in BUILD_TYPES) {
848 allBuildTypes.push(modifier);
849 return;
850 }
851 });
852 if (!allBuildTypes.length)
853 allBuildTypes = Object.keys(BUILD_TYPES);
854
855 allBuildTypes.forEach(function(buildType) {
856 g_allExpectations[test][platformName][buildType] = {modifiers: modifiers, expectations: expectations};
857 });
858}
859
860function processExpectationsForPlatform(platformObject, platformName, expectationsArray)
861{
862 if (!g_allExpectations)
863 g_allExpectations = {};
864
865 if (!expectationsArray)
866 return;
867
868 // Sort the array to hit more specific paths last. More specific
869 // paths (e.g. foo/bar/baz.html) override entries for less-specific ones (e.g. foo/bar).
870 expectationsArray.sort(alphanumericCompare('path'));
871
872 for (var i = 0; i < expectationsArray.length; i++) {
873 var path = expectationsArray[i].path;
874 var modifiers = expectationsArray[i].modifiers;
875 var expectations = expectationsArray[i].expectations;
876
877 var shouldProcessExpectation = false;
878 var hasPlatformModifierUnions = false;
879 if (platformObject.fallbackPlatforms) {
880 platformObject.fallbackPlatforms.forEach(function(fallbackPlatform) {
881 if (shouldProcessExpectation)
882 return;
883
884 var fallbackPlatformObject = platformObjectForName(fallbackPlatform);
885 if (!fallbackPlatformObject.platformModifierUnions)
886 return;
887
888 modifiers.split(' ').forEach(function(modifier) {
889 if (modifier in fallbackPlatformObject.platformModifierUnions) {
890 hasPlatformModifierUnions = true;
891 if (fallbackPlatformObject.platformModifierUnions[modifier].indexOf(platformName) != -1)
892 shouldProcessExpectation = true;
893 }
894 });
895 });
896 }
897
898 if (!hasPlatformModifierUnions)
899 shouldProcessExpectation = true;
900
901 if (!shouldProcessExpectation)
902 continue;
903
904 getAllTestsTrie().forEach(function(triePath) {
905 addTestToAllExpectationsForPlatform(triePath, platformName, expectations, modifiers);
906 }, path);
907 }
908}
909
910function processExpectations()
911{
912 // FIXME: An expectations-by-platform object should be passed into this function rather than checking
913 // for a global object. That way this function can be better tested and meaningful errors can
914 // be reported when expectations for a given platform are not found in that object.
915 if (!g_expectationsByPlatform)
916 return;
917
918 traversePlatformsTree(function(platform, platformName) {
919 if (platform.fallbackPlatforms) {
920 platform.fallbackPlatforms.forEach(function(fallbackPlatform) {
921 if (fallbackPlatform in g_expectationsByPlatform)
922 processExpectationsForPlatform(platform, platformName, g_expectationsByPlatform[fallbackPlatform]);
923 });
924 }
925
926 if (platformName in g_expectationsByPlatform)
927 processExpectationsForPlatform(platform, platformName, g_expectationsByPlatform[platformName]);
928 });
929
930 g_expectationsByPlatform = undefined;
931}
932
933function processMissingTestsWithExpectations(builder, platform, buildType)
934{
935 var noFailures = [];
936 var skipped = [];
937
938 var allTestsForPlatformAndBuildType = allTestsWithSamePlatformAndBuildType(platform, buildType);
939 for (var test in g_allExpectations) {
940 var expectations = getExpectations(test, platform, buildType);
941
942 if (!expectations)
943 continue;
944
945 // Test has expectations, but no result in the builders results.
946 // This means it's either SKIP or passes on all builds.
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000947 if (!allTestsForPlatformAndBuildType[test] && !string.contains(expectations.modifiers, 'WONTFIX')) {
948 if (string.contains(expectations.modifiers, 'SKIP'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000949 skipped.push(test);
950 else if (!expectations.expectations.match(/^\s*PASS\s*$/)) {
951 // Don't show tests expected to always pass. This is used in ways like
952 // the following:
953 // foo/bar = FAIL
954 // foo/bar/baz.html = PASS
955 noFailures.push({test: test, expectations: expectations.expectations, modifiers: expectations.modifiers});
956 }
957 }
958 }
959
960 g_perBuilderSkippedPaths[builder] = skipped.sort();
961 g_perBuilderWithExpectationsButNoFailures[builder] = noFailures.sort();
962}
963
964function processTestResultsForBuilderAsync(builder)
965{
966 setTimeout(function() { processTestRunsForBuilder(builder); }, 0);
967}
968
969function processTestRunsForAllBuilders()
970{
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000971 for (var builder in currentBuilders())
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000972 processTestRunsForBuilder(builder);
973}
974
975function processTestRunsForBuilder(builderName)
976{
977 if (g_perBuilderFailures[builderName])
978 return;
979
980 if (!g_resultsByBuilder[builderName]) {
981 console.error('No tests found for ' + builderName);
982 g_perBuilderFailures[builderName] = [];
983 return;
984 }
985
986 processExpectations();
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000987
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000988 var buildInfo = platformAndBuildType(builderName);
989 var platform = buildInfo.platform;
990 var buildType = buildInfo.buildType;
991 processMissingTestsWithExpectations(builderName, platform, buildType);
992
993 var failures = [];
994 var allTestsForThisBuilder = g_resultsByBuilder[builderName].tests;
995
996 for (var test in allTestsForThisBuilder) {
997 var resultsForTest = createResultsObjectForTest(test, builderName);
998 populateExpectationsData(resultsForTest);
999
1000 var rawTest = g_resultsByBuilder[builderName].tests[test];
1001 resultsForTest.rawTimes = rawTest.times;
1002 var rawResults = rawTest.results;
1003 resultsForTest.rawResults = rawResults;
1004
1005 // FIXME: Switch to resultsByBuild
1006 var times = resultsForTest.rawTimes;
1007 var numTimesSeen = 0;
1008 var numResultsSeen = 0;
1009 var resultsIndex = 0;
1010 var currentResult;
1011 for (var i = 0; i < times.length; i++) {
1012 numTimesSeen += times[i][RLE.LENGTH];
1013
1014 while (rawResults[resultsIndex] && numTimesSeen > (numResultsSeen + rawResults[resultsIndex][RLE.LENGTH])) {
1015 numResultsSeen += rawResults[resultsIndex][RLE.LENGTH];
1016 resultsIndex++;
1017 }
1018
1019 if (rawResults && rawResults[resultsIndex])
1020 currentResult = rawResults[resultsIndex][RLE.VALUE];
1021
1022 var time = times[i][RLE.VALUE]
1023
1024 // Ignore times for crashing/timeout runs for the sake of seeing if
1025 // a test should be marked slow.
1026 if (currentResult != 'C' && currentResult != 'T')
1027 resultsForTest.slowestNonTimeoutCrashTime = Math.max(resultsForTest.slowestNonTimeoutCrashTime, time);
1028 resultsForTest.slowestTime = Math.max(resultsForTest.slowestTime, time);
1029 }
1030
1031 processMissingAndExtraExpectations(resultsForTest);
1032 failures.push(resultsForTest);
1033
1034 if (!g_testToResultsMap[test])
1035 g_testToResultsMap[test] = [];
1036 g_testToResultsMap[test].push(resultsForTest);
1037 }
1038
1039 g_perBuilderFailures[builderName] = failures;
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001040}
1041
1042function processMissingAndExtraExpectations(resultsForTest)
1043{
1044 // Heuristic for determining whether expectations apply to a given test:
1045 // -If a test result happens < MIN_RUNS_FOR_FLAKE, then consider it a flaky
1046 // result and include it in the list of expected results.
1047 // -Otherwise, grab the first contiguous set of runs with the same result
1048 // for >= MIN_RUNS_FOR_FLAKE and ignore all following runs >=
1049 // MIN_RUNS_FOR_FLAKE.
1050 // This lets us rule out common cases of a test changing expectations for
1051 // a few runs, then being fixed or otherwise modified in a non-flaky way.
1052 var rawResults = resultsForTest.rawResults;
1053
1054 // If the first result is no-data that means the test is skipped or is
1055 // being run on a different builder (e.g. moved from one shard to another).
1056 // Ignore these results since we have no real data about what's going on.
1057 if (rawResults[0][RLE.VALUE] == 'N')
1058 return;
1059
1060 // Only consider flake if it doesn't happen twice in a row.
1061 var MIN_RUNS_FOR_FLAKE = 2;
1062 var resultsMap = {}
1063 var numResultsSeen = 0;
1064 var haveSeenNonFlakeResult = false;
1065 var numRealResults = 0;
1066
1067 var seenResults = {};
1068 for (var i = 0; i < rawResults.length; i++) {
1069 var numResults = rawResults[i][RLE.LENGTH];
1070 numResultsSeen += numResults;
1071
1072 var result = rawResults[i][RLE.VALUE];
1073
1074 var hasMinRuns = numResults >= MIN_RUNS_FOR_FLAKE;
1075 if (haveSeenNonFlakeResult && hasMinRuns)
1076 continue;
1077 else if (hasMinRuns)
1078 haveSeenNonFlakeResult = true;
1079 else if (!seenResults[result]) {
1080 // Only consider a short-lived result if we've seen it more than once.
1081 // Otherwise, we include lots of false-positives due to tests that fail
1082 // for a couple runs and then start passing.
1083 seenResults[result] = true;
1084 continue;
1085 }
1086
1087 var expectation = expectationsFileStringForResult(result);
1088 resultsMap[expectation] = true;
1089 numRealResults++;
1090 }
1091
1092 resultsForTest.flips = i - 1;
1093 resultsForTest.isFlaky = numRealResults > 1;
1094
1095 var missingExpectations = [];
1096 var extraExpectations = [];
1097
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001098 if (g_history.isLayoutTestResults()) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001099 var expectationsArray = resultsForTest.expectations ? resultsForTest.expectations.split(' ') : [];
1100 extraExpectations = expectationsArray.filter(
1101 function(element) {
1102 // FIXME: Once all the FAIL lines are removed from
1103 // TestExpectations, delete all the legacyExpectationsSemantics
1104 // code.
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001105 if (g_history.dashboardSpecificState.legacyExpectationsSemantics) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001106 if (element == 'FAIL') {
1107 for (var i = 0; i < FAIL_RESULTS.length; i++) {
1108 if (resultsMap[FAIL_RESULTS[i]])
1109 return false;
1110 }
1111 return true;
1112 }
1113 }
1114
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001115 return element && !resultsMap[element] && !string.contains(element, 'BUG');
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001116 });
1117
1118 for (var result in resultsMap) {
1119 resultsForTest.actualResults.push(result);
1120 var hasExpectation = false;
1121 for (var i = 0; i < expectationsArray.length; i++) {
1122 var expectation = expectationsArray[i];
1123 // FIXME: Once all the FAIL lines are removed from
1124 // TestExpectations, delete all the legacyExpectationsSemantics
1125 // code.
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001126 if (g_history.dashboardSpecificState.legacyExpectationsSemantics) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001127 if (expectation == 'FAIL') {
1128 for (var j = 0; j < FAIL_RESULTS.length; j++) {
1129 if (result == FAIL_RESULTS[j]) {
1130 hasExpectation = true;
1131 break;
1132 }
1133 }
1134 }
1135 }
1136
1137 if (result == expectation)
1138 hasExpectation = true;
1139
1140 if (hasExpectation)
1141 break;
1142 }
1143 // If we have no expectations for a test and it only passes, then don't
1144 // list PASS as a missing expectation. We only want to list PASS if it
1145 // flaky passes, so there would be other expectations.
1146 if (!hasExpectation && !(!expectationsArray.length && result == 'PASS' && numRealResults == 1))
1147 missingExpectations.push(result);
1148 }
1149
1150 // Only highlight tests that take > 2 seconds as needing to be marked as
1151 // slow. There are too many tests that take ~2 seconds every couple
1152 // hundred runs. It's not worth the manual maintenance effort.
1153 // Also, if a test times out, then it should not be marked as slow.
1154 var minTimeForNeedsSlow = isDebug(resultsForTest.builder) ? 2 : 1;
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001155 if (isSlowTest(resultsForTest) && !resultsMap['TIMEOUT'] && (!resultsForTest.modifiers || !string.contains(resultsForTest.modifiers, 'SLOW')))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001156 missingExpectations.push('SLOW');
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001157 else if (isFastTest(resultsForTest) && resultsForTest.modifiers && string.contains(resultsForTest.modifiers, 'SLOW'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001158 extraExpectations.push('SLOW');
1159
1160 // If there are no missing results or modifiers besides build
1161 // type, platform, or bug and the expectations are all extra
1162 // that is, extraExpectations - expectations = PASS,
1163 // include PASS as extra, since that means this line in
1164 // test_expectations can be deleted..
1165 if (!missingExpectations.length && !(resultsForTest.modifiers && realModifiers(resultsForTest.modifiers))) {
1166 var extraPlusPass = extraExpectations.concat(['PASS']);
1167 if (extraPlusPass.sort().toString() == expectationsArray.slice(0).sort().toString())
1168 extraExpectations.push('PASS');
1169 }
1170
1171 }
1172
1173 resultsForTest.meetsExpectations = !missingExpectations.length && !extraExpectations.length;
1174 resultsForTest.missing = missingExpectations.sort().join(' ');
1175 resultsForTest.extra = extraExpectations.sort().join(' ');
1176}
1177
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001178var BUG_URL_PREFIX = '<a href="http://';
1179var BUG_URL_POSTFIX = '/$1">crbug.com/$1</a> ';
1180var WEBKIT_BUG_URL_POSTFIX = '/$1">webkit.org/b/$1</a> ';
1181var INTERNAL_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'b' + BUG_URL_POSTFIX;
1182var EXTERNAL_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'crbug.com' + BUG_URL_POSTFIX;
1183var WEBKIT_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'webkit.org/b' + WEBKIT_BUG_URL_POSTFIX;
1184
1185function htmlForBugs(bugs)
1186{
1187 bugs = bugs.replace(/crbug.com\/(\d+)(\ |$)/g, EXTERNAL_BUG_REPLACE_VALUE);
1188 bugs = bugs.replace(/webkit.org\/b\/(\d+)(\ |$)/g, WEBKIT_BUG_REPLACE_VALUE);
1189 return bugs;
1190}
1191
1192function linkHTMLToOpenWindow(url, text)
1193{
1194 return '<a href="' + url + '" target="_blank">' + text + '</a>';
1195}
1196
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001197// Returns whether the result for index'th result for testName on builder was
1198// a failure.
1199function isFailure(builder, testName, index)
1200{
1201 var currentIndex = 0;
1202 var rawResults = g_resultsByBuilder[builder].tests[testName].results;
1203 for (var i = 0; i < rawResults.length; i++) {
1204 currentIndex += rawResults[i][RLE.LENGTH];
1205 if (currentIndex > index)
1206 return isFailingResult(rawResults[i][RLE.VALUE]);
1207 }
1208 console.error('Index exceeds number of results: ' + index);
1209}
1210
1211// Returns an array of indexes for all builds where this test failed.
1212function indexesForFailures(builder, testName)
1213{
1214 var rawResults = g_resultsByBuilder[builder].tests[testName].results;
1215 var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
1216 var index = 0;
1217 var failures = [];
1218 for (var i = 0; i < rawResults.length; i++) {
1219 var numResults = rawResults[i][RLE.LENGTH];
1220 if (isFailingResult(rawResults[i][RLE.VALUE])) {
1221 for (var j = 0; j < numResults; j++)
1222 failures.push(index + j);
1223 }
1224 index += numResults;
1225 }
1226 return failures;
1227}
1228
1229// Returns the path to the failure log for this non-webkit test.
1230function pathToFailureLog(testName)
1231{
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001232 return '/steps/' + g_history.crossDashboardState.testType + '/logs/' + testName.split('.')[1]
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001233}
1234
1235function showPopupForBuild(e, builder, index, opt_testName)
1236{
1237 var html = '';
1238
1239 var time = g_resultsByBuilder[builder].secondsSinceEpoch[index];
1240 if (time) {
1241 var date = new Date(time * 1000);
1242 html += date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
1243 }
1244
1245 var buildNumber = g_resultsByBuilder[builder].buildNumbers[index];
1246 var master = builderMaster(builder);
1247 var buildBasePath = master.logPath(builder, buildNumber);
1248
1249 html += '<ul><li>' + linkHTMLToOpenWindow(buildBasePath, 'Build log') +
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +01001250 '</li><li>Blink: ' + ui.html.blinkRevisionLink(g_resultsByBuilder[builder], index) + '</li>';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001251
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +01001252 html += '</li><li>Chromium: ' + ui.html.chromiumRevisionLink(g_resultsByBuilder[builder], index) + '</li>';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001253
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +01001254 var chromeRevision = g_resultsByBuilder[builder].chromeRevision[index];
1255 if (chromeRevision && g_history.isLayoutTestResults()) {
1256 html += '<li><a href="' + TEST_RESULTS_BASE_PATH + currentBuilders()[builder] +
1257 '/' + chromeRevision + '/layout-test-results.zip">layout-test-results.zip</a></li>';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001258 }
1259
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001260 if (!g_history.isLayoutTestResults() && opt_testName && isFailure(builder, opt_testName, index))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001261 html += '<li>' + linkHTMLToOpenWindow(buildBasePath + pathToFailureLog(opt_testName), 'Failure log') + '</li>';
1262
1263 html += '</ul>';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001264 ui.popup.show(e.target, html);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001265}
1266
1267function htmlForTestResults(test)
1268{
1269 var html = '';
1270 var results = test.rawResults.concat();
1271 var times = test.rawTimes.concat();
1272 var builder = test.builder;
1273 var master = builderMaster(builder);
1274 var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
1275
1276 var indexToReplaceCurrentResult = -1;
1277 var indexToReplaceCurrentTime = -1;
1278 var currentResultArray, currentTimeArray, currentResult, innerHTML, resultString;
1279 for (var i = 0; i < buildNumbers.length; i++) {
1280 if (i > indexToReplaceCurrentResult) {
1281 currentResultArray = results.shift();
1282 if (currentResultArray) {
1283 currentResult = currentResultArray[RLE.VALUE];
1284 // Treat simplified diff failures as just text failures.
1285 if (currentResult == 'S')
1286 currentResult = 'F';
1287 indexToReplaceCurrentResult += currentResultArray[RLE.LENGTH];
1288 } else {
1289 currentResult = 'N';
1290 indexToReplaceCurrentResult += buildNumbers.length;
1291 }
1292 resultString = expectationsFileStringForResult(currentResult);
1293 }
1294
1295 if (i > indexToReplaceCurrentTime) {
1296 currentTimeArray = times.shift();
1297 var currentTime = 0;
1298 if (currentResultArray) {
1299 currentTime = currentTimeArray[RLE.VALUE];
1300 indexToReplaceCurrentTime += currentTimeArray[RLE.LENGTH];
1301 } else
1302 indexToReplaceCurrentTime += buildNumbers.length;
1303
1304 innerHTML = currentTime || '&nbsp;';
1305 }
1306
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001307 html += '<td title="' + (resultString || 'NO DATA') + '. Click for more info." class="results ' + currentResult +
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +01001308 '" onclick=\'showPopupForBuild(event, "' + builder + '",' + i + ',"' + test.test + '")\'>' + innerHTML;
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001309 }
1310 return html;
1311}
1312
1313function htmlForTestsWithExpectationsButNoFailures(builder)
1314{
1315 var tests = g_perBuilderWithExpectationsButNoFailures[builder];
1316 var skippedPaths = g_perBuilderSkippedPaths[builder];
1317 var showUnexpectedPassesLink = linkHTMLToToggleState('showUnexpectedPasses', 'tests that have not failed in last ' + g_resultsByBuilder[builder].buildNumbers.length + ' runs');
1318 var showSkippedLink = linkHTMLToToggleState('showSkipped', 'skipped tests in TestExpectations');
1319
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001320 var html = '';
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +01001321 if (g_history.isLayoutTestResults() && (tests.length || skippedPaths.length)) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001322 var buildInfo = platformAndBuildType(builder);
1323 html += '<h2 style="display:inline-block">Expectations for ' + buildInfo.platform + '-' + buildInfo.buildType + '</h2> ';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001324 if (!g_history.dashboardSpecificState.showUnexpectedPasses && tests.length)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001325 html += showUnexpectedPassesLink;
1326 html += ' ';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001327 if (!g_history.dashboardSpecificState.showSkipped && skippedPaths.length)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001328 html += showSkippedLink;
1329 }
1330
1331 var open = '<div onclick="selectContents(this)">';
1332
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001333 if (g_history.dashboardSpecificState.showUnexpectedPasses && tests.length) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001334 html += '<div id="passing-tests">' + showUnexpectedPassesLink;
1335 for (var i = 0; i < tests.length; i++)
1336 html += open + tests[i].test + '</div>';
1337 html += '</div>';
1338 }
1339
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001340 if (g_history.dashboardSpecificState.showSkipped && skippedPaths.length)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001341 html += '<div id="skipped-tests">' + showSkippedLink + open + skippedPaths.join('</div>' + open) + '</div></div>';
1342 return html + '<br>';
1343}
1344
1345// Returns whether we should exclude test results from the test table.
1346function shouldHideTest(testResult)
1347{
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +01001348 // For non-layout tests, we always show everything.
1349 if (!g_history.isLayoutTestResults())
1350 return false;
1351
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001352 if (testResult.isWontFixSkip)
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001353 return !g_history.dashboardSpecificState.showWontFixSkip;
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001354
1355 if (testResult.isFlaky)
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001356 return !g_history.dashboardSpecificState.showFlaky;
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001357
1358 if (isSlowTest(testResult))
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001359 return !g_history.dashboardSpecificState.showSlow;
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001360
1361 if (testResult.meetsExpectations)
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001362 return !g_history.dashboardSpecificState.showCorrectExpectations;
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001363
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001364 return !g_history.dashboardSpecificState.showWrongExpectations;
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001365}
1366
1367// Sets the browser's selection to the element's contents.
1368function selectContents(element)
1369{
1370 window.getSelection().selectAllChildren(element);
1371}
1372
1373function createBugHTML(test)
1374{
1375 var symptom = test.isFlaky ? 'flaky' : 'failing';
1376 var title = encodeURIComponent('Layout Test ' + test.test + ' is ' + symptom);
1377 var description = encodeURIComponent('The following layout test is ' + symptom + ' on ' +
1378 '[insert platform]\n\n' + test.test + '\n\nProbable cause:\n\n' +
1379 '[insert probable cause]');
1380
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +01001381 url = 'https://code.google.com/p/chromium/issues/entry?template=Layout%20Test%20Failure&summary=' + title + '&comment=' + description;
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001382 return '<a href="' + url + '" class="file-bug">FILE BUG</a>';
1383}
1384
1385function isCrossBuilderView()
1386{
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001387 return g_history.dashboardSpecificState.tests || g_history.dashboardSpecificState.result || g_history.dashboardSpecificState.expectationsUpdate;
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001388}
1389
1390function tableHeaders(opt_getAll)
1391{
1392 var headers = [];
1393 if (isCrossBuilderView() || opt_getAll)
1394 headers.push('builder');
1395
1396 if (!isCrossBuilderView() || opt_getAll)
1397 headers.push('test');
1398
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001399 if (g_history.isLayoutTestResults() || opt_getAll)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001400 headers.push('bugs', 'modifiers', 'expectations');
1401
1402 headers.push('slowest run', 'flakiness (numbers are runtimes in seconds)');
1403 return headers;
1404}
1405
1406function htmlForSingleTestRow(test)
1407{
1408 if (!isCrossBuilderView() && shouldHideTest(test)) {
1409 // The innerHTML call is considerably faster if we exclude the rows for
1410 // items we're not showing than if we hide them using display:none.
1411 // For the crossBuilderView, we want to show all rows the user is
1412 // explicitly listing tests to view.
1413 return '';
1414 }
1415
1416 var headers = tableHeaders();
1417 var html = '';
1418 for (var i = 0; i < headers.length; i++) {
1419 var header = headers[i];
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001420 if (string.startsWith(header, 'test') || string.startsWith(header, 'builder')) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001421 // If isCrossBuilderView() is true, we're just viewing a single test
1422 // with results for many builders, so the first column is builder names
1423 // instead of test paths.
1424 var testCellClassName = 'test-link' + (isCrossBuilderView() ? ' builder-name' : '');
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001425 var testCellHTML = isCrossBuilderView() ? test.builder : '<span class="link" onclick="g_history.setQueryParameter(\'tests\',\'' + test.test +'\');">' + test.test + '</span>';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001426
1427 html += '<tr><td class="' + testCellClassName + '">' + testCellHTML;
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001428 } else if (string.startsWith(header, 'bugs'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001429 html += '<td class=options-container>' + (test.bugs ? htmlForBugs(test.bugs) : createBugHTML(test));
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001430 else if (string.startsWith(header, 'modifiers'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001431 html += '<td class=options-container>' + test.modifiers;
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001432 else if (string.startsWith(header, 'expectations'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001433 html += '<td class=options-container>' + test.expectations;
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001434 else if (string.startsWith(header, 'slowest'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001435 html += '<td>' + (test.slowestTime ? test.slowestTime + 's' : '');
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001436 else if (string.startsWith(header, 'flakiness'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001437 html += htmlForTestResults(test);
1438 }
1439 return html;
1440}
1441
1442function sortColumnFromTableHeader(headerText)
1443{
1444 return headerText.split(' ', 1)[0];
1445}
1446
1447function htmlForTableColumnHeader(headerName, opt_fillColSpan)
1448{
1449 // Use the first word of the header title as the sortkey
1450 var thisSortValue = sortColumnFromTableHeader(headerName);
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001451 var arrowHTML = thisSortValue == g_history.dashboardSpecificState.sortColumn ?
1452 '<span class=' + g_history.dashboardSpecificState.sortOrder + '>' + (g_history.dashboardSpecificState.sortOrder == FORWARD ? '&uarr;' : '&darr;' ) + '</span>' : '';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001453 return '<th sortValue=' + thisSortValue +
1454 // Extend last th through all the rest of the columns.
1455 (opt_fillColSpan ? ' colspan=10000' : '') +
1456 // Extra span here is so flex boxing actually centers.
1457 // There's probably a better way to do this with CSS only though.
1458 '><div class=table-header-content><span></span>' + arrowHTML +
1459 '<span class=header-text>' + headerName + '</span>' + arrowHTML + '</div></th>';
1460}
1461
1462function htmlForTestTable(rowsHTML, opt_excludeHeaders)
1463{
1464 var html = '<table class=test-table>';
1465 if (!opt_excludeHeaders) {
1466 html += '<thead><tr>';
1467 var headers = tableHeaders();
1468 for (var i = 0; i < headers.length; i++)
1469 html += htmlForTableColumnHeader(headers[i], i == headers.length - 1);
1470 html += '</tr></thead>';
1471 }
1472 return html + '<tbody>' + rowsHTML + '</tbody></table>';
1473}
1474
1475function appendHTML(html)
1476{
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001477 // InnerHTML to a div that's not in the document. This is
1478 // ~300ms faster in Safari 4 and Chrome 4 on mac.
1479 var div = document.createElement('div');
1480 div.innerHTML = html;
1481 document.body.appendChild(div);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001482 postHeightChangedMessage();
1483}
1484
1485function alphanumericCompare(column, reverse)
1486{
1487 return reversibleCompareFunction(function(a, b) {
1488 // Put null entries at the bottom
1489 var a = a[column] ? String(a[column]) : 'z';
1490 var b = b[column] ? String(b[column]) : 'z';
1491
1492 if (a < b)
1493 return -1;
1494 else if (a == b)
1495 return 0;
1496 else
1497 return 1;
1498 }, reverse);
1499}
1500
1501function numericSort(column, reverse)
1502{
1503 return reversibleCompareFunction(function(a, b) {
1504 a = parseFloat(a[column]);
1505 b = parseFloat(b[column]);
1506 return a - b;
1507 }, reverse);
1508}
1509
1510function reversibleCompareFunction(compare, reverse)
1511{
1512 return function(a, b) {
1513 return compare(reverse ? b : a, reverse ? a : b);
1514 };
1515}
1516
1517function changeSort(e)
1518{
1519 var target = e.currentTarget;
1520 e.preventDefault();
1521
1522 var sortValue = target.getAttribute('sortValue');
1523 while (target && target.tagName != 'TABLE')
1524 target = target.parentNode;
1525
1526 var sort = 'sortColumn';
1527 var orderKey = 'sortOrder';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001528 if (sortValue == g_history.dashboardSpecificState[sort] && g_history.dashboardSpecificState[orderKey] == FORWARD)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001529 order = BACKWARD;
1530 else
1531 order = FORWARD;
1532
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001533 g_history.setQueryParameter(sort, sortValue, orderKey, order);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001534}
1535
1536function sortTests(tests, column, order)
1537{
1538 var resultsProperty, sortFunctionGetter;
1539 if (column == 'flakiness') {
1540 sortFunctionGetter = numericSort;
1541 resultsProperty = 'flips';
1542 } else if (column == 'slowest') {
1543 sortFunctionGetter = numericSort;
1544 resultsProperty = 'slowestTime';
1545 } else {
1546 sortFunctionGetter = alphanumericCompare;
1547 resultsProperty = column;
1548 }
1549
1550 tests.sort(sortFunctionGetter(resultsProperty, order == BACKWARD));
1551}
1552
1553// Sorts a space separated expectations string in alphanumeric order.
1554// @param {string} str The expectations string.
1555// @return {string} The sorted string.
1556function sortExpectationsString(str)
1557{
1558 return str.split(' ').sort().join(' ');
1559}
1560
1561function addUpdate(testsNeedingUpdate, test, builderName, missing, extra)
1562{
1563 if (!testsNeedingUpdate[test])
1564 testsNeedingUpdate[test] = {};
1565
1566 var buildInfo = platformAndBuildType(builderName);
1567 var builder = buildInfo.platform + ' ' + buildInfo.buildType;
1568 if (!testsNeedingUpdate[test][builder])
1569 testsNeedingUpdate[test][builder] = {};
1570
1571 if (missing)
1572 testsNeedingUpdate[test][builder].missing = sortExpectationsString(missing);
1573
1574 if (extra)
1575 testsNeedingUpdate[test][builder].extra = sortExpectationsString(extra);
1576}
1577
1578
1579// From a string of modifiers, returns a string of modifiers that
1580// are for real result changes, like SLOW, and excludes modifiers
1581// that specificy things like platform, build_type, bug.
1582// @param {string} modifierString String containing all modifiers.
1583// @return {string} String containing only modifiers that effect the results.
1584function realModifiers(modifierString)
1585{
1586 var modifiers = modifierString.split(' ');;
1587 return modifiers.filter(function(modifier) {
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001588 if (modifier in BUILD_TYPES || string.startsWith(modifier, 'BUG'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001589 return false;
1590
1591 var matchesPlatformOrUnion = false;
1592 traversePlatformsTree(function(platform, platformName) {
1593 if (matchesPlatformOrUnion)
1594 return;
1595
1596 if (platform.fallbackPlatforms) {
1597 platform.fallbackPlatforms.forEach(function(fallbackPlatform) {
1598 if (matchesPlatformOrUnion)
1599 return;
1600
1601 var fallbackPlatformObject = platformObjectForName(fallbackPlatform);
1602 if (!fallbackPlatformObject.platformModifierUnions)
1603 return;
1604
1605 matchesPlatformOrUnion = modifier in fallbackPlatformObject.subPlatforms || modifier in fallbackPlatformObject.platformModifierUnions;
1606 });
1607 }
1608 });
1609
1610 return !matchesPlatformOrUnion;
1611 }).join(' ');
1612}
1613
1614function generatePageForExpectationsUpdate()
1615{
1616 // Always show all runs when auto-updating expectations.
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001617 if (!g_history.crossDashboardState.showAllRuns)
1618 g_history.setQueryParameter('showAllRuns', true);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001619
1620 processTestRunsForAllBuilders();
1621 var testsNeedingUpdate = {};
1622 for (var test in g_testToResultsMap) {
1623 var results = g_testToResultsMap[test];
1624 for (var i = 0; i < results.length; i++) {
1625 var thisResult = results[i];
1626
1627 if (!thisResult.missing && !thisResult.extra)
1628 continue;
1629
1630 var allPassesOrNoDatas = thisResult.rawResults.filter(function (x) { return x[1] != "P" && x[1] != "N"; }).length == 0;
1631
1632 if (allPassesOrNoDatas)
1633 continue;
1634
1635 addUpdate(testsNeedingUpdate, test, thisResult.builder, thisResult.missing, thisResult.extra);
1636 }
1637 }
1638
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001639 for (var builder in currentBuilders()) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001640 var tests = g_perBuilderWithExpectationsButNoFailures[builder]
1641 for (var i = 0; i < tests.length; i++) {
1642 // Anything extra in this case is what is listed in expectations
1643 // plus modifiers other than bug, platform, build type.
1644 var modifiers = realModifiers(tests[i].modifiers);
1645 var extras = tests[i].expectations;
1646 extras += modifiers ? ' ' + modifiers : '';
1647 addUpdate(testsNeedingUpdate, tests[i].test, builder, null, extras);
1648 }
1649 }
1650
1651 // Get the keys in alphabetical order, so it is easy to process groups
1652 // of tests.
1653 var keys = Object.keys(testsNeedingUpdate).sort();
1654 showUpdateInfoForTest(testsNeedingUpdate, keys);
1655}
1656
1657// Show the test results and the json for differing expectations, and
1658// allow the user to include or exclude this update.
1659//
1660// @param {Object} testsNeedingUpdate Tests that need updating.
1661// @param {Array.<string>} keys Keys into the testNeedingUpdate object.
1662function showUpdateInfoForTest(testsNeedingUpdate, keys)
1663{
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001664 var test = keys[g_history.dashboardSpecificState.updateIndex];
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001665 document.body.innerHTML = '';
1666
1667 // FIXME: Make this DOM creation less verbose.
1668 var index = document.createElement('div');
1669 index.style.cssFloat = 'right';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001670 index.textContent = (g_history.dashboardSpecificState.updateIndex + 1) + ' of ' + keys.length + ' tests';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001671 document.body.appendChild(index);
1672
1673 var buttonRegion = document.createElement('div');
1674 var includeBtn = document.createElement('input');
1675 includeBtn.type = 'button';
1676 includeBtn.value = 'include selected';
1677 includeBtn.addEventListener('click', partial(handleUpdate, testsNeedingUpdate, keys), false);
1678 buttonRegion.appendChild(includeBtn);
1679
1680 var previousBtn = document.createElement('input');
1681 previousBtn.type = 'button';
1682 previousBtn.value = 'previous';
1683 previousBtn.addEventListener('click',
1684 function() {
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001685 setUpdateIndex(g_history.dashboardSpecificState.updateIndex - 1, testsNeedingUpdate, keys);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001686 },
1687 false);
1688 buttonRegion.appendChild(previousBtn);
1689
1690 var nextBtn = document.createElement('input');
1691 nextBtn.type = 'button';
1692 nextBtn.value = 'next';
1693 nextBtn.addEventListener('click', partial(nextUpdate, testsNeedingUpdate, keys), false);
1694 buttonRegion.appendChild(nextBtn);
1695
1696 var doneBtn = document.createElement('input');
1697 doneBtn.type = 'button';
1698 doneBtn.value = 'done';
1699 doneBtn.addEventListener('click', finishUpdate, false);
1700 buttonRegion.appendChild(doneBtn);
1701
1702 document.body.appendChild(buttonRegion);
1703
1704 var updates = testsNeedingUpdate[test];
1705 var checkboxes = document.createElement('div');
1706 for (var builder in updates) {
1707 // Create a checkbox for each builder.
1708 var checkboxRegion = document.createElement('div');
1709 var checkbox = document.createElement('input');
1710 checkbox.type = 'checkbox';
1711 checkbox.id = builder;
1712 checkbox.checked = true;
1713 checkboxRegion.appendChild(checkbox);
1714 checkboxRegion.appendChild(document.createTextNode(builder + ' : ' + JSON.stringify(updates[builder])));
1715 checkboxes.appendChild(checkboxRegion);
1716 }
1717 document.body.appendChild(checkboxes);
1718
1719 var div = document.createElement('div');
1720 div.innerHTML = htmlForIndividualTestOnAllBuildersWithResultsLinks(test);
1721 document.body.appendChild(div);
1722 appendExpectations();
1723}
1724
1725
1726// When the user has finished selecting expectations to update, provide them
1727// with json to copy over.
1728function finishUpdate()
1729{
1730 document.body.innerHTML = 'The next step is to copy the output below ' +
1731 'into a local file and save it. Then, run<br><code>python ' +
1732 'src/webkit/tools/layout_tests/webkitpy/layout_tests/update_expectat' +
1733 'ions_from_dashboard.py path/to/local/file</code><br>in order to ' +
1734 'update the expectations file.<br><textarea id="results" '+
1735 'style="width:600px;height:600px;"> ' +
1736 JSON.stringify(g_confirmedTests) + '</textarea>';
1737 results.focus();
1738 document.execCommand('SelectAll');
1739}
1740
1741// Handle user click on "include selected" button.
1742// Includes the tests that are selected and exclude the rest.
1743// @param {Object} testsNeedingUpdate Tests that need updating.
1744// @param {Array.<string>} keys Keys into the testNeedingUpdate object.
1745function handleUpdate(testsNeedingUpdate, keys)
1746{
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001747 var test = keys[g_history.dashboardSpecificState.updateIndex];
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001748 var updates = testsNeedingUpdate[test];
1749 for (var builder in updates) {
1750 // Add included tests, and delete excluded tests if
1751 // they were previously included.
1752 if ($(builder).checked) {
1753 if (!g_confirmedTests[test])
1754 g_confirmedTests[test] = {};
1755 g_confirmedTests[test][builder] = testsNeedingUpdate[test][builder];
1756 } else if (g_confirmedTests[test] && g_confirmedTests[test][builder]) {
1757 delete g_confirmedTests[test][builder];
1758 if (!Object.keys(g_confirmedTests[test]).length)
1759 delete g_confirmedTests[test];
1760 }
1761 }
1762 nextUpdate(testsNeedingUpdate, keys);
1763}
1764
1765
1766// Move to the next item to update.
1767// @param {Object} testsNeedingUpdate Tests that need updating.
1768// @param {Array.<string>} keys Keys into the testNeedingUpdate object.
1769function nextUpdate(testsNeedingUpdate, keys)
1770{
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001771 setUpdateIndex(g_history.dashboardSpecificState.updateIndex + 1, testsNeedingUpdate, keys);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001772}
1773
1774
1775// Advance the index we are updating at. If we walk over the end
1776// or beginning, just loop.
1777// @param {string} newIndex The index into the keys to move to.
1778// @param {Object} testsNeedingUpdate Tests that need updating.
1779// @param {Array.<string>} keys Keys into the testNeedingUpdate object.
1780function setUpdateIndex(newIndex, testsNeedingUpdate, keys)
1781{
1782 if (newIndex == -1)
1783 newIndex = keys.length - 1;
1784 else if (newIndex == keys.length)
1785 newIndex = 0;
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001786 g_history.setQueryParameter("updateIndex", newIndex);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001787 showUpdateInfoForTest(testsNeedingUpdate, keys);
1788}
1789
1790function htmlForIndividualTestOnAllBuilders(test)
1791{
1792 processTestRunsForAllBuilders();
1793
1794 var testResults = g_testToResultsMap[test];
1795 if (!testResults)
1796 return '<div class="not-found">Test not found. Either it does not exist, is skipped or passes on all platforms.</div>';
1797
1798 var html = '';
1799 var shownBuilders = [];
1800 for (var j = 0; j < testResults.length; j++) {
1801 shownBuilders.push(testResults[j].builder);
1802 html += htmlForSingleTestRow(testResults[j]);
1803 }
1804
1805 var skippedBuilders = []
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001806 for (builder in currentBuilders()) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001807 if (shownBuilders.indexOf(builder) == -1)
1808 skippedBuilders.push(builder);
1809 }
1810
1811 var skippedBuildersHtml = '';
1812 if (skippedBuilders.length) {
1813 skippedBuildersHtml = '<div>The following builders either don\'t run this test (e.g. it\'s skipped) or all runs passed:</div>' +
1814 '<div class=skipped-builder-list><div class=skipped-builder>' + skippedBuilders.join('</div><div class=skipped-builder>') + '</div></div>';
1815 }
1816
1817 return htmlForTestTable(html) + skippedBuildersHtml;
1818}
1819
1820function htmlForIndividualTestOnAllBuildersWithResultsLinks(test)
1821{
1822 processTestRunsForAllBuilders();
1823
1824 var testResults = g_testToResultsMap[test];
1825 var html = '';
1826 html += htmlForIndividualTestOnAllBuilders(test);
1827
1828 html += '<div class=expectations test=' + test + '><div>' +
1829 linkHTMLToToggleState('showExpectations', 'results')
1830
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001831 if (g_history.isLayoutTestResults() || g_history.isGPUTestResults()) {
1832 if (g_history.isLayoutTestResults())
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001833 html += ' | ' + linkHTMLToToggleState('showLargeExpectations', 'large thumbnails');
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001834 html += ' | <b>Only shows actual results/diffs from the most recent *failure* on each bot.</b>';
1835 } else {
1836 html += ' | <span>Results height:<input ' +
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001837 'onchange="g_history.setQueryParameter(\'resultsHeight\',this.value)" value="' +
1838 g_history.dashboardSpecificState.resultsHeight + '" style="width:2.5em">px</span>';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001839 }
1840 html += '</div></div>';
1841 return html;
1842}
1843
1844function getExpectationsContainer(expectationsContainers, parentContainer, expectationsType)
1845{
1846 if (!expectationsContainers[expectationsType]) {
1847 var container = document.createElement('div');
1848 container.className = 'expectations-container';
1849 parentContainer.appendChild(container);
1850 expectationsContainers[expectationsType] = container;
1851 }
1852 return expectationsContainers[expectationsType];
1853}
1854
1855function ensureTrailingSlash(path)
1856{
1857 if (path.match(/\/$/))
1858 return path;
1859 return path + '/';
1860}
1861
1862function maybeAddPngChecksum(expectationDiv, pngUrl)
1863{
1864 // pngUrl gets served from the browser cache since we just loaded it in an
1865 // <img> tag.
1866 loader.request(pngUrl,
1867 function(xhr) {
1868 // Convert the first 2k of the response to a byte string.
1869 var bytes = xhr.responseText.substring(0, 2048);
1870 for (var position = 0; position < bytes.length; ++position)
1871 bytes[position] = bytes[position] & 0xff;
1872
1873 // Look for the comment.
1874 var commentKey = 'tEXtchecksum\x00';
1875 var checksumPosition = bytes.indexOf(commentKey);
1876 if (checksumPosition == -1)
1877 return;
1878
1879 var checksum = bytes.substring(checksumPosition + commentKey.length, checksumPosition + commentKey.length + 32);
1880 var checksumContainer = document.createElement('span');
1881 checksumContainer.innerText = 'Embedded checksum: ' + checksum;
1882 checksumContainer.setAttribute('class', 'pngchecksum');
1883 expectationDiv.parentNode.appendChild(checksumContainer);
1884 },
1885 function(xhr) {},
1886 true);
1887}
1888
1889// Adds a specific expectation. If it's an image, it's only added on the
1890// image's onload handler. If it's a text file, then a script tag is appended
1891// as a hack to see if the file 404s (necessary since it's cross-domain).
1892// Once all the expectations for a specific type have loaded or errored
1893// (e.g. all the text results), then we go through and identify which platform
1894// uses which expectation.
1895//
1896// @param {Object} expectationsContainers Map from expectations type to
1897// container DIV.
1898// @param {Element} parentContainer Container element for
1899// expectationsContainer divs.
1900// @param {string} platform Platform string. Empty string for non-platform
1901// specific expectations.
1902// @param {string} path Relative path to the expectation.
1903// @param {string} base Base path for the expectation URL.
1904// @param {string} opt_builder Builder whose actual results this expectation
1905// points to.
1906// @param {string} opt_suite "virtual suite" that the test belongs to, if any.
1907function addExpectationItem(expectationsContainers, parentContainer, platform, path, base, opt_builder, opt_suite)
1908{
1909 var parts = path.split('.')
1910 var fileExtension = parts[parts.length - 1];
1911 if (fileExtension == 'html')
1912 fileExtension = 'txt';
1913
1914 var container = getExpectationsContainer(expectationsContainers, parentContainer, fileExtension);
1915 var isImage = path.match(/\.png$/);
1916
1917 // FIXME: Stop using script tags once all the places we pull from support CORS.
1918 var platformPart = platform ? ensureTrailingSlash(platform) : '';
1919 var suitePart = opt_suite ? ensureTrailingSlash(opt_suite) : '';
1920
1921 var childContainer = document.createElement('span');
1922 childContainer.className = 'unloaded';
1923
1924 var appendExpectationsItem = function(item) {
1925 childContainer.appendChild(expectationsTitle(platformPart + suitePart, path, opt_builder));
1926 childContainer.className = 'expectations-item';
1927 item.className = 'expectation ' + fileExtension;
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001928 if (g_history.dashboardSpecificState.showLargeExpectations)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001929 item.className += ' large';
1930 childContainer.appendChild(item);
1931 handleFinishedLoadingExpectations(container);
1932 };
1933
1934 var url = base + platformPart + path;
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001935 if (isImage || !string.startsWith(base, 'http://svn.webkit.org')) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001936 var dummyNode = document.createElement(isImage ? 'img' : 'script');
1937 dummyNode.src = url;
1938 dummyNode.onload = function() {
1939 var item;
1940 if (isImage) {
1941 item = dummyNode;
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00001942 if (string.startsWith(base, 'http://svn.webkit.org'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001943 maybeAddPngChecksum(item, url);
1944 } else {
1945 item = document.createElement('iframe');
1946 item.src = url;
1947 }
1948 appendExpectationsItem(item);
1949 }
1950 dummyNode.onerror = function() {
1951 childContainer.parentNode.removeChild(childContainer);
1952 handleFinishedLoadingExpectations(container);
1953 }
1954
1955 // Append script elements now so that they load. Images load without being
1956 // appended to the DOM.
1957 if (!isImage)
1958 childContainer.appendChild(dummyNode);
1959 } else {
1960 loader.request(url,
1961 function(xhr) {
1962 var item = document.createElement('pre');
1963 item.innerText = xhr.responseText;
1964 appendExpectationsItem(item);
1965 },
1966 function(xhr) {/* Do nothing on errors since they're expected */});
1967 }
1968
1969 container.appendChild(childContainer);
1970}
1971
1972
1973// Identifies which expectations are used on which platform once all the
1974// expectations of a given type have loaded (e.g. the container for png
1975// expectations for this test had no child elements with the class
1976// "unloaded").
1977//
1978// @param {string} container Element containing the expectations for a given
1979// test and a given type (e.g. png).
1980function handleFinishedLoadingExpectations(container)
1981{
1982 if (container.getElementsByClassName('unloaded').length)
1983 return;
1984
1985 var titles = container.getElementsByClassName('expectations-title');
1986 for (var platform in g_fallbacksMap) {
1987 var fallbacks = g_fallbacksMap[platform];
1988 var winner = null;
1989 var winningIndex = -1;
1990 for (var i = 0; i < titles.length; i++) {
1991 var title = titles[i];
1992
1993 if (!winner && title.platform == "") {
1994 winner = title;
1995 continue;
1996 }
1997
1998 var rawPlatform = title.platform && title.platform.replace('platform/', '');
1999 for (var j = 0; j < fallbacks.length; j++) {
2000 if ((winningIndex == -1 || winningIndex > j) && rawPlatform == fallbacks[j]) {
2001 winningIndex = j;
2002 winner = title;
2003 break;
2004 }
2005 }
2006 }
2007 if (winner)
2008 winner.getElementsByClassName('platforms')[0].innerHTML += '<div class=used-platform>' + platform + '</div>';
2009 else {
2010 console.log('No expectations identified for this test. This means ' +
2011 'there is a logic bug in the dashboard for which expectations a ' +
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +01002012 'platform uses or src.chromium.org is giving 5XXs.');
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002013 }
2014 }
2015
2016 consolidateUsedPlatforms(container);
2017}
2018
2019// Consolidate platforms when all sub-platforms for a given platform are represented.
2020// e.g., if all of the WIN- platforms are there, replace them with just WIN.
2021function consolidateUsedPlatforms(container)
2022{
2023 var allPlatforms = Object.keys(g_fallbacksMap);
2024
2025 var platformElements = container.getElementsByClassName('platforms');
2026 for (var i = 0, platformsLength = platformElements.length; i < platformsLength; i++) {
2027 var usedPlatforms = platformElements[i].getElementsByClassName('used-platform');
2028 if (!usedPlatforms.length)
2029 continue;
2030
2031 var platforms = {};
2032 platforms['MAC'] = {};
2033 platforms['WIN'] = {};
2034 platforms['LINUX'] = {};
2035 allPlatforms.forEach(function(platform) {
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002036 if (string.startsWith(platform, 'MAC'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002037 platforms['MAC'][platform] = 1;
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002038 else if (string.startsWith(platform, 'WIN'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002039 platforms['WIN'][platform] = 1;
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002040 else if (string.startsWith(platform, 'LINUX'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002041 platforms['LINUX'][platform] = 1;
2042 });
2043
2044 for (var j = 0, usedPlatformsLength = usedPlatforms.length; j < usedPlatformsLength; j++) {
2045 for (var platform in platforms)
2046 delete platforms[platform][usedPlatforms[j].textContent];
2047 }
2048
2049 for (var platform in platforms) {
2050 if (!Object.keys(platforms[platform]).length) {
2051 var nodesToRemove = [];
2052 for (var j = 0, usedPlatformsLength = usedPlatforms.length; j < usedPlatformsLength; j++) {
2053 var usedPlatform = usedPlatforms[j];
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002054 if (string.startsWith(usedPlatform.textContent, platform))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002055 nodesToRemove.push(usedPlatform);
2056 }
2057
2058 nodesToRemove.forEach(function(element) { element.parentNode.removeChild(element); });
2059 platformElements[i].insertAdjacentHTML('afterBegin', '<div class=used-platform>' + platform + '</div>');
2060 }
2061 }
2062 }
2063}
2064
2065function addExpectations(expectationsContainers, container, base,
2066 platform, text, png, reftest_html_file, reftest_mismatch_html_file, suite)
2067{
2068 var builder = '';
2069 addExpectationItem(expectationsContainers, container, platform, text, base, builder, suite);
2070 addExpectationItem(expectationsContainers, container, platform, png, base, builder, suite);
2071 addExpectationItem(expectationsContainers, container, platform, reftest_html_file, base, builder, suite);
2072 addExpectationItem(expectationsContainers, container, platform, reftest_mismatch_html_file, base, builder, suite);
2073}
2074
2075function expectationsTitle(platform, path, builder)
2076{
2077 var header = document.createElement('h3');
2078 header.className = 'expectations-title';
2079
2080 var innerHTML;
2081 if (builder) {
2082 var resultsType;
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002083 if (string.endsWith(path, '-crash-log.txt'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002084 resultsType = 'STACKTRACE';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002085 else if (string.endsWith(path, '-actual.txt') || string.endsWith(path, '-actual.png'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002086 resultsType = 'ACTUAL RESULTS';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002087 else if (string.endsWith(path, '-wdiff.html'))
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002088 resultsType = 'WDIFF';
2089 else
2090 resultsType = 'DIFF';
2091
2092 innerHTML = resultsType + ': ' + builder;
2093 } else if (platform === "") {
2094 var parts = path.split('/');
2095 innerHTML = parts[parts.length - 1];
2096 } else
2097 innerHTML = platform || path;
2098
2099 header.innerHTML = '<div class=title>' + innerHTML +
2100 '</div><div style="float:left">&nbsp;</div>' +
2101 '<div class=platforms style="float:right"></div>';
2102 header.platform = platform;
2103 return header;
2104}
2105
2106function loadExpectations(expectationsContainer)
2107{
2108 var test = expectationsContainer.getAttribute('test');
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002109 if (g_history.isLayoutTestResults())
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002110 loadExpectationsLayoutTests(test, expectationsContainer);
2111 else {
2112 var results = g_testToResultsMap[test];
2113 for (var i = 0; i < results.length; i++)
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002114 if (g_history.isGPUTestResults())
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002115 loadGPUResultsForBuilder(results[i].builder, test, expectationsContainer);
2116 else
2117 loadNonWebKitResultsForBuilder(results[i].builder, test, expectationsContainer);
2118 }
2119}
2120
2121function gpuResultsPath(chromeRevision, builder)
2122{
2123 return chromeRevision + '_' + builder.replace(/[^A-Za-z0-9]+/g, '_');
2124}
2125
2126function loadGPUResultsForBuilder(builder, test, expectationsContainer)
2127{
2128 var container = document.createElement('div');
2129 container.className = 'expectations-container';
2130 container.innerHTML = '<div><b>' + builder + '</b></div>';
2131 expectationsContainer.appendChild(container);
2132
2133 var failureIndex = indexesForFailures(builder, test)[0];
2134
2135 var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndex];
2136 var pathToLog = builderMaster(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
2137
2138 var chromeRevision = g_resultsByBuilder[builder].chromeRevision[failureIndex];
2139 var resultsUrl = GPU_RESULTS_BASE_PATH + gpuResultsPath(chromeRevision, builder);
2140 var filename = test.split(/\./)[1] + '.png';
2141
2142 appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
2143 appendNonWebKitResults(container, resultsUrl + '/gen/' + filename, 'gpu-test-results', 'Generated');
2144 appendNonWebKitResults(container, resultsUrl + '/ref/' + filename, 'gpu-test-results', 'Reference');
2145 appendNonWebKitResults(container, resultsUrl + '/diff/' + filename, 'gpu-test-results', 'Diff');
2146}
2147
2148function loadNonWebKitResultsForBuilder(builder, test, expectationsContainer)
2149{
2150 var failureIndexes = indexesForFailures(builder, test);
2151 var container = document.createElement('div');
2152 container.innerHTML = '<div><b>' + builder + '</b></div>';
2153 expectationsContainer.appendChild(container);
2154 for (var i = 0; i < failureIndexes.length; i++) {
2155 // FIXME: This doesn't seem to work anymore. Did the paths change?
2156 // Once that's resolved, see if we need to try each GTEST_MODIFIERS prefix as well.
2157 var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndexes[i]];
2158 var pathToLog = builderMaster(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
2159 appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
2160 }
2161}
2162
2163function appendNonWebKitResults(container, url, itemClassName, opt_title)
2164{
2165 // Use a script tag to detect whether the URL 404s.
2166 // Need to use a script tag since the URL is cross-domain.
2167 var dummyNode = document.createElement('script');
2168 dummyNode.src = url;
2169
2170 dummyNode.onload = function() {
2171 var item = document.createElement('iframe');
2172 item.src = dummyNode.src;
2173 item.className = itemClassName;
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002174 item.style.height = g_history.dashboardSpecificState.resultsHeight + 'px';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002175
2176 if (opt_title) {
2177 var childContainer = document.createElement('div');
2178 childContainer.style.display = 'inline-block';
2179 var title = document.createElement('div');
2180 title.textContent = opt_title;
2181 childContainer.appendChild(title);
2182 childContainer.appendChild(item);
2183 container.replaceChild(childContainer, dummyNode);
2184 } else
2185 container.replaceChild(item, dummyNode);
2186 }
2187 dummyNode.onerror = function() {
2188 container.removeChild(dummyNode);
2189 }
2190
2191 container.appendChild(dummyNode);
2192}
2193
2194function buildInfoForRevision(builder, revision)
2195{
Torne (Richard Coles)e5249552013-05-15 11:35:13 +01002196 var revisions = g_resultsByBuilder[builder][BLINK_REVISION_KEY];
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002197 var revisionStart = 0, revisionEnd = 0, buildNumber = 0;
2198 for (var i = 0; i < revisions.length; i++) {
2199 if (revision > revisions[i]) {
2200 revisionStart = revisions[i - 1];
2201 revisionEnd = revisions[i];
2202 buildNumber = g_resultsByBuilder[builder].buildNumbers[i - 1];
2203 break;
2204 }
2205 }
2206
2207 if (revisionEnd)
2208 revisionEnd++;
2209 else
2210 revisionEnd = '';
2211
2212 return {revisionStart: revisionStart, revisionEnd: revisionEnd, buildNumber: buildNumber};
2213}
2214
2215function lookupVirtualTestSuite(test) {
2216 for (var suite in VIRTUAL_SUITES) {
2217 if (test.indexOf(suite) != -1)
2218 return suite;
2219 }
2220 return '';
2221}
2222
2223function baseTest(test, suite) {
2224 base = VIRTUAL_SUITES[suite];
2225 return base ? test.replace(suite, base) : test;
2226}
2227
2228function loadBaselinesForTest(expectationsContainers, expectationsContainer, test) {
2229 var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
2230 var text = testWithoutSuffix + "-expected.txt";
2231 var png = testWithoutSuffix + "-expected.png";
2232 var reftest_html_file = testWithoutSuffix + "-expected.html";
2233 var reftest_mismatch_html_file = testWithoutSuffix + "-expected-mismatch.html";
2234 var suite = lookupVirtualTestSuite(test);
2235
2236 if (!suite)
2237 addExpectationItem(expectationsContainers, expectationsContainer, null, test, TEST_URL_BASE_PATH);
2238
2239 addExpectations(expectationsContainers, expectationsContainer,
2240 TEST_URL_BASE_PATH, '', text, png, reftest_html_file, reftest_mismatch_html_file, suite);
2241
2242 var fallbacks = allFallbacks();
2243 for (var i = 0; i < fallbacks.length; i++) {
2244 var fallback = 'platform/' + fallbacks[i];
2245 addExpectations(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH, fallback, text, png,
2246 reftest_html_file, reftest_mismatch_html_file, suite);
2247 }
2248
2249 if (suite)
2250 loadBaselinesForTest(expectationsContainers, expectationsContainer, baseTest(test, suite));
2251}
2252
2253function loadExpectationsLayoutTests(test, expectationsContainer)
2254{
2255 // Map from file extension to container div for expectations of that type.
2256 var expectationsContainers = {};
2257
2258 var revisionContainer = document.createElement('div');
2259 revisionContainer.textContent = "Showing results for: "
2260 expectationsContainer.appendChild(revisionContainer);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002261 loadBaselinesForTest(expectationsContainers, expectationsContainer, test);
2262
2263 var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
2264 var actualResultSuffixes = ['-actual.txt', '-actual.png', '-crash-log.txt', '-diff.txt', '-wdiff.html', '-diff.png'];
2265
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002266 for (var builder in currentBuilders()) {
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +01002267 var actualResultsBase = TEST_RESULTS_BASE_PATH + currentBuilders()[builder] + '/results/layout-test-results/';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002268
2269 for (var i = 0; i < actualResultSuffixes.length; i++) {
2270 addExpectationItem(expectationsContainers, expectationsContainer, null,
2271 testWithoutSuffix + actualResultSuffixes[i], actualResultsBase, builder);
2272 }
2273 }
2274
2275 // Add a clearing element so floated elements don't bleed out of their
2276 // containing block.
2277 var br = document.createElement('br');
2278 br.style.clear = 'both';
2279 expectationsContainer.appendChild(br);
2280}
2281
2282var g_allFallbacks;
2283
2284// Returns the reverse sorted, deduped list of all platform fallback
2285// directories.
2286function allFallbacks()
2287{
2288 if (!g_allFallbacks) {
2289 var holder = {};
2290 for (var platform in g_fallbacksMap) {
2291 var fallbacks = g_fallbacksMap[platform];
2292 for (var i = 0; i < fallbacks.length; i++)
2293 holder[fallbacks[i]] = 1;
2294 }
2295
2296 g_allFallbacks = [];
2297 for (var fallback in holder)
2298 g_allFallbacks.push(fallback);
2299
2300 g_allFallbacks.sort(function(a, b) {
2301 if (a == b)
2302 return 0;
2303 return a < b;
2304 });
2305 }
2306 return g_allFallbacks;
2307}
2308
2309function appendExpectations()
2310{
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002311 var expectations = g_history.dashboardSpecificState.showExpectations ? document.getElementsByClassName('expectations') : [];
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002312 // Loading expectations is *very* slow. Use a large timeout to avoid
2313 // totally hanging the renderer.
2314 performChunkedAction(expectations, function(chunk) {
2315 for (var i = 0, len = chunk.length; i < len; i++)
2316 loadExpectations(chunk[i]);
2317 postHeightChangedMessage();
2318
2319 }, hideLoadingUI, 10000);
2320}
2321
2322function hideLoadingUI()
2323{
2324 var loadingDiv = $('loading-ui');
2325 if (loadingDiv)
2326 loadingDiv.style.display = 'none';
2327 postHeightChangedMessage();
2328}
2329
2330function generatePageForIndividualTests(tests)
2331{
2332 console.log('Number of tests: ' + tests.length);
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002333 if (g_history.dashboardSpecificState.showChrome)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002334 appendHTML(htmlForNavBar());
2335 performChunkedAction(tests, function(chunk) {
2336 appendHTML(htmlForIndividualTests(chunk));
2337 }, appendExpectations, 500);
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002338 if (g_history.dashboardSpecificState.showChrome)
2339 $('tests-input').value = g_history.dashboardSpecificState.tests;
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002340}
2341
2342function performChunkedAction(tests, handleChunk, onComplete, timeout, opt_index) {
2343 var index = opt_index || 0;
2344 setTimeout(function() {
2345 var chunk = Array.prototype.slice.call(tests, index * CHUNK_SIZE, (index + 1) * CHUNK_SIZE);
2346 if (chunk.length) {
2347 handleChunk(chunk);
2348 performChunkedAction(tests, handleChunk, onComplete, timeout, ++index);
2349 } else
2350 onComplete();
2351 // No need for a timeout on the first chunked action.
2352 }, index ? timeout : 0);
2353}
2354
2355function htmlForIndividualTests(tests)
2356{
2357 var testsHTML = [];
2358 for (var i = 0; i < tests.length; i++) {
2359 var test = tests[i];
2360 var testNameHtml = '';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002361 if (g_history.dashboardSpecificState.showChrome || tests.length > 1) {
2362 if (g_history.isLayoutTestResults()) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002363 var suite = lookupVirtualTestSuite(test);
2364 var base = suite ? baseTest(test, suite) : test;
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +01002365 var versionControlUrl = TEST_URL_BASE_PATH_IN_VERSION_CONTROL + base;
2366 testNameHtml += '<h2>' + linkHTMLToOpenWindow(versionControlUrl, test) + '</h2>';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002367 } else
2368 testNameHtml += '<h2>' + test + '</h2>';
2369 }
2370
2371 testsHTML.push(testNameHtml + htmlForIndividualTestOnAllBuildersWithResultsLinks(test));
2372 }
2373 return testsHTML.join('<hr>');
2374}
2375
2376function htmlForNavBar()
2377{
2378 var extraHTML = '';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002379 var html = ui.html.testTypeSwitcher(false, extraHTML, isCrossBuilderView());
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002380 html += '<div class=forms><form id=result-form ' +
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002381 'onsubmit="g_history.setQueryParameter(\'result\', result.value);' +
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002382 'return false;">Show all tests with result: ' +
2383 '<input name=result placeholder="e.g. CRASH" id=result-input>' +
2384 '</form><form id=tests-form ' +
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002385 'onsubmit="g_history.setQueryParameter(\'tests\', tests.value);' +
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002386 'return false;"><span>Show tests on all platforms: </span>' +
2387 '<input name=tests ' +
2388 'placeholder="Comma or space-separated list of tests or partial ' +
2389 'paths to show test results across all builders, e.g., ' +
2390 'foo/bar.html,foo/baz,domstorage" id=tests-input></form>' +
2391 '<span class=link onclick="showLegend()">Show legend [type ?]</span></div>';
2392 return html;
2393}
2394
2395function checkBoxToToggleState(key, text)
2396{
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002397 var stateEnabled = g_history.dashboardSpecificState[key];
2398 return '<label><input type=checkbox ' + (stateEnabled ? 'checked ' : '') + 'onclick="g_history.setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + text + '</label> ';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002399}
2400
2401function linkHTMLToToggleState(key, linkText)
2402{
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002403 var stateEnabled = g_history.dashboardSpecificState[key];
2404 return '<span class=link onclick="g_history.setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + (stateEnabled ? 'Hide' : 'Show') + ' ' + linkText + '</span>';
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002405}
2406
2407function headerForTestTableHtml()
2408{
2409 return '<h2 style="display:inline-block">Failing tests</h2>' +
2410 checkBoxToToggleState('showWontFixSkip', 'WONTFIX/SKIP') +
2411 checkBoxToToggleState('showCorrectExpectations', 'tests with correct expectations') +
2412 checkBoxToToggleState('showWrongExpectations', 'tests with wrong expectations') +
2413 checkBoxToToggleState('showFlaky', 'flaky') +
2414 checkBoxToToggleState('showSlow', 'slow');
2415}
2416
2417function generatePageForBuilder(builderName)
2418{
2419 processTestRunsForBuilder(builderName);
2420
2421 var results = g_perBuilderFailures[builderName];
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002422 sortTests(results, g_history.dashboardSpecificState.sortColumn, g_history.dashboardSpecificState.sortOrder);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002423
2424 var testsHTML = '';
2425 if (results.length) {
2426 var tableRowsHTML = '';
2427 for (var i = 0; i < results.length; i++)
2428 tableRowsHTML += htmlForSingleTestRow(results[i])
2429 testsHTML = htmlForTestTable(tableRowsHTML);
2430 } else {
2431 testsHTML = '<div>No tests found. ';
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002432 if (g_history.isLayoutTestResults())
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002433 testsHTML += 'Try showing tests with correct expectations.</div>';
2434 else
2435 testsHTML += 'This means no tests have failed!</div>';
2436 }
2437
2438 var html = htmlForNavBar();
2439
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002440 if (g_history.isLayoutTestResults())
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002441 html += htmlForTestsWithExpectationsButNoFailures(builderName) + headerForTestTableHtml();
2442
2443 html += '<br>' + testsHTML;
2444 appendHTML(html);
2445
2446 var ths = document.getElementsByTagName('th');
2447 for (var i = 0; i < ths.length; i++) {
2448 ths[i].addEventListener('click', changeSort, false);
2449 ths[i].className = "sortable";
2450 }
2451
2452 hideLoadingUI();
2453}
2454
2455var VALID_KEYS_FOR_CROSS_BUILDER_VIEW = {
2456 tests: 1,
2457 result: 1,
2458 showChrome: 1,
2459 showExpectations: 1,
2460 showLargeExpectations: 1,
2461 legacyExpectationsSemantics: 1,
2462 resultsHeight: 1,
2463 revision: 1
2464};
2465
2466function isInvalidKeyForCrossBuilderView(key)
2467{
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002468 return !(key in VALID_KEYS_FOR_CROSS_BUILDER_VIEW) && !(key in history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002469}
2470
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002471function hideLegend()
2472{
2473 var legend = $('legend');
2474 if (legend)
2475 legend.parentNode.removeChild(legend);
2476}
2477
2478var g_fallbacksMap = {};
2479g_fallbacksMap['WIN-XP'] = ['chromium-win-xp', 'chromium-win', 'chromium'];
2480g_fallbacksMap['WIN-7'] = ['chromium-win', 'chromium'];
2481g_fallbacksMap['MAC-SNOWLEOPARD'] = ['chromium-mac-snowleopard', 'chromium-mac', 'chromium'];
2482g_fallbacksMap['MAC-LION'] = ['chromium-mac', 'chromium'];
2483g_fallbacksMap['LINUX-32'] = ['chromium-linux-x86', 'chromium-linux', 'chromium-win', 'chromium'];
2484g_fallbacksMap['LINUX-64'] = ['chromium-linux', 'chromium-win', 'chromium'];
2485
2486function htmlForFallbackHelp(fallbacks)
2487{
2488 return '<ol class=fallback-list><li>' + fallbacks.join('</li><li>') + '</li></ol>';
2489}
2490
2491function showLegend()
2492{
2493 var legend = $('legend');
2494 if (!legend) {
2495 legend = document.createElement('div');
2496 legend.id = 'legend';
2497 document.body.appendChild(legend);
2498 }
2499
2500 var html = '<div id=legend-toggle onclick="hideLegend()">Hide ' +
2501 'legend [type esc]</div><div id=legend-contents>';
2502 for (var expectation in expectationsMap())
2503 html += '<div class=' + expectation + '>' + expectationsMap()[expectation] + '</div>';
2504
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002505 if (g_history.isLayoutTestResults()) {
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002506 html += '</div><br style="clear:both">' +
2507 '</div><h3>Test expectatons fallback order.</h3>';
2508
2509 for (var platform in g_fallbacksMap)
2510 html += '<div class=fallback-header>' + platform + '</div>' + htmlForFallbackHelp(g_fallbacksMap[platform]);
2511
2512 html += '<div>TIMES:</div>' +
2513 htmlForSlowTimes(MIN_SECONDS_FOR_SLOW_TEST) +
2514 '<div>DEBUG TIMES:</div>' +
2515 htmlForSlowTimes(MIN_SECONDS_FOR_SLOW_TEST_DEBUG);
2516 }
2517
2518 legend.innerHTML = html;
2519}
2520
2521function htmlForSlowTimes(minTime)
2522{
2523 return '<ul><li>&lt;1 second == !SLOW</li><li>&gt;1 second && &lt;' +
2524 minTime + ' seconds == SLOW || !SLOW is fine</li><li>&gt;' +
2525 minTime + ' seconds == SLOW</li></ul>';
2526}
2527
2528function postHeightChangedMessage()
2529{
2530 if (window == parent)
2531 return;
2532
2533 var root = document.documentElement;
2534 var height = root.offsetHeight;
2535 if (root.offsetWidth < root.scrollWidth) {
2536 // We have a horizontal scrollbar. Include it in the height.
2537 var dummyNode = document.createElement('div');
2538 dummyNode.style.overflow = 'scroll';
2539 document.body.appendChild(dummyNode);
2540 var scrollbarWidth = dummyNode.offsetHeight - dummyNode.clientHeight;
2541 document.body.removeChild(dummyNode);
2542 height += scrollbarWidth;
2543 }
2544 parent.postMessage({command: 'heightChanged', height: height}, '*')
2545}
2546
2547if (window != parent)
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002548 window.addEventListener('blur', ui.popup.hide);
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002549
2550document.addEventListener('keydown', function(e) {
2551 if (e.keyIdentifier == 'U+003F' || e.keyIdentifier == 'U+00BF') {
2552 // WebKit MAC retursn 3F. WebKit WIN returns BF. This is a bug!
2553 // ? key
2554 showLegend();
2555 } else if (e.keyIdentifier == 'U+001B') {
2556 // escape key
2557 hideLegend();
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002558 ui.popup.hide();
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00002559 }
2560}, false);
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002561
2562window.addEventListener('load', function() {
Torne (Richard Coles)53e740f2013-05-09 18:38:43 +01002563 resourceLoader = new loader.Loader();
Torne (Richard Coles)926b0012013-03-28 15:32:48 +00002564 resourceLoader.load();
2565}, false);