New flakiness dashboard should hyperlink test names, WebKit revisions, and bubbles
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 22 Oct 2013 04:42:54 +0000 (04:42 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 22 Oct 2013 04:42:54 +0000 (04:42 +0000)
https://bugs.webkit.org/show_bug.cgi?id=123134

Reviewed by Stephanie Lewis.

Copied admin.css, admin-header.php, admin-footer.php, builders.php, repositories.php from WebKit Perf Monitor.
(Unfortunately WebKit Perf Monitor hasn't been committed into WebKit repository just yet.)

Updated various parts of index.html to linkify test names, build numbers, and bubbles (to results page).

* admin/admin.css: Added.
* admin/builders.php: Added.
* admin/index.php: Removed the duplicated code now that it uses admin-header.php.
* admin/repositories.php: Added.
* api/manifest.php: Use camelCase for blame_url and build_url to be consistent with other JSON properties.
Also exported testCategories from config so that we can linkify test names in the dashboard.
* include/admin-footer.php: Added.
* include/admin-header.php: Added.
* include/config.json: Added test categories. This avoids hard-coding the URL to trac in php/js.
* include/init-database.sql: Added name and build_url to builders table and category to tests.
* include/test-results.php: Assume the test category to be LayoutTest for now.

* index.html:
(TestResultsView): Initialize _builders, _slaves, _repositories, _testCategories as dictionaries as intended.
(TestResultsView.setTestCategories): Added.
(TestResultsView._createResultCell): Dynamically resolve URLs of results page and and build page.
(TestResultsView._populateTestPane): Linkify the test name. Unfortunately we don't have a test object anywhere.
We need to figure out a way to find the test object here eventually. For now, hard-coding "LayoutTest" works.
(TestResultsView._linkifiedTestName): Added.
(TestResultsView._createBuildsAndComputeSlownessOfResults): Takes builderId to set "builder" property on each
result object as it's used by Build class.
(TestResultsView._populateBuilderPane):
(fetchManigest):

* js/build.js:
(Build.buildUrl): Support $builderName so that we don't have to keep repeating builder names in the database.
(Build.revision): Don't access [0] if revisions[repositoryId] was undefined.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@157775 268f45cc-cd09-0410-ab3c-d52691b4dbfc

13 files changed:
Websites/test-results/ChangeLog
Websites/test-results/admin/admin.css [new file with mode: 0644]
Websites/test-results/admin/builders.php [new file with mode: 0644]
Websites/test-results/admin/index.php
Websites/test-results/admin/repositories.php [new file with mode: 0644]
Websites/test-results/api/manifest.php
Websites/test-results/include/admin-footer.php [new file with mode: 0644]
Websites/test-results/include/admin-header.php [new file with mode: 0644]
Websites/test-results/include/config.json
Websites/test-results/include/init-database.sql
Websites/test-results/include/test-results.php
Websites/test-results/index.html
Websites/test-results/js/build.js

index bbc0f2024e4ef93a8d9d0050b2623c12cdfa6bdf..d45a8b29041cd8f95ebbca3843f370746d320a27 100644 (file)
@@ -1,3 +1,43 @@
+2013-10-21  Ryosuke Niwa  <rniwa@webkit.org>
+
+        New flakiness dashboard should hyperlink test names, WebKit revisions, and bubbles
+        https://bugs.webkit.org/show_bug.cgi?id=123134
+
+        Reviewed by Stephanie Lewis.
+
+        Copied admin.css, admin-header.php, admin-footer.php, builders.php, repositories.php from WebKit Perf Monitor.
+        (Unfortunately WebKit Perf Monitor hasn't been committed into WebKit repository just yet.)
+
+        Updated various parts of index.html to linkify test names, build numbers, and bubbles (to results page).
+
+        * admin/admin.css: Added.
+        * admin/builders.php: Added.
+        * admin/index.php: Removed the duplicated code now that it uses admin-header.php.
+        * admin/repositories.php: Added.
+        * api/manifest.php: Use camelCase for blame_url and build_url to be consistent with other JSON properties.
+        Also exported testCategories from config so that we can linkify test names in the dashboard.
+        * include/admin-footer.php: Added.
+        * include/admin-header.php: Added.
+        * include/config.json: Added test categories. This avoids hard-coding the URL to trac in php/js.
+        * include/init-database.sql: Added name and build_url to builders table and category to tests.
+        * include/test-results.php: Assume the test category to be LayoutTest for now.
+
+        * index.html:
+        (TestResultsView): Initialize _builders, _slaves, _repositories, _testCategories as dictionaries as intended.
+        (TestResultsView.setTestCategories): Added.
+        (TestResultsView._createResultCell): Dynamically resolve URLs of results page and and build page.
+        (TestResultsView._populateTestPane): Linkify the test name. Unfortunately we don't have a test object anywhere.
+        We need to figure out a way to find the test object here eventually. For now, hard-coding "LayoutTest" works.
+        (TestResultsView._linkifiedTestName): Added.
+        (TestResultsView._createBuildsAndComputeSlownessOfResults): Takes builderId to set "builder" property on each
+        result object as it's used by Build class.
+        (TestResultsView._populateBuilderPane):
+        (fetchManigest):
+
+        * js/build.js:
+        (Build.buildUrl): Support $builderName so that we don't have to keep repeating builder names in the database.
+        (Build.revision): Don't access [0] if revisions[repositoryId] was undefined.
+
 2013-10-21  Ryosuke Niwa  <rniwa@webkit.org>
 
         New flakiness dashboard should align results by revision numbers
diff --git a/Websites/test-results/admin/admin.css b/Websites/test-results/admin/admin.css
new file mode 100644 (file)
index 0000000..1baa999
--- /dev/null
@@ -0,0 +1,54 @@
+table {
+    font-size: small;
+}
+
+table, td {
+    border-collapse: collapse;
+    border: solid 1px #ccc;
+}
+
+td {
+    padding: 5px;
+}
+
+td pre {
+    max-height: 30em;
+    overflow: scroll;
+    margin: 0;
+    padding: 0;
+}
+
+tbody.odd {
+    background: #f6f6f6;
+}
+
+.action-field, .notice {
+    min-width: 50ex;
+    display: inline-block;
+    margin: 1em 0px;
+    margin-right: 1em;
+    border: solid 1px #ccc;
+    border-radius: 5px;
+    padding: 5px;
+}
+
+.action-field h2 {
+    font-size: 1em;
+    font-weight: normal;
+    padding: 0;
+    margin: 0 0 1em 0;
+}
+
+form {
+    display: inline;
+}
+
+label {
+    display: inline-block;
+}
+
+pre {
+    white-space: pre-wrap;
+    word-wrap: break-word;
+    word-break: break-all;
+}
diff --git a/Websites/test-results/admin/builders.php b/Websites/test-results/admin/builders.php
new file mode 100644 (file)
index 0000000..f663563
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+require('../include/admin-header.php');
+
+if ($db) {
+
+    if ($action == 'add') {
+        if ($db->insert_row('builders', NULL, array(
+            'name' => $_POST['name'], 'password_hash' => hash('sha256', $_POST['password']), 'build_url' => array_get($_POST, 'build_url')))) {
+            notice('Inserted the new builder.');
+            regenerate_manifest();
+        } else
+            notice('Could not add the builder.');
+    } else if ($action == 'update') {
+        if (update_field('builders', NULL, 'name') || update_field('builders', NULL, 'build_url'))
+            regenerate_manifest();
+        else
+            notice('Invalid parameters.');
+    }
+
+    $page = new AdministrativePage($db, 'builders', NULL, array(
+        'master' => array(),
+        'name' => array('size' => 50, 'editing_mode' => 'string'),
+        'build_url' => array('label' => 'Build URL', 'size' => 100, 'editing_mode' => 'url'),
+    ));
+
+    $page->render_table('name');
+}
+
+require('../include/admin-footer.php');
+
+?>
index 76d05c69c5ce8beef30506cef3b1d716fb1c3d13..a7d87833681c4023eedc10ccad81d5320e23bde9 100644 (file)
@@ -1,34 +1,9 @@
-<!DOCTYPE html>
-<html>
-<head>
-<title>WebKit Test Results</title>
-<link rel="stylesheet" href="/common.css">
-<style type="text/css">
-
-.unfetched {
-    color: gray;
-}
-
-</style>
-</head>
-<body>
-<header id="title">
-<h1><a href="/">WebKit Test Results</a></h1>
-<ul>
-    <li><a href="/admin/update-master">Update Master</a></li>
-</ul>
-</header>
-
-<div id="mainContents">
-<p><strong>FIXME: This page is broken!</strong></p>
 <?php
 
-require_once('../include/db.php');
-require_once('../include/test-results.php');
+include('../include/admin-header.php');
+include('../include/test-results.php');
 
-function notice($message) {
-    echo "<p class='notice'>$message</p>";
-}
+notice("FIXME: This page is broken!");
 
 define('MAX_FETCH_COUNT', 10);
 
diff --git a/Websites/test-results/admin/repositories.php b/Websites/test-results/admin/repositories.php
new file mode 100644 (file)
index 0000000..59721dc
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+include('../include/admin-header.php');
+
+if ($db) {
+    if ($action == 'update') {
+        if (update_field('repositories', NULL, 'name')
+            || update_field('repositories', NULL, 'url')
+            || update_field('repositories', NULL, 'blame_url'))
+            regenerate_manifest();
+        else
+            notice('Invalid parameters.');
+    }
+
+    $page = new AdministrativePage($db, 'repositories', NULL, array(
+        'name' => array('editing_mode' => 'string'),
+        'url' => array('label' => 'Revision URL (At revision $1)', 'editing_mode' => 'url'),
+        'blame_url' => array('label' => 'Blame URL (From revision $1 to revision $2)', 'editing_mode' => 'url')
+    ));
+
+    $page->render_table('name');
+}
+
+include('../include/admin-footer.php');
+
+?>
index 19cf6f923c04d528a106ee3ba91dd4dddd923a4e..20fb7dfe76e390ae997cb397ee60ec7d3bb52218 100644 (file)
@@ -4,9 +4,17 @@ require_once('../include/json-shared.php');
 
 $db = connect();
 
+$repositories = $db->fetch_table('repositories');
+foreach ($repositories as &$value)
+    $value['blameUrl'] = $value['blame_url'];
+
+$builders = $db->fetch_table('builders');
+foreach ($builders as &$value)
+    $value['buildUrl'] = $value['build_url'];
+
 exit_with_success(array('tests' => $db->fetch_table('tests'),
-    'builders' => $db->fetch_table('builders'),
+    'builders' => $builders,
     'slaves' => $db->fetch_table('slaves'),
-    'repositories' => $db->fetch_table('repositories')));
-
+    'repositories' => $repositories,
+    'testCategories' => config('testCategories')));
 ?>
diff --git a/Websites/test-results/include/admin-footer.php b/Websites/test-results/include/admin-footer.php
new file mode 100644 (file)
index 0000000..aa5a7c3
--- /dev/null
@@ -0,0 +1,4 @@
+</div>
+
+</body>
+</html>
diff --git a/Websites/test-results/include/admin-header.php b/Websites/test-results/include/admin-header.php
new file mode 100644 (file)
index 0000000..0399623
--- /dev/null
@@ -0,0 +1,224 @@
+<?php
+
+require_once('db.php');
+
+?><!DOCTYPE html>
+<html>
+<head>
+<title>WebKit Test Results</title>
+<link rel="stylesheet" href="/common.css">
+<link rel="stylesheet" href="/admin/admin.css">
+</head>
+<body>
+<header id="title">
+<h1><a href="/">WebKit Perf Monitor</a></h1>
+<ul>
+    <li><a href="/admin/">Admin</a></li>
+    <li><a href="/admin/builders">Builders</a></li>
+    <li><a href="/admin/repositories">Repositories</a></li>
+</ul>
+</header>
+
+<div id="mainContents">
+<?php
+
+function regenerate_manifest() {
+    // manifest.php doesn't need to be regenerated but WebKit Perf Monitor generates a static manifest.json.
+}
+
+function notice($message) {
+    echo "<p class='notice'>$message</p>";
+}
+
+$db = new Database;
+if (!$db->connect()) {
+    notice('Failed to connect to the database');
+    $db = NULL;
+} else
+    $action = array_key_exists('action', $_POST) ? $_POST['action'] : NULL;
+
+function execute_query_and_expect_one_row_to_be_affected($query, $params, $success_message, $failure_message) {
+    global $db;
+
+    foreach ($params as &$param) {
+        if ($param == '')
+            $param = NULL;
+    }
+
+    $affected_rows = $db->query_and_get_affected_rows($query, $params);
+    if ($affected_rows) {
+        assert('$affected_rows == 1');
+        notice($success_message);
+        return true;
+    }
+
+    notice($failure_message);
+    return false;
+}
+
+function update_field($table, $prefix, $field_name) {
+    global $db;
+
+    if (!array_key_exists('id', $_POST) || !array_key_exists($field_name, $_POST))
+        return FALSE;
+
+    $id = intval($_POST['id']);
+    $prefixed_field_name = $prefix . $field_name;
+    $id_field_name = $prefix ? $prefix . '_id' : 'id';
+
+    execute_query_and_expect_one_row_to_be_affected("UPDATE $table SET $prefixed_field_name = \$2 WHERE $id_field_name = \$1",
+        array($id, $_POST[$field_name]),
+        "Updated the $prefix $id",
+        "Could not update $prefix $id");
+
+    return TRUE;
+}
+
+class AdministrativePage {
+    private $table;
+    private $prefix;
+    private $column_to_be_ordered_by;
+    private $column_info;
+
+    function __construct($db, $table, $prefix, $column_info) {
+        $this->db = $db;
+        $this->table = $table;
+        $this->prefix = $prefix ? $prefix . '_' : '';
+        $this->column_info = $column_info;
+    }
+
+    private function name_to_titlecase($name) {
+        return ucwords(str_replace('_', ' ', $name));
+    }
+
+    private function column_label($name) {
+        return array_get($this->column_info[$name], 'label', $this->name_to_titlecase($name));
+    }
+
+    private function render_form_control_for_column($editing_mode, $name, $value = '', $show_update_button_if_needed = FALSE, $size = NULL) {
+        if ($editing_mode == 'text') {
+            echo <<< END
+<textarea name="$name" rows="7" cols="50">$value</textarea><br>
+END;
+            if ($show_update_button_if_needed) {
+                echo <<< END
+
+<button type="submit" name="action" value="update">Update</button>
+END;
+            }
+            return;
+        }
+
+        if ($editing_mode == 'url') {
+            if (!$size)
+                $size = 70;
+            echo <<< END
+<input type="text" name="$name" value="$value" size="$size">
+END;
+            return;
+        }
+
+        $sizeIfExits = $size ? " size=\"$size\"" : '';
+        echo <<< END
+<input type="text" name="$name" value="$value"$sizeIfExits>
+END;
+    }
+
+    function render_table($column_to_be_ordered_by) {
+        $column_names = array_keys($this->column_info);
+        $labels = array();
+        foreach ($column_names as $name) {
+            if (array_get($this->column_info[$name], 'pre_insertion'))
+                continue;
+            array_push($labels, htmlspecialchars($this->column_label($name)));
+        }
+
+
+        $headers = join('</td><td>', $labels);
+        echo <<< END
+<table>
+<thead><tr><td>ID</td><td>$headers</td></tr></thead>
+<tbody>
+
+END;
+
+        assert(ctype_alnum_underscore($column_to_be_ordered_by));
+        $rows = $this->db->fetch_table($this->table, $this->prefix . $column_to_be_ordered_by);
+        if ($rows) {
+            foreach ($rows as $row) {
+                $id = intval($row[$this->prefix . 'id']);
+                echo "<tr>\n<td>$id</td>\n";
+                foreach ($column_names as $name) {
+                    if (array_get($this->column_info[$name], 'pre_insertion'))
+                        continue;
+
+                    $custom = array_get($this->column_info[$name], 'custom');
+                    if ($custom) {
+                        echo "<td>";
+                        $custom($row);
+                        echo "</td>\n";
+                        continue;
+                    }
+
+                    $value = htmlspecialchars($row[$this->prefix . $name], ENT_QUOTES);
+                    $editing_mode = array_get($this->column_info[$name], 'editing_mode');
+                    if (!$editing_mode) {
+                        echo "<td>$value</td>\n";
+                        continue;
+                    }
+
+                    echo <<< END
+<td>
+<form method="POST">
+<input type="hidden" name="id" value="$id">
+<input type="hidden" name="action" value="update">
+
+END;
+                    $size = array_get($this->column_info[$name], 'size');
+                    $this->render_form_control_for_column($editing_mode, $name, $value, TRUE, $size);
+                    echo "</form></td>\n";
+
+                }
+                echo "</tr>\n";
+            }
+        }
+        echo <<< END
+</tbody>
+</table>
+END;
+    }
+
+    function render_form_to_add($title = NULL) {
+
+        if (!$title) # Can't use the table name since it needs to be singular.
+            $title = 'New ' . $this->name_to_titlecase($this->prefix);
+
+echo <<< END
+<section class="action-field">
+<h2>$title</h2>
+<form method="POST">
+
+END;
+        foreach (array_keys($this->column_info) as $name) {
+            $editing_mode = array_get($this->column_info[$name], 'editing_mode');
+            if (array_get($this->column_info[$name], 'custom') || !$editing_mode)
+                continue;
+
+            $label = htmlspecialchars($this->column_label($name));
+            echo "<label>$label<br>\n";
+            $this->render_form_control_for_column($editing_mode, $name);
+            echo "</label><br>\n";
+        }
+
+echo <<< END
+
+<button type="submit" name="action" value="add">Add</button>
+</form>
+</section>
+END;
+
+    }
+
+}
+
+?>
index a407f465613990926773a4bcd5254fc47658f4ed..3f45f02a26d5e6ffd38e98a4198550d26888add4 100644 (file)
     "masters": [
         "build.webkit.org",
         "build-safari.apple.com"
-    ]
+    ],
+    "testCategories": {
+        "LayoutTest": {
+            "url": "https://trac.webkit.org/browser/trunk/LayoutTests/$testName"
+        }
+    }
 }
