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