Reverted erroneously committed changes from the previous commit.
[WebKit-https.git] / Websites / test-results / index.html
index 01f1ab6..66fd109 100644 (file)
@@ -4,6 +4,8 @@
 <title>WebKit Test Results</title>
 <link rel="stylesheet" href="common.css">
 <link rel="stylesheet" href="main.css">
+<script src="https://svn.webkit.org/repository/webkit/!svn/bc/128779/trunk/PerformanceTests/Dromaeo/resources/dromaeo/web/lib/jquery-1.6.4.js"></script>
+<script src="https://svn.webkit.org/repository/webkit/!svn/bc/128779/trunk/PerformanceTests/resources/jquery.tablesorter.min.js"></script>
 <script src="js/autocompleter.js"></script>
 <script src="js/build.js"></script>
 <script src="js/dom.js"></script>
     placeholder="Type in a test name, or copy and paste test names on results.html or NRWT stdout (including junks)"></form>
 <div id="testView"></div>
 
+<div id="buildersView">
 <form>
-<label>Show failing tests for</label>
-<select id="builderListView"><option value="">Select builder</option></select>
-<select id="builderDaysView"><option>5</option><option>15</option><option>30</option></select>
+Show
+<label>tests <select id="builderFailureTypeView"><option>failing</option><option>flaky</option><option value="wrongexpectations">has wrong expectations</option></select></label>
+<label>on <select id="builderListView"><option value="">Select builder</option></select></label>
+<label for="builderDaysView">in the last <select id="builderDaysView"><option>5</option><option>15</option><option>30</option></select> days</label>
 </form>
 <div id="builderFailingTestsView"></div>
+</div>
 
 <div id="tooltipContainer"></div>
 
