From 178cbcad76bc850b4abcfc683764e18a1f83e2b8 Mon Sep 17 00:00:00 2001
From: "ojan@google.com" <ojan@google.com@0039d316-1c4b-4281-b951-d872f2087c98>
Date: Tue, 15 Sep 2009 18:57:35 +0000
Subject: [PATCH] Flakiness dashboard changes: -Add a view of skipped tests for
 that platform -Don't mark tests that timeout as needing a SLOW modifier.
 -Show date and webkit/chrome blamelists when you click on results -Show a
 line for when a webkit merge happens. Review URL:
 http://codereview.chromium.org/195081

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@26247 0039d316-1c4b-4281-b951-d872f2087c98
---
 .../layout_tests/flakiness_dashboard.html     | 315 +++++++++++++-----
 1 file changed, 231 insertions(+), 84 deletions(-)

diff --git a/webkit/tools/layout_tests/flakiness_dashboard.html b/webkit/tools/layout_tests/flakiness_dashboard.html
index 066b6fcc604c8..d92e23b71fb2d 100644
--- a/webkit/tools/layout_tests/flakiness_dashboard.html
+++ b/webkit/tools/layout_tests/flakiness_dashboard.html
@@ -15,6 +15,10 @@
       font-size: 16px;
       margin-bottom: .25em;
     }
+    h3 {
+      font-size: 13px;
+      margin: 0;
+    }
     #max-results-form {
       display: inline;
     }
@@ -124,6 +128,12 @@
     .O {
       background-color: #69f;
     }
+    .merge {
+      background-color: green;
+    }
+    :not(#legend) > .merge {
+      width: 1px;
+    }
     .separator {
       border: 1px solid lightgray;
       height: 0px;
@@ -136,10 +146,10 @@
       font-weight: bold;
     }
     #passing-tests {
-      -webkit-column-count: 2;
+      -webkit-column-count: 3;
       -webkit-column-gap: 25px;
       -webkit-column-rule: 1px dashed black;
-      -moz-column-count: 2;
+      -moz-column-count: 3;
       -moz-column-gap: 25px;
       -moz-column-rule: 1px dashed black;
     }
@@ -157,6 +167,26 @@
       text-align: center;
       font-weight: bold;
     }
+    #popup {
+      background-color: white;
+      overflow: hidden;
+      width: 250px;
+      z-index: 1;
+      position: absolute;
+      border: 3px solid grey;
+      padding: 3px;
+      -webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
+      -moz-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
+      -webkit-border-radius: 5px;
+      -moz-border-radius: 5px;
+    }
+    #popup > * {
+      width: 100%;
+    }
+    #popup > ul {
+      margin: 0;
+      padding-left: 20px;
+    }
   </style>
 
   <script>
@@ -177,38 +207,43 @@
    *   -add the builder name to the list of builders below.
    */
 