index 8a011ebda46088648e5012c71b88818b8d0b7b7c..fca78f38cb8f568d57eacfa2070e9a84519f022f 100644 (file)
@@ -9,7 +9,8 @@ DROP TABLE builders CASCADE;
 CREATE TABLE builders (
     id serial PRIMARY KEY,
     master varchar(64) NOT NULL,
-    name varchar(64) NOT NULL UNIQUE);
+    name varchar(64) NOT NULL UNIQUE,
+    build_url varchar(1024));
 
 CREATE TABLE repositories (
     id serial PRIMARY KEY,
@@ -44,6 +45,7 @@ CREATE INDEX revision_repository_index ON build_revisions(repository);
 CREATE TABLE tests (
     id serial PRIMARY KEY,
     name varchar(1024) NOT NULL UNIQUE,
+    category varchar(64) NOT NULL,
     reftest_type varchar(64));
 
 CREATE TABLE results (
index f24921e34d3811df6eb33addf9de32975ae6b029..423d21c28111e72c13f7efb21ead0e53048f0d33 100644 (file)
@@ -70,9 +70,11 @@ function recursively_add_test_results($db, $build_id, $tests, $full_name) {
         require_format('test_modifiers', $modifiers, '/^[A-Za-z0-9 \.\/]+$/');
     else
         $modifiers = NULL;
+    $category = 'LayoutTest'; // FIXME: Support other test categories.
 
     $test_id = $db->select_or_insert_row('tests', NULL,
-        array('name' => $full_name), array('name' => $full_name, 'reftest_type' => json_encode(array_get($tests, 'reftest_type'))));
+        array('name' => $full_name),
+        array('name' => $full_name, 'reftest_type' => json_encode(array_get($tests, 'reftest_type')), 'category' => $category));
 
     $db->insert_row('results', NULL, array('test' => $test_id, 'build' => $build_id,
         'expected' => $tests['expected'], 'actual' => $tests['actual'],
index 2dd04290143efefe5508f698d38e600f02c7bb92..d14c8f82cd166ecd62fbbd8b9c51befc27b63c4a 100644 (file)
@@ -55,9 +55,10 @@ var TestResultsView = new (function () {
     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) {
@@ -76,6 +77,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;
@@ -95,43 +100,44 @@ 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 timeIfSlow = result.isSlow ? result.roundedTime : '';
-    var anchor = element('a', {'href': this._urlFromBuilder('result', master, builder, revision, build)}, [timeIfSlow]);
+
+    // 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', ['Revision: '].concat(revisionDescription)),
+                element('li', ['Build: ', element('a', {'href': result.build.buildUrl()}, [buildNumber])]),
                 element('li', ['Actual: ' + actual]),
                 element('li', ['Expected: ' + expected]),
             ])
@@ -144,14 +150,15 @@ 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'}, [element('caption', [this._linkifiedTestName(test)])]);
     table.appendChild(this._createTestResultHeader('Builder'));
 
     var resultsByBuilder = results['builders'];
     var buildTimes = new Array();
     for (var builderId in resultsByBuilder) {
         var results = resultsByBuilder[builderId];
-        this._createBuildsAndComputeSlownessOfResults(results);
+        this._createBuildsAndComputeSlownessOfResults(builderId, results);
         for (var i = 0; i < results.length; i++) {
             var time = results[i].build.time();
             if (buildTimes.indexOf(time) < 0)
@@ -168,6 +175,14 @@ TestResultsView._populateTestPane = function(testName, results, section) {
     section.appendChild(table);
 }
 
+TestResultsView._linkifiedTestName = function (test) {
+    var category = this._testCategories[test.category];
+    if (!category)
+        return test.name;
+
+    return element('a', {'href': category.url.replace(/\$testName/g, test.name)}, [test.name]);
+}
+
 TestResultsView._createTestResultHeader = function (labelForFirstColumn) {
     return element('thead', [element('tr', [
         element('th', [labelForFirstColumn]),
@@ -176,9 +191,10 @@ TestResultsView._createTestResultHeader = function (labelForFirstColumn) {
         element('th', ['Slowest'])])]);
 }
 
-TestResultsView._createBuildsAndComputeSlownessOfResults = function (results) {
+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;
@@ -224,7 +240,7 @@ TestResultsView._createTestResultRow = function (title, results, builder, buildT
         slowestTime = '';
 
     return element('tr', [
-        element('th', ['' + title]),
+        element('th', [title]),
         element('td', {'class': 'modifiers'}, formattedModifiers),
         element('td', {'class': 'expected'}, [sortedResults[0].expected]),
         element('td', {'class': 'slowestTime'}, [slowestTime]),
@@ -330,8 +346,8 @@ TestResultsView._populateBuilderPane = function(builderName, failureType, result
         var results = resultsByTests[testId];
         if (!results.length || !this._matchesFailureType(results, failureType, this._availableTests[testId].name))
             continue;
-        this._createBuildsAndComputeSlownessOfResults(resultsByTests[testId]);
-        table.appendChild(this._createTestResultRow(this._availableTests[testId].name, resultsByTests[testId], builder));
+        this._createBuildsAndComputeSlownessOfResults(builderId, resultsByTests[testId]);
+        table.appendChild(this._createTestResultRow(this._linkifiedTestName(this._availableTests[testId]), resultsByTests[testId], builder));
     }
     section.appendChild(table);
 }
@@ -461,6 +477,7 @@ fetchManifest(function (response) {
     TestResultsView.setBuilders(mapById(response['builders']));
     TestResultsView.setSlaves(mapById(response['slaves']));
     TestResultsView.setRepositories(mapById(response['repositories']));
+    TestResultsView.setTestCategories(response['testCategories']);
     // FIXME: Updating location.href shouldn't be TestResultsView's responsibility.
     var parsedStates = TestResultsView.loadTestsFromLocationHash();
     if (parsedStates['builder']) {
index d2be2ff530300c1abb75cdfe5ea1141eb1ff7399..29a81dd3409bac2f3887bbd765b365a93c781bc8 100644 (file)
@@ -18,10 +18,16 @@ function TestBuild(repositories, builders, rawRun) {
     this.builder = function () { return builders[rawRun.builder].name; }
     this.buildNumber = function () { return rawRun.buildNumber; }
     this.buildUrl = function () {
-        var template = builders[rawRun.builder].buildUrl;
-        return template ? template.replace(/\$buildNumber/g, this.buildNumber()) : null;
+        var builderData = builders[rawRun.builder];
+        var template = builderData.buildUrl;
+        if (!template)
+            return null;
+        return template.replace(/\$builderName/g, builderData.name).replace(/\$buildNumber/g, this.buildNumber());
+    }
+    this.revision = function(repositoryId) {
+        var repository = revisions[repositoryId];
+        return repository ? repository[0] : null;
     }
-    this.revision = function(repositoryId) { return revisions[repositoryId][0]; }
     this.formattedRevisions = function (previousBuild) {
         var result = {};
         for (var repositoryId in revisions) {