@@ -51,10 +56,13 @@ var TestResultsView = new (function () {
     this._tooltipContainer = tooltipContainer;
     this._tests = [];
     this._currentBuilder = null;
+    this._currentBuilderFailureType = null;
+    this._currentBuilderDays = null;
     this._oldHash = null;
-    this._builders = [];
-    this._slaves = [];
-    this._repositories = [];
+    this._builders = {};
+    this._slaves = {};
+    this._repositories = {};
+    this._testCategories = {};
 });
 
 TestResultsView.setAvailableTests = function (availableTests) {
@@ -73,6 +81,10 @@ TestResultsView.setRepositories = function (repositories) {
     this._repositories = repositories;
 }
 
+TestResultsView.setTestCategories = function (testCategories) {
+    this._testCategories = testCategories;
+}
+
 TestResultsView.showTooltip = function (anchor, contentElement) {
     var tooltipContainer = this._tooltipContainer;
     tooltipContainer.style.display = null;
@@ -92,43 +104,46 @@ TestResultsView.showTooltip = function (anchor, contentElement) {
     tooltipContainer.style.top = (position.y - contentElement.clientHeight - 5) + 'px';
 }
 
-TestResultsView._urlFromBuilder = function (urlType, master, builder, revision, build) {
-    // FIXME: We should probably make this configurable or fetch from buildbot configuraration.
-    return {
-        "build": "http://$master/builders/$builder/builds/$build",
-        "result": "http://$master/results/$builder/r$revision%20($build)/results.html"
-    }[urlType].replace(/\$master/g, master).replace(/\$builder/g, builder)
-        .replace(/\$revision/g, revision).replace(/\$build/g, build);
-}
-
 TestResultsView._createResultCell = function (master, builder, result, previousResult) {
     var buildTime = result['buildTime'];
-    var revision = result['revision'];
+    var revisions = result['revisions'];
     var slave = result['slave'];
-    var build = result['buildNumber'];
+    var buildNumber = result['buildNumber'];
     var actual = result['actual'];
     var expected = result['expected'];
-    var anchor = element('a', {'href': this._urlFromBuilder('result', master, builder, revision, build)});
+    var timeIfSlow = result.isSlow ? result.roundedTime : '';
+
+    // FIXME: We shouldn't be hard-coding WebKit revisions here.
+    var webkitRepositoryId;
+    for (var repositoryId in TestResultsView._repositories) {
+        if (TestResultsView._repositories[repositoryId].name == 'WebKit')
+            webkitRepositoryId = repositoryId;
+    }
+    var webkitRevision = result.build.revision(webkitRepositoryId);
+    var resultsPage = webkitRevision ? "http://" + master + "/results/" + builder + "/r" + webkitRevision + "%20(" + buildNumber + ")/results.html"
+        : 'javascript:alert("Could no resolve WebKit revision")';
+
+    var anchor = element('a', {'href': resultsPage }, [timeIfSlow]);
     anchor.onmouseenter = function () {
-        var repositoryById = TestResultsView._repositories;
         var formattedRevisions = result.build.formattedRevisions(previousResult ? previousResult.build : null);
-        var revisionDescription = '';
+        var revisionDescription = [];
         for (var repositoryName in formattedRevisions) {
             var revision = formattedRevisions[repositoryName];
-            if (revisionDescription)
-                revisionDescription += ', ';
+            if (revisionDescription.length)
+                revisionDescription.push(', ');
             if (revision.url)
-                revisionDescription += repositoryName + ': ' + revision.url;
+                revisionDescription.push(element('a', {'href': revision.url}, [revision.label]));
             else
-                revisionDescription += revision.label;
+                revisionDescription.push(revision.label);
         }
 
         TestResultsView.showTooltip(anchor, element('div', {'class': 'tooltip'}, [
             element('ul', [
                 element('li', ['Build Time: ' + result.build.formattedBuildTime()]),
-                element('li', ['Revision: ' +  revisionDescription]),
-                element('li', ['Build: ', element('a', {'href': TestResultsView._urlFromBuilder('build', master, builder, revision, build)}, [build])]),
-                element('li', ['Result: ' + actual]),
+                element('li', ['Revision: '].concat(revisionDescription)),
+                element('li', ['Build: ', element('a', {'href': result.build.buildUrl()}, [buildNumber])]),
+                element('li', ['Actual: ' + actual]),
+                element('li', ['Expected: ' + expected]),
             ])
         ]));
     }
@@ -139,35 +154,138 @@ TestResultsView._createResultCell = function (master, builder, result, previousR
 }
 
 TestResultsView._populateTestPane = function(testName, results, section) {
-    var table = element('table', {'class': 'resultsTable'}, [element('caption', [testName])]);
+    var test = {name: testName, category: 'LayoutTest'}; // FIXME: Use the real test object.
+    var table = element('table', {'class': 'resultsTable tablesorter'}, [element('caption', [this._linkifiedTestName(test)])]);
+
     var resultsByBuilder = results['builders'];
+    var buildTimes = new Array();
     for (var builderId in resultsByBuilder) {
-        var resultsByTest = resultsByBuilder[builderId];
-        var results;
-        for (var testId in resultsByTest)
-            results = resultsByTest[testId];
-        if (!results)
-            continue;
+        var results = resultsByBuilder[builderId];
+        this._createBuildsAndComputeSlownessOfResults(builderId, results);
+        for (var i = 0; i < results.length; i++) {
+            var time = results[i].build.time();
+            if (buildTimes.indexOf(time) < 0)
+                buildTimes.push(time);
+        }
+    }
+    buildTimes.sort(function (a, b) { return b - a; });
 
+    var repositories = [];
+    for (var repositoryId in this._repositories)
+        repositories.push(this._repositories[repositoryId]);
+
+    table.appendChild(this._createTestResultHeader('Builder', repositories));
+
+    var tbody = element('tbody');
+    for (var builderId in resultsByBuilder) {
         var builder = this._builders[builderId];
         // FIXME: Add a master name if there is more than one.
-        table.appendChild(this._createTestResultRow(builder.name, results, builder));
+        tbody.appendChild(this._createTestResultRow(builder.name, resultsByBuilder[builderId], builder, buildTimes, repositories));
     }
+    table.appendChild(tbody);
     section.appendChild(table);
+    $(table).tablesorter();
 }
 
-TestResultsView._createTestResultRow = function (title, results, builder) {
-    for (var i = 0; i < results.length; i++)
-        results[i].build = new TestBuild(this._repositories, this._builders, results[i]);
+TestResultsView._urlFromTest = function (test) {
+    var category = this._testCategories[test.category];
+    if (!category)
+        return null;
+    return category.url.replace(/\$testName/g, test.name);
+}
+
+TestResultsView._linkifiedTestName = function (test) {
+    var url = this._urlFromTest(test);
+    return url ? element('a', {'href': url}, [test.name]) : test.name;
+}
+
+TestResultsView._createTestResultHeader = function (labelForFirstColumn, repositories) {
+    return element('thead', [element('tr', [
+        element('th', [labelForFirstColumn]),
+        element('th', ['Bug']),
+        element('th', ['Expectations']),
+        element('th', ['Slowest'])]
+        .concat(repositories ? repositories.map(function (repository) { return element('th', [repository.name]); }) : [])
+        .concat([element('th')]))]);
+}
+
+TestResultsView._createBuildsAndComputeSlownessOfResults = function (builderId, results) {
+    for (var i = 0; i < results.length; i++) {
+        var result = results[i];
+        result.builder = builderId;
+        result.build = new TestBuild(this._repositories, this._builders, result);
+        result.roundedTime = result.time > 10000 ? Math.round(result.time / 1000) : Math.round(result.time / 100) / 10;
+        result.isSlow = result.time > 1000;
+    }
+}
+
+TestResultsView._createTestResultRow = function (title, results, builder, buildTimes, repositories) {
+    var sortedResults = results.sort(function (result1, result2) { return result2.build.time() - result1.build.time(); });
+    var cells = new Array();
+
+    function addEmptyCell() { cells.push(element('span', {'class': 'resultCell'}, [element('a')])); }
+
+    var buildTimeIndex = 0;
+    for (var i = 0; i < sortedResults.length; i++) {
+        var result = sortedResults[i];
+        if (buildTimes) {
+            while (buildTimes[buildTimeIndex] > result.build.time()) {
+                addEmptyCell();
+                buildTimeIndex++;
+            }
+            if (buildTimes[buildTimeIndex] == result.build.time())
+                buildTimeIndex++;
+        }
+        cells.push(this._createResultCell(builder.master, builder.name, result, sortedResults[i - 1]));
+    }
+    if (buildTimes) {
+        while (buildTimeIndex < buildTimes.length) {
+            addEmptyCell();
+            buildTimeIndex++;
+        }
+    }
+
+    var seenBugLink = false;
+    var formattedModifiers = sortedResults[0].modifiers.split(' ').map(function (modifier) {
+        if (modifier.indexOf('/') > 0) {
+            seenBugLink = true;
+            return element('a', {'href': (modifier.indexOf('http') == 0 ? '' : 'http://') + modifier}, [modifier]);
+        }
+        return modifier;
+    });
 
-    var sortedResults = results.sort(function (result1, result2) { return result1.build.time() - result2.build.time(); });
-    var cells = new Array(sortedResults.length);
-    for (var i = 0; i < sortedResults.length; i++)
-        cells[i] = this._createResultCell(builder.master, builder.name, sortedResults[i], sortedResults[i - 1]);
+    if (!seenBugLink) {
+        // FIXME: Make bug tracker configurable.
+        formattedModifiers.push(element('a',
+            {'href': 'https://bugs.webkit.org/enter_bug.cgi?product=WebKit&component=Tools%20/%20Tests&form_name=enter_bug&keywords=LayoutTestFailure'},
+            ['File a bug']));
+    }
+
+    var slowestTime = Math.max.apply(Math, results.map(function (result) { return result.roundedTime; }));
+    if (slowestTime >= 1)
+        slowestTime += 's';
+    else
+        slowestTime = '';
+
+    var formattedRevisionCells = [];
+    if (repositories) {
+        var build = sortedResults[0].build;
+        for (var i = 0; i < repositories.length; i++) {
+            var revisionInfo = build.formattedRevision(repositories[i].id);
+            if (revisionInfo.url)
+                formattedRevisionCells.push(element('a', {'href': revisionInfo.url}, [revisionInfo.label]));
+            else
+                formattedRevisionCells.push(revisionInfo.label);
+        }
+    }
 
-    var passCount = cells.filter(function (cell) { return cell.className == 'PASS'; }).length;
-    var passingRate = Math.round(passCount / cells.length * 100) + '%';
-    return element('tr', [element('th', ['' + title]), element('td', {'class': 'passingRate'}, [passingRate]), element('td', cells)]);
+    return element('tr', [
+        element('th', [title]),
+        element('td', {'class': 'modifiers'}, formattedModifiers),
+        element('td', {'class': 'expected'}, [sortedResults[0].expected]),
+        element('td', {'class': 'slowestTime'}, [slowestTime])]
+        .concat(formattedRevisionCells)
+        .concat([element('td', cells)]));
 }
 
 TestResultsView.fetchTest = function (testName) {
@@ -219,8 +337,39 @@ TestResultsView.fetchTests = function (testNames, doNotUpdateHash) {
         this.updateLocationHash();
 }
 
-TestResultsView._populateBuilderPane = function(builderName, results, section) {
-    var table = element('table', {'class': 'resultsTable'}, [element('caption', [builderName])]);
+TestResultsView._matchesFailureType = function (results, failureType, tn) {
+    if (!results.length)
+        return false;
+    var latestActualResult = results[0].actual;
+    var latestExpectedResult = results[0].expected;
+    switch (failureType) {
+    case 'failing':
+        return results[0].actual != 'PASS';
+    case 'flaky':
+        var offOneChangeCount = 0;
+        for (var i = 1; i + 1 < results.length; i++) {
+            var previousActual = results[i - 1].actual;
+            var nextActual = results[i + 1].actual;
+            if (previousActual == nextActual && results[i].actual != previousActual)
+                offOneChangeCount++;
+        }
+        return offOneChangeCount; // Heuristics.
+    case 'wrongexpectations':
+        if (latestExpectedResult == latestActualResult)
+            return false;
+        var expectedTokens = latestExpectedResult.split(' ');
+        if (expectedTokens.indexOf(latestActualResult) >= 0)
+            return false;
+        if (latestActualResult == 'TEXT' || latestActualResult == 'TEXT+IMAGE' && expectedTokens.indexOf('FAIL') >= 0)
+            return false;
+        return true;
+    }
+
+    return false;
+}
+
+TestResultsView._populateBuilderPane = function(builderName, failureType, results, section) {
+    var table = element('table', {'class': 'resultsTable tablesorter'}, [element('caption', [builderName])]);
     var resultsByBuilder = results['builders'];
     var resultsByTests;
     var builderId;
@@ -231,13 +380,34 @@ TestResultsView._populateBuilderPane = function(builderName, results, section) {
     if (!resultsByTests)
         return;
 
+    table.appendChild(this._createTestResultHeader('Test'));
+
+    var tbody = element('tbody');
     var builder = this._builders[builderId];
-    for (var testId in resultsByTests)
-        table.appendChild(this._createTestResultRow(this._availableTests[testId].name, resultsByTests[testId], builder));
+    var self = this;
+    for (var testId in resultsByTests) {
+        var results = resultsByTests[testId];
+        if (!results.length || !this._matchesFailureType(results, failureType, this._availableTests[testId].name))
+            continue;
+        this._createBuildsAndComputeSlownessOfResults(builderId, resultsByTests[testId]);
+        var test = this._availableTests[testId];
+        var externalTestLink = element('a', {'class': 'externalTestLink', 'href': this._urlFromTest(test)});
+        var testName = element('a', {'href':'#'}, [test.name]);
+        testName.onclick = function (event) {
+            self.fetchTests([this.textContent]);
+            event.preventDefault();
+            return false;
+        }
+        tbody.appendChild(this._createTestResultRow(element('span', [testName, externalTestLink]), resultsByTests[testId], builder));
+    }
+
+    table.appendChild(tbody);
     section.appendChild(table);
+
+    $(table).tablesorter(); 
 }
 
-TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDays, doNotUpdateHash) {
+TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDays, failureType, doNotUpdateHash) {
     var section = element('section', {'class': 'testResults'}, ['Loading...']);
 
     var container = document.getElementById('builderFailingTestsView');
@@ -246,7 +416,7 @@ TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDay
 
     var self = this;
     var xhr = new XMLHttpRequest();
-    xhr.open("GET", 'api/failing-tests.php?builder=' + escape(builderName) + '&' + 'days=' + numberOfDays, true);  
+    xhr.open("GET", 'api/failing-tests.php?builder=' + escape(builderName) + '&days=' + numberOfDays, true);  
     xhr.onload = function(event) {
         var response = JSON.parse(xhr.response);
         section.innerHTML = '';
@@ -254,19 +424,22 @@ TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDay
             section.appendChild(text('Failed to load results for ' + builderName + ': ' + response['status']));
             return;
         }
+        // FIXME: Normalize failureType.
         self._currentBuilder = builderName;
+        self._currentBuilderFailureType = failureType;
         self._currentBuilderDays = numberOfDays;
-        self._populateBuilderPane(builderName, response, section);
+        self._populateBuilderPane(builderName, failureType, response, section);
         if (!doNotUpdateHash)
             self.updateLocationHash();
     }
     xhr.send();
 }
 
-TestResultsView.updateLocationHash = function () {
+TestResultsView._createLocationHash = function (tests) {
     var params = {
-        'tests': this._tests.join(','),
+        'tests': tests.join(','),
         'builder': this._currentBuilder,
+        'builderFailureType': this._currentBuilderFailureType,
         'builderDays': this._currentBuilderDays,
     };
     var hash = '';
@@ -276,7 +449,11 @@ TestResultsView.updateLocationHash = function () {
             continue;
         hash += '&' + decodeURIComponent(key) + '=' + decodeURIComponent(value);
     }
-    location.hash = hash;
+    return hash;
+}
+
+TestResultsView.updateLocationHash = function () {
+    location.hash = this._createLocationHash(this._tests);
     this._oldHash = location.hash;
 }
 
@@ -303,8 +480,13 @@ TestResultsView.loadTestsFromLocationHash = function () {
         document.getElementById('testView').innerHTML = '';
         this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
     }
-    if (parsed['builder'])
-        this.fetchFailingTestsForBuilder(parsed['builder'], parsed['builderDays'] || 5, doNotUpdateHash);
+    if (parsed['builder']) {
+        this.fetchFailingTestsForBuilder(
+            parsed['builder'],
+            parseInt(parsed['builderDays']) || 5,
+            parsed['builderFailureType'] || 'failing', doNotUpdateHash);
+    }
+    return parsed;
 }
 
 function fetchManifest(callback) {
@@ -332,14 +514,16 @@ fetchManifest(function (response) {
         builderListView.appendChild(element('option', [text(response['builders'][builderId].name)]));
 
     var builderDaysView = document.getElementById('builderDaysView');
+    var builderFailureTypeView = document.getElementById('builderFailureTypeView');
 
     function updateBuilderView() {
         if (builderListView.value)
-            TestResultsView.fetchFailingTestsForBuilder(builderListView.value, builderDaysView.value);
+            TestResultsView.fetchFailingTestsForBuilder(builderListView.value, builderDaysView.value, builderFailureTypeView.value);
     }
 
     builderListView.addEventListener('change', updateBuilderView);
     builderDaysView.addEventListener('change', updateBuilderView);
+    builderFailureTypeView.addEventListener('change', updateBuilderView);
 
     function mapById(items) {
         var results = {};
@@ -352,7 +536,14 @@ fetchManifest(function (response) {
     TestResultsView.setBuilders(mapById(response['builders']));
     TestResultsView.setSlaves(mapById(response['slaves']));
     TestResultsView.setRepositories(mapById(response['repositories']));
-    TestResultsView.loadTestsFromLocationHash();
+    TestResultsView.setTestCategories(response['testCategories']);
+    // FIXME: Updating location.href shouldn't be TestResultsView's responsibility.
+    var parsedStates = TestResultsView.loadTestsFromLocationHash();
+    if (parsedStates['builder']) {
+        builderListView.value = parsedStates['builder'];
+        builderDaysView.value = parsedStates['builderDays'];
+        builderFailureTypeView.value = parsedStates['builderFailureType'];
+    }
 });
 
 function pasteHelper(input, event) {