-   // CONSTANTS
-   var FORWARD = 'forward';
-   var BACKWARD = 'backward';
-   var TEST_URL_BASE_PATH =
-       'http://trac.webkit.org/projects/webkit/browser/trunk/';
-   var BUILDERS_BASE_PATH =
-       'http://build.chromium.org/buildbot/waterfall/builders/';
-   var EXPECTATIONS_MAP = {
-     'T': 'TIMEOUT',
-     'C': 'CRASH',
-     'P': 'PASS',
-     'F': 'TEXT FAIL',
-     'S': 'SIMPLIFIED',
-     'I': 'IMAGE',
-     'O': 'OTHER',
-     'N': 'NO DATA'
-   };
-   var PLATFORMS = {'MAC': 'MAC', 'LINUX': 'LINUX', 'WIN': 'WIN'};
-   var BUILD_TYPES = {'DEBUG': 'DBG', 'RELEASE': 'RELEASE'};
-
-   // GLOBALS
-   // The DUMMYVALUE gets shifted off the array in the first call to
-   // generatePage.
-   var tableHeaders = ['DUMMYVALUE', 'bugs', 'modifiers', 'expectations',
-       'missing', 'extra', 'slowest run',
-       'flakiness (numbers are runtimes in seconds)'];
-   var perBuilderPlatformAndBuildType = {};
-   var perBuilderFailures = {};
-   // Map of builder to arrays of tests that are listed in the expectations file
-   // but have for that builder.
-   var perBuilderWithExpectationsButNoFailures = {};
-
+  // CONSTANTS
+  var FORWARD = 'forward';
+  var BACKWARD = 'backward';
+  var TEST_URL_BASE_PATH =
+      'http://trac.webkit.org/projects/webkit/browser/trunk/';
+  var BUILDERS_BASE_PATH =
+      'http://build.chromium.org/buildbot/waterfall/builders/';
+  var TEST_RESULTS_BASE_PATH =
+     'http://build.chromium.org/buildbot/layout_test_results/';
+  var EXPECTATIONS_MAP = {
+    'T': 'TIMEOUT',
+    'C': 'CRASH',
+    'P': 'PASS',
+    'F': 'TEXT FAIL',
+    'S': 'SIMPLIFIED',
+    'I': 'IMAGE',
+    'O': 'OTHER',
+    'N': 'NO DATA'
+  };
+  var PLATFORMS = {'MAC': 'MAC', 'LINUX': 'LINUX', 'WIN': 'WIN'};
+  var BUILD_TYPES = {'DEBUG': 'DBG', 'RELEASE': 'RELEASE'};
+
+  // GLOBALS
+  // The DUMMYVALUE gets shifted off the array in the first call to
+  // generatePage.
+  var tableHeaders = ['DUMMYVALUE', 'bugs', 'modifiers', 'expectations',
+      'missing', 'extra', 'slowest run',
+      'flakiness (numbers are runtimes in seconds)'];
+  var perBuilderPlatformAndBuildType = {};
+  var perBuilderFailures = {};
+  // Map of builder to arrays of tests that are listed in the expectations file
+  // but have for that builder.
+  var perBuilderWithExpectationsButNoFailures = {};
+  // Map of builder to arrays of paths that are skipped. This shows the raw
+  // path used in test_expectations.txt rather than the test path since we
+  // don't actually have any data here for skipped tests.
+  var perBuilderSkippedPaths = {};
 
   // Generic utility functions.
   function $(id) {
@@ -306,24 +341,23 @@
     showWontFix: false,
     showCorrectExpectations: false,
     showFlaky: true,
+    showSkipped: false,
     maxResults: 200,
     testType: 'layout_test_results'
   };
 
-  for (var builder in builders) {
-    defaultStateValues.builder = builder;
-    break;
-  }
-
   function fillDefaultStateValues() {
-    // tests has no states with default values.
-    if (currentState.tests)
-      return;
-
     for (var state in defaultStateValues) {
       if (!(state in currentState))
         currentState[state] = defaultStateValues[state];
     }
+
+    if (!currentState.tests && !('builder' in currentState)) {
+      for (var builder in builders) {
+        currentState.builder = builder;
+        break;
+      }
+    }
   }
 
   function handleValidHashParameter(key, value) {
@@ -602,15 +636,21 @@
    * whether the modifiers match the builders platform and buildType.
    */
   function addTestAndExpectations(test, prefixPath, builder, expectationsMap,
-      expectations, testPrefixes) {
+      expectations, testPrefixes, skippedPaths) {
     var modifiersForBuilder =
         getModifierThatHasPlatformAndBuildType(builder, expectations);
-    if (modifiersForBuilder) {
-      if (getAllTestsWithSamePlatformAndBuildType(builder)[test]) {
-        expectationsMap[test] = expectations;
-        testPrefixes[test] = prefixPath;
-      } else if (!stringContains(modifiersForBuilder.modifiers, 'SKIP') &&
-          !modifiersForBuilder.expectations.match(/^\s*PASS\s*$/)) {
+
+    if (!modifiersForBuilder)
+      return false;
+
+    if (getAllTestsWithSamePlatformAndBuildType(builder)[test]) {
+      expectationsMap[test] = expectations;
+      testPrefixes[test] = prefixPath;
+    } else if (!stringContains(modifiersForBuilder.modifiers, 'WONTFIX')) {
+
+      if (stringContains(modifiersForBuilder.modifiers, 'SKIP')) {
+        skippedPaths[prefixPath] = true;
+      } else if (!modifiersForBuilder.expectations.match(/^\s*PASS\s*$/)) {
         // Don't include skip tests here as they'll look the same as a test
         // that passes on all builders.
         // Also don't include tests that are only expected to pass, e.g.
@@ -618,9 +658,9 @@
         // TODO(ojan): Should we also exclude WONTFIX tests here?
         perBuilderWithExpectationsButNoFailures[builder].push(test);
       }
-      return true;
     }
-    return false;
+
+    return true;
   }
 
   /**
@@ -665,11 +705,12 @@
 
     var testPrefixes = {};
     perBuilderWithExpectationsButNoFailures[builderName] = [];
+    var skippedPaths = {};
     for (var path in expectationsByTest) {
       var expectations = expectationsByTest[path];
       if (!isDirectory(path) &&
           addTestAndExpectations(path, path, builderName, expectationsMap,
-              expectations, testPrefixes)) {
+              expectations, testPrefixes, skippedPaths)) {
         continue;
       }
       // Test path doesn't match a specific test, see if it prefix matches
@@ -679,10 +720,16 @@
             (!testPrefixes[test] ||
              !stringContains(testPrefixes[test], path))) {
           addTestAndExpectations(test, path, builderName, expectationsMap,
-              expectations, testPrefixes);
+              expectations, testPrefixes, skippedPaths);
         }
       }
     }
+
+    perBuilderSkippedPaths[builderName] = [];
+    for (var path in skippedPaths) {
+      perBuilderSkippedPaths[builderName].push(path);
+    }
+    perBuilderSkippedPaths[builderName].sort();
     perBuilderWithExpectationsButNoFailures[builderName].sort();
 
     var allTestsForThisBuilder = resultsByBuilder[builderName].tests;
@@ -776,7 +823,7 @@
             times[i][1]);
       }
 
-      if (resultsForTest.slowestTime &&
+      if (resultsForTest.slowestTime && !resultsMap['TIMEOUT'] &&
           (!resultsForTest.expectations ||
            !stringContains(resultsForTest.expectations, 'TIMEOUT')) &&
           (!resultsForTest.modifiers ||
@@ -829,8 +876,54 @@
     return bugs;
   }
 
-  function loadBuilderPageForBuildNumber(builderName, buildNumber) {
-    window.open(BUILDERS_BASE_PATH + builderName + '/builds/' + buildNumber);
+  function getLinkHTMLToOpenWindow(url, text) {
+    return '<li class=link onclick="window.open(\'' + url + '\')">' + text +
+        '</li>';
+  }
+
+  function showPopupForBuild(e, builder, index) {
+    var html = '';
+
+    var time = resultsByBuilder[builder].secondsSinceEpoch[index];
+    if (time) {
+      var date = new Date(time * 1000);
+      html += date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
+    }
+
+    html += '<ul>';
+
+    var webkitRevision = resultsByBuilder[builder].webkitRevision;
+    var thisWebkitRevision = webkitRevision[index];
+    if (thisWebkitRevision) {
+      var previousWebkitRevision = webkitRevision[index + 1] ||
+          thisWebkitRevision;
+      html += getLinkHTMLToOpenWindow('http://trac.webkit.org/log/?rev=' +
+              thisWebkitRevision + '&stop_rev=' + previousWebkitRevision,
+          'WebKit blamelist r' + previousWebkitRevision + ':r' +
+              thisWebkitRevision);
+    }
+
+    var chromeRevision = resultsByBuilder[builder].chromeRevision;
+    var thisChromeRevision = chromeRevision[index];
+    if (thisChromeRevision) {
+      var previousChromeRevision = chromeRevision[index + 1] ||
+          thisChromeRevision;
+      html += getLinkHTMLToOpenWindow(
+          'http://build.chromium.org/buildbot/perf/dashboard/ui/' +
+              'changelog.html?url=/trunk/src&mode=html&range=' +
+              previousChromeRevision + ':' + thisChromeRevision,
+          'Chrome blamelist r' + previousChromeRevision + ':r' +
+              thisChromeRevision) +
+          getLinkHTMLToOpenWindow(TEST_RESULTS_BASE_PATH + builders[builder] +
+                  '/' + thisChromeRevision + '/layout-test-results.zip',
+              'layout-test-results.zip');
+    }
+
+    var buildNumbers = resultsByBuilder[builder].buildNumbers;
+    html += getLinkHTMLToOpenWindow(BUILDERS_BASE_PATH + builder + '/builds/' +
+        buildNumbers[index], 'Build log and blamelist') + '</ul>';
+
+    showPopup(e, html);
   }
 
   function getHtmlForTestResults(test, builder) {
@@ -842,9 +935,8 @@
     var indexToReplaceCurrentResult = -1;
     var indexToReplaceCurrentTime = -1;
     var currentResultArray, currentTimeArray, currentResult, innerHTML;
-    for (var i = 0;
-         i < buildNumbers.length && i < currentState.maxResults;
-         i++) {
+    var maxIndex = Math.min(buildNumbers.length, currentState.maxResults);
+    for (var i = 0; i < maxIndex; i++) {
       if (i > indexToReplaceCurrentResult) {
         currentResultArray = results.shift();
         if (currentResultArray) {
@@ -868,25 +960,49 @@
         innerHTML = currentTime || '&nbsp;';
       }
 
-      var buildNumber = buildNumbers[i];
-      html += '<td title="Build:' + buildNumber + '" class="results ' +
-          currentResult + '" onclick=\'loadBuilderPageForBuildNumber("' +
-          builder + '","' + buildNumber + '")\'>' + innerHTML + '</td>';
+      html += '<td title="Click results for handy links." class="results ' +
+          currentResult + '" onclick=\'showPopupForBuild(event, "' + builder +
+          '",' + i + ')\'>' + innerHTML + '</td>';
+
+      var webkitRevision = resultsByBuilder[builder].webkitRevision;
+      var isWebkitMerge = webkitRevision[i + 1] &&
+          webkitRevision[i] != webkitRevision[i + 1];
+      if (isWebkitMerge)
+        html += '<td class=merge></td>';
     }
     return html;
   }
 
   function getHTMLForTestsWithExpectationsButNoFailures(builder) {
     var tests = perBuilderWithExpectationsButNoFailures[builder];
-    if (!tests.length)
-      return '';
+    var skippedPaths = perBuilderSkippedPaths[builder];
 
-    var buildInfo = getPlatFormAndBuildType(builder);
-    return '<h2>Have expectations for ' + buildInfo.platform + '-' +
-          buildInfo.buildType + ' but have not failed in last ' +
+    var html = '';
+    if (tests.length || skippedPaths.length) {
+      var buildInfo = getPlatFormAndBuildType(builder);
+      html += '<h2>Expectations for ' + buildInfo.platform + '-' +
+          buildInfo.buildType + ':</h2>';
+    }
+
+    if (tests.length) {
+      html +=  '<h3>Have not failed in last ' +
           resultsByBuilder[builderName].buildNumbers.length +
-          ' runs.</h2><div id="passing-tests"><div>' +
+          ' runs.</h3><div id="passing-tests"><div>' +
           tests.join('</div><div>') + '</div></div>';
+    }
+
+    if (skippedPaths.length) {
+      html += '<h3>' +
+          getLinkHTMLToToggleState('showSkipped',
+              'Skipped tests in text_expectations.txt') +
+          '</h3>';
+
+      if (currentState.showSkipped) {
+        html += '<div id="passing-tests"><div>' +
+          skippedPaths.join('</div><div>') + '</div></div>';
+      }
+    }
+    return html;
   }
 
   /**
@@ -1145,13 +1261,13 @@
           EXPECTATIONS_MAP[expectation] + '</div>';
     }
     return html + '<div class=wrong-expectations>WRONG EXPECTATIONS</div>' +
-        '</div>';
+        '<div class=merge>WEBKIT MERGE</div></div>';
   }
 
   function getLinkHTMLToToggleState(key, linkText) {
     var isTrue = currentState[key];
     return '<span class=link onclick="setState(\'' + key + '\', ' + !isTrue +
-        ')">' + (isTrue ? 'Hide' : 'Show') + ' ' + linkText + '</span> | ';
+        ')">' + (isTrue ? 'Hide' : 'Show') + ' ' + linkText + '</span>';
   }
 
   function generatePageForBuilder(builderName) {
@@ -1170,13 +1286,13 @@
     var html = getHTMLForNavBar(builderName) +
         getHTMLForTestsWithExpectationsButNoFailures(builderName) +
         '<h2>Failing tests</h2><div>' +
-        getLinkHTMLToToggleState('showWontFix', 'WONTFIX tests')  +
+        getLinkHTMLToToggleState('showWontFix', 'WONTFIX tests') + ' | ' +
         getLinkHTMLToToggleState('showCorrectExpectations',
-            'tests with correct expectations') +
-        getLinkHTMLToToggleState('showFlaky', 'flaky tests') +
+            'tests with correct expectations') + ' | ' +
+        getLinkHTMLToToggleState('showFlaky', 'flaky tests') + ' | ' +
         '<form id=max-results-form ' +
         'onsubmit="setState(\'maxResults\', maxResults.value);return false;"' +
-        '><span>Max results to show: </span>' +
+        '><span>Number of results to show: </span>' +
         '<input name=maxResults id=max-results-input></form> | ' +
         '<b>All columns are sortable. | Skipped tests are not listed. | ' +
         'Flakiness reader order is newer --> older runs.</b></div>' +
@@ -1193,9 +1309,6 @@
     $('max-results-input').value = currentState.maxResults;
   }
 
-  var singleBuilderViewParameters = ['sortOrder', 'sortColumn', 'showWontFix',
-      'showCorrectExpectations', 'showFlaky: true', 'maxResults'];
-
   /**
    * Sets the page state and regenerates the page. Takes varargs of key, value
    * pairs.
@@ -1203,11 +1316,7 @@
   function setState(key, value) {
     for (var i = 0; i < arguments.length; i = i + 2) {
       var key = arguments[i];
-      if (key == 'tests') {
-        for (var state in singleBuilderViewParameters) {
-          delete currentState[state];
-        }
-      } else {
+      if (key != 'tests') {
         delete currentState.tests;
       }
 
@@ -1241,6 +1350,44 @@
     console.log(msg + ': ' + (Date.now() - startTime));
   }
 
+  function hidePopup() {
+    var popup = $('popup');
+    popup.parentNode.removeChild(popup);
+  }
+
+  function showPopup(e, html) {
+    var popup = $('popup');
+    if (!popup) {
+      popup = document.createElement('div');
+      popup.id = 'popup';
+      document.body.appendChild(popup);
+    }
+
+    // Set html first so that we can get accurate size metrics on the popup.
+    popup.innerHTML = html;
+
+    var targetRect = e.target.getBoundingClientRect();
+
+    var x = Math.min(targetRect.left - 10,
+        document.documentElement.clientWidth - popup.offsetWidth);
+    popup.style.left = x + document.body.scrollLeft + 'px';
+
+    var y = targetRect.top + targetRect.height;
+    if (y + popup.offsetHeight > document.documentElement.clientHeight) {
+      y = targetRect.top - popup.offsetHeight;
+    }
+    popup.style.top = y + document.body.scrollTop + 'px';
+  }
+
+  document.onmousedown = function(e) {
+    // Clear the open popup, unless the click was inside the popup.
+    var popup = $('popup');
+    if (popup && e.target != popup &&
+        !(popup.compareDocumentPosition(e.target) & 16)) {
+      hidePopup();
+    }
+  };
+
   window.onload = function() {
     // This doesn't seem totally accurate as there is a race between
     // onload firing and the last script tag being executed.
-- 
GitLab