+2013-10-16 Ryosuke Niwa <rniwa@webkit.org>
+
+ Add a new flakiness dashboard clone
+ https://bugs.webkit.org/show_bug.cgi?id=122936
+
+ Reviewed by Anders Carlsson.
+
+ Added the initial prototype.
+
+ * Websites/test-results: Added.
+ * Websites/test-results/.htaccess: Added.
+ * Websites/test-results/admin: Added.
+ * Websites/test-results/admin/index.php: Added.
+ * Websites/test-results/api: Added.
+ * Websites/test-results/api/manifest.php: Added.
+ * Websites/test-results/api/report.php: Added.
+ * Websites/test-results/api/results.php: Added.
+ * Websites/test-results/include: Added.
+ * Websites/test-results/include/config.json: Added.
+ * Websites/test-results/include/db.php: Added.
+ * Websites/test-results/include/init-database.sql: Added.
+ * Websites/test-results/include/json-shared.php: Added.
+ * Websites/test-results/include/test-results.php: Added.
+ * Websites/test-results/index.html: Added.
+ * Websites/test-results/js: Added.
+ * Websites/test-results/js/autocompleter.js: Added.
+ * Websites/test-results/js/build.js: Added.
+ * Websites/test-results/js/dom.js: Added.
+
2013-10-16 Csaba Osztrogonác <ossy@webkit.org>
[WK2][Efl][CMake] Add support for ENABLE_NETWORK_PROCESS to the build system
--- /dev/null
+php_value post_max_size 20M
+
+<IfModule mod_php5.c>
+php_value upload_max_filesize 100000000
+php_value post_max_size 110000000
+php_value memory_limit 120000000
+php_value max_input_time 60
+</IfModule>
--- /dev/null
+<!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');
+
+function notice($message) {
+ echo "<p class='notice'>$message</p>";
+}
+
+define('MAX_FETCH_COUNT', 10);
+
+function fetch_builders($db, $master) {
+ flush();
+ $builders_json = fetch_and_parse_test_results_json("http://$master/json/builders/");
+ if (!$builders_json)
+ return notice("Failed to fetch or decode /json/builders from $master");
+
+ echo "<h2>Fetching builds from $master</h2>\n";
+ echo "<ul>\n";
+
+ foreach ($builders_json as $builder_name => $builder_data) {
+ if (!stristr($builder_name, 'Test') || stristr($builder_name, 'Apple Win'))
+ continue;
+ $builder_id = $db->select_or_insert_row('builders', NULL, array('master' => $master, 'name' => $builder_name));
+ $escaped_builder_name = htmlspecialchars($builder_name);
+ echo "<li><em>$escaped_builder_name</em> (id: $builder_id) - ";
+
+ $fetchCount = 0;
+ $builds = $builder_data['cachedBuilds'];
+ foreach (array_reverse($builds) as $build_number) {
+ $build_number = intval($build_number);
+ $build = $db->select_or_insert_row('builds', NULL, array('builder' => $builder_id, 'number' => $build_number), NULL, '*');
+ if ($db->is_true($build['fetched']))
+ $class = 'fetched';
+ else if ($fetchCount >= MAX_FETCH_COUNT)
+ $class = 'unfetched';
+ else {
+ $class = fetch_build($db, $master, $builder_name, $build['id'], $build_number) ? 'fetched' : 'unfetched';
+ $fetchCount++;
+ }
+ echo "<span class=\"$class\">$build_number</a> ";
+ }
+ echo "</li>\n";
+ }
+ echo "<ul>\n";
+
+ return TRUE;
+}
+
+function urlencode_without_plus($url) {
+ return str_replace('+', '%20', urlencode($url));
+}
+
+function fetch_build($db, $master, $builder_name, $build_id, $build_number) {
+ flush();
+ $builder_name = urlencode_without_plus($builder_name);
+ $build_json = fetch_and_parse_test_results_json("http://$master/json/builders/$builder_name/builds/$build_number");
+ if (!$build_json || !array_key_exists('times', $build_json) || count($build_json['times']) != 2)
+ return FALSE;
+
+ $revision = NULL;
+ $slavename = NULL;
+ foreach ($build_json['properties'] as $property) {
+ if ($property[0] == 'got_revision')
+ $revision = $property[1];
+ if ($property[0] == 'slavename')
+ $slavename = $property[2];
+ }
+ if (!$revision || !$slavename)
+ return FALSE;
+
+ $start_time = float_to_time($build_json['times'][0]);
+ $end_time = float_to_time($build_json['times'][1]);
+
+ flush();
+ $full_results = fetch_and_parse_test_results_json("http://$master/results/$builder_name/r$revision%20($build_number)/full_results.json", TRUE);
+ return store_test_results($db, $full_results, $build_id, $revision, $start_time, $end_time, $slavename);
+}
+
+$db = new Database;
+if (!$db->connect())
+ notice('Failed to connect to the database');
+else if (array_key_exists('master', $_GET)) {
+ $master = htmlspecialchars($_GET['master']); // Okay since hostname shoudln't contain any HTML special characters.
+ if (in_array($master, config('masters')))
+ fetch_builders($db, $master);
+ else
+ notice("The master $master not found");
+} else {
+ echo "<ul>\n";
+ foreach (config('masters') as $master)
+ echo "<li><a href=\"?master=$master\">$master</a></li>\n";
+ echo "</ul>\n";
+}
+
+?></div>
+
+</body>
+</html>
--- /dev/null
+<?php
+
+require_once('../include/json-shared.php');
+
+$db = connect();
+$tests = $db->fetch_table('tests');
+
+function fetch_table_and_create_map_by_id($table_name) {
+ global $db;
+
+ $rows = $db->fetch_table($table_name);
+ if (!$rows)
+ return array();
+
+ $results = array();
+ foreach ($rows as $row) {
+ $results[$row['id']] = $row;
+ }
+ return $results;
+}
+
+exit_with_success(array('tests' => $tests,
+ 'builders' => fetch_table_and_create_map_by_id('builders'),
+ 'slaves' => fetch_table_and_create_map_by_id('slaves'),
+ 'repositories' => fetch_table_and_create_map_by_id('repositories')));
+
+?>
--- /dev/null
+<?php
+
+require_once('../include/json-shared.php');
+require_once('../include/test-results.php');
+
+$db = connect();
+
+require_existence_of($_POST, array(
+ 'master' => '/[A-Za-z0-9\.]+/',
+ 'builder_name' => '/^[A-Za-z0-9 \(\)\-_]+$/',
+ 'build_number' => '/^[0-9]+?$/',
+ 'build_slave' => '/^[A-Za-z0-9\-_]+$/',
+ 'revisions' => '/^.+?$/',
+ 'start_time' => '/^[0-9]+(\.[0-9]+)?$/',
+ 'end_time' => '/^[0-9]+(\.[0-9]+)?$/'));
+
+if (!array_key_exists('file', $_FILES) or !array_key_exists('tmp_name', $_FILES['file']) or count($_FILES['file']['tmp_name']) <= 0)
+ exit_with_error('ResultsJSONNotIncluded');
+
+$revisions = json_decode($_POST['revisions'], true);
+foreach ($revisions as $repository_name => $revision_data) {
+ require_format('repository_name', $repository_name, '/^\w+$/');
+ require_existence_of($revision_data, array(
+ 'revision' => '/^[a-z0-9]+$/',
+ 'timestamp' => '/^[a-z0-9\-\.:TZ]+$/',
+ ), 'revision');
+}
+
+$test_results = fetch_and_parse_test_results_json($_FILES['file']['tmp_name']);
+if (!$test_results)
+ exit_with_error('InvalidResultsJSON');
+
+$start_time = float_to_time($_POST['start_time']);
+$end_time = float_to_time($_POST['end_time']);
+
+$build_id = add_build($db, $_POST['master'], $_POST['builder_name'], intval($_POST['build_number']));
+if (!$build_id)
+ exit_with_error('FailedToInsertBuild', array('master' => $_POST['master'], 'builderName' => $_POST['builder_name'], 'buildNumber' => $_POST['build_number']));
+
+foreach ($revisions as $repository_name => $revision_data) {
+ $repository_id = $db->select_or_insert_row('repositories', NULL, array('name' => $repository_name));
+ if (!$repository_id)
+ exit_with_error('FailedToInsertRepository', array('name' => $repository_name));
+
+ $revision_data = array(
+ 'repository' => $repository_id,
+ 'build' => $build_id,
+ 'value' => $revision_data['revision'],
+ 'time' => array_get($revision_data, 'timestamp'));
+ $db->select_or_insert_row('build_revisions', NULL, array('repository' => $repository_id, 'build' => $build_id), $revision_data, 'value')
+ or exit_with_error('FailedToInsertRevision', array('name' => $repository_name, 'data' => $revision_data));
+}
+
+$slave_id = add_slave($db, $_POST['build_slave']);
+if (!store_test_results($db, $test_results, $build_id, $start_time, $end_time, $slave_id))
+ exit_with_error('FailedToStoreResults', array('buildId' => $build_id));
+
+exit_with_success();
+
+?>
--- /dev/null
+<?php
+
+require_once('../include/json-shared.php');
+
+$db = connect();
+
+require_existence_of($_GET, array('test' => '/[A-Za-z0-9\._\- ]+/'));
+
+$test = $db->select_first_row('tests', NULL, array('name' => $_GET['test']));
+if (!$test)
+ exit_with_error('TestNotFound');
+
+$result_rows = $db->query_and_fetch_all(
+'SELECT results.*, builds.*, array_agg((build_revisions.repository, build_revisions.value, build_revisions.time)) AS revisions
+ FROM results, builds, build_revisions
+ WHERE build_revisions.build = builds.id AND results.test = $1 AND results.build = builds.id
+ GROUP BY results.id, builds.id', array($test['id']));
+if (!$result_rows)
+ exit_with_error('ResultsNotFound');
+
+date_default_timezone_set('UTC');
+function parse_revisions_array($postgres_array) {
+ // e.g. {"(WebKit,131456,\"2012-10-16 14:53:00\")","(Safari,162004,)"}
+ $outer_array = json_decode('[' . trim($postgres_array, '{}') . ']');
+ $revisions = array();
+ foreach ($outer_array as $item) {
+ $name_and_revision = explode(',', trim($item, '()'));
+ $time = strtotime(trim($name_and_revision[2], '"')) * 1000;
+ $revisions[trim($name_and_revision[0], '"')] = array(trim($name_and_revision[1], '"'), $time);
+ }
+ return $revisions;
+}
+
+$builders = array();
+foreach ($result_rows as $result) {
+ array_push(array_ensure_item_has_array($builders, $result['builder']),
+ array('buildTime' => strtotime($result['start_time']) * 1000,
+ 'revisions' => parse_revisions_array($result['revisions']),
+ 'builder' => $result['builder'],
+ 'slave' => $result['slave'],
+ 'buildNumber' => $result['number'],
+ 'actual' => $result['actual'],
+ 'expected' => $result['expected']));
+}
+
+exit_with_success(array('builders' => $builders));
+
+?>
--- /dev/null
+{
+ "debug": true,
+ "database": {
+ "host": "localhost",
+ "port": "5432",
+ "username": "safari-test-history",
+ "password": "password",
+ "name": "safari-test-history-db"
+ },
+ "masters": [
+ "build.webkit.org",
+ "build-safari.apple.com"
+ ]
+}
--- /dev/null
+<?php
+
+// Note: This code is identical to SafariPerfMonitor's db.php
+
+error_reporting(E_ALL | E_STRICT);
+
+function ends_with($str, $key) {
+ return strrpos($str, $key) == strlen($str) - strlen($key);
+}
+
+function ctype_alnum_underscore($str) {
+ return ctype_alnum(str_replace('_', '', $str));
+}
+
+function &array_ensure_item_has_array(&$array, $key) {
+ if (!array_key_exists($key, $array))
+ $array[$key] = array();
+ return $array[$key];
+}
+
+function array_get($array, $key, $default = NULL) {
+ if (!array_key_exists($key, $array))
+ return $default;
+ return $array[$key];
+}
+
+$_config = NULL;
+
+function config($key) {
+ global $_config;
+ if (!$_config)
+ $_config = json_decode(file_get_contents(dirname(__FILE__) . '/config.json'), true);
+ return $_config[$key];
+}
+
+if (config('debug'))
+ ini_set('display_errors', 'On');
+
+class Database
+{
+ private $connection = false;
+
+ function __destruct() {
+ if ($this->connection)
+ pg_close($this->connection);
+ $this->connection = false;
+ }
+
+ function is_true($value) {
+ return $value == 't';
+ }
+
+ function connect() {
+ $databaseConfig = config('database');
+ $this->connection = pg_connect('host=' . $databaseConfig['host'] . ' port=' . $databaseConfig['port']
+ . ' dbname=' . $databaseConfig['name'] . ' user=' . $databaseConfig['username'] . ' password=' . $databaseConfig['password']);
+ return $this->connection ? true : false;
+ }
+
+ private function prefixed_column_names($columns, $prefix = NULL) {
+ if (!$prefix)
+ return join(', ', $columns);
+ return $prefix . '_' . join(', ' . $prefix . '_', $columns);
+ }
+
+ private function prefixed_name($column, $prefix = NULL) {
+ return $prefix ? $prefix . '_' . $column : $column;
+ }
+
+ private function prepare_params($params, &$placeholders, &$values) {
+ $column_names = array_keys($params);
+
+ $i = count($values) + 1;
+ foreach ($column_names as $name) {
+ assert(ctype_alnum_underscore($name));
+ array_push($placeholders, '$' . $i);
+ array_push($values, $params[$name]);
+ $i++;
+ }
+
+ return $column_names;
+ }
+
+ function insert_row($table, $prefix, $params, $returning = 'id') {
+ $placeholders = array();
+ $values = array();
+ $column_names = $this->prepare_params($params, $placeholders, $values);
+
+ assert(!$prefix || ctype_alnum_underscore($prefix));
+ $column_names = $this->prefixed_column_names($column_names, $prefix);
+ $placeholders = join(', ', $placeholders);
+
+ if ($returning) {
+ $returning_column_name = $this->prefixed_name($returning, $prefix);
+ $rows = $this->query_and_fetch_all("INSERT INTO $table ($column_names) VALUES ($placeholders) RETURNING $returning_column_name", $values);
+ return $rows ? $rows[0][$returning_column_name] : NULL;
+ }
+
+ return $this->query_and_get_affected_rows("INSERT INTO $table ($column_names) VALUES ($placeholders)", $values) == 1;
+ }
+
+ function select_or_insert_row($table, $prefix, $select_params, $insert_params = NULL, $returning = 'id') {
+ $values = array();
+
+ $select_placeholders = array();
+ $select_column_names = $this->prepare_params($select_params, $select_placeholders, $values);
+ $select_values = array_slice($values, 0);
+
+ if ($insert_params === NULL)
+ $insert_params = $select_params;
+ $insert_placeholders = array();
+ $insert_column_names = $this->prepare_params($insert_params, $insert_placeholders, $values);
+
+ assert(!!$returning);
+ assert(!$prefix || ctype_alnum_underscore($prefix));
+ $returning_column_name = $returning == '*' ? '*' : $this->prefixed_name($returning, $prefix);
+ $select_column_names = $this->prefixed_column_names($select_column_names, $prefix);
+ $select_placeholders = join(', ', $select_placeholders);
+ $query = "SELECT $returning_column_name FROM $table WHERE ($select_column_names) = ($select_placeholders)";
+
+ $insert_column_names = $this->prefixed_column_names($insert_column_names, $prefix);
+ $insert_placeholders = join(', ', $insert_placeholders);
+ $rows = $this->query_and_fetch_all("INSERT INTO $table ($insert_column_names) SELECT $insert_placeholders WHERE NOT EXISTS
+ ($query) RETURNING $returning_column_name", $values);
+ if (!$rows)
+ $rows = $this->query_and_fetch_all($query, $select_values);
+
+ return $rows ? ($returning == '*' ? $rows[0] : $rows[0][$returning_column_name]) : NULL;
+ }
+
+ function select_first_row($table, $prefix, $params, $order_by = NULL) {
+ $placeholders = array();
+ $values = array();
+ $column_names = join(', ', $this->prepare_params($params, $placeholders, $values));
+ $placeholders = join(', ', $placeholders);
+ $query = "SELECT * FROM $table WHERE ($column_names) = ($placeholders)";
+ if ($order_by) {
+ assert(!ctype_alnum_underscore($order_by));
+ $query .= ' ORDER BY ' . $this->prefixed_name($order_by, $prefix);
+ }
+ $rows = $this->query_and_fetch_all($query . ' LIMIT 1', $values);
+
+ return $rows ? $rows[0] : NULL;
+ }
+
+ function query_and_get_affected_rows($query, $params = array()) {
+ if (!$this->connection)
+ return 0;
+ $result = pg_query_params($this->connection, $query, $params);
+ if (!$result)
+ return 0;
+ return pg_affected_rows($result);
+ }
+
+ function query_and_fetch_all($query, $params = array()) {
+ if (!$this->connection)
+ return false;
+ $result = pg_query_params($this->connection, $query, $params);
+ if (!$result)
+ return false;
+ return pg_fetch_all($result);
+ }
+
+ function fetch_table($table_name, $column_to_be_ordered_by = null) {
+ if (!$this->connection || !ctype_alnum_underscore($table_name) || ($column_to_be_ordered_by && !ctype_alnum_underscore($column_to_be_ordered_by)))
+ return false;
+ $clauses = '';
+ if ($column_to_be_ordered_by)
+ $clauses .= 'ORDER BY ' . $column_to_be_ordered_by;
+ return $this->query_and_fetch_all("SELECT * FROM $table_name $clauses");
+ }
+
+ function begin_transaction() {
+ return $this->connection and pg_query($this->connection, "BEGIN");
+ }
+
+ function commit_transaction() {
+ return $this->connection and pg_query($this->connection, 'COMMIT');
+ }
+
+ function rollback_transaction() {
+ return $this->connection and pg_query($this->connection, 'ROLLBACK');
+ }
+
+}
+
+?>
\ No newline at end of file
--- /dev/null
+DROP TABLE results CASCADE;
+DROP TABLE tests CASCADE;
+DROP TABLE build_revision_map CASCADE;
+DROP TABLE revisions CASCADE;
+DROP TABLE builds CASCADE;
+DROP TABLE slaves CASCADE;
+DROP TABLE builders CASCADE;
+
+CREATE TABLE builders (
+ id serial PRIMARY KEY,
+ master varchar(64) NOT NULL,
+ name varchar(64) NOT NULL UNIQUE);
+
+CREATE TABLE repositories (
+ id serial PRIMARY KEY,
+ name varchar(64) NOT NULL,
+ url varchar(1024),
+ blame_url varchar(1024));
+
+CREATE TABLE slaves (
+ id serial PRIMARY KEY,
+ name varchar(128) NOT NULL UNIQUE);
+
+CREATE TABLE builds (
+ id serial PRIMARY KEY,
+ builder integer REFERENCES builders ON DELETE CASCADE,
+ number integer NOT NULL,
+ start_time timestamp,
+ end_time timestamp,
+ slave integer REFERENCES slaves ON DELETE CASCADE,
+ fetched bool NOT NULL DEFAULT FALSE,
+ CONSTRAINT builder_and_build_number_must_be_unique UNIQUE(builder, number));
+CREATE INDEX build_builder_index ON builds(builder);
+CREATE INDEX build_slave_index ON builds(slave);
+
+CREATE TABLE build_revisions (
+ build integer NOT NULL REFERENCES builds ON DELETE CASCADE,
+ repository integer NOT NULL REFERENCES repositories ON DELETE CASCADE,
+ value varchar(64) NOT NULL,
+ time timestamp,
+ PRIMARY KEY (repository, build));
+CREATE INDEX revision_build_index ON build_revisions(build);
+CREATE INDEX revision_repository_index ON build_revisions(repository);
+
+CREATE TABLE tests (
+ id serial PRIMARY KEY,
+ name varchar(1024) NOT NULL UNIQUE,
+ reftest_type varchar(64));
+
+CREATE TABLE results (
+ id serial PRIMARY KEY,
+ test integer REFERENCES tests ON DELETE CASCADE,
+ build integer REFERENCES builds ON DELETE CASCADE,
+ expected varchar(64) NOT NULL,
+ actual varchar(64) NOT NULL);
+CREATE INDEX results_test ON results(test);
+CREATE INDEX results_build ON results(build);
--- /dev/null
+<?php
+
+require_once('../include/db.php');
+
+header('Content-type: application/json');
+
+function exit_with_error($status, $details = array()) {
+ $details['status'] = $status;
+ echo json_encode($details);
+ exit(1);
+}
+
+function exit_with_success($details = array()) {
+ $details['status'] = 'OK';
+ echo json_encode($details);
+ exit(0);
+}
+
+function connect() {
+ $db = new Database;
+ if (!$db->connect())
+ exit_with_error('DatabaseConnectionError');
+ return $db;
+}
+
+function camel_case_words_separated_by_underscore($name) {
+ return implode('', array_map('ucfirst', explode('_', $name)));
+}
+
+function require_format($key, $value, $pattern) {
+ if (!preg_match($pattern, $value))
+ exit_with_error('Invalid' . camel_case_words_separated_by_underscore($key), array('value' => $value));
+}
+
+function require_existence_of($array, $list_of_arguments, $prefix = '') {
+ if ($prefix)
+ $prefix .= '_';
+ foreach ($list_of_arguments as $key => $pattern) {
+ $name = camel_case_words_separated_by_underscore($prefix . $key);
+ if (!array_key_exists($key, $array))
+ exit_with_error($name . 'NotSpecified');
+ require_format($name, $array[$key], $pattern);
+ }
+}
+
+?>
--- /dev/null
+<?php
+
+require_once('db.php');
+
+function float_to_time($time_in_float) {
+ $time = new DateTime();
+ $time->setTimestamp(floatval($time_in_float));
+ return $time;
+}
+
+function add_build($db, $master, $builder_name, $build_number) {
+ if (!in_array($master, config('masters')))
+ return NULL;
+
+ $builder_id = $db->select_or_insert_row('builders', NULL, array('master' => $master, 'name' => $builder_name));
+ if (!$builder_id)
+ return NULL;
+
+ return $db->select_or_insert_row('builds', NULL, array('builder' => $builder_id, 'number' => $build_number));
+}
+
+function add_slave($db, $name) {
+ return $db->select_or_insert_row('slaves', NULL, array('name' => $name));
+}
+
+function fetch_and_parse_test_results_json($url, $jsonp = FALSE) {
+ $json_contents = file_get_contents($url);
+ if (!$json_contents)
+ return NULL;
+
+ if ($jsonp)
+ $json_contents = preg_replace('/^\w+\(|\);$/', '', $json_contents);
+
+ return json_decode($json_contents, true);
+}
+
+function store_test_results($db, $test_results, $build_id, $start_time, $end_time, $slave_id) {
+ $db->begin_transaction();
+
+ try {
+ foreach ($test_results['tests'] as $name => $subtests)
+ recursively_add_test_results($db, $build_id, $subtests, $name);
+ $db->query_and_get_affected_rows('UPDATE builds SET (start_time, end_time, slave, fetched) = ($1, $2, $3, TRUE) WHERE id = $4',
+ array($start_time->format('Y-m-d H:i:s.u'), $end_time->format('Y-m-d H:i:s.u'), $slave_id, $build_id));
+ $db->commit_transaction();
+ } catch (Exception $e) {
+ $db->rollback_transaction();
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+function recursively_add_test_results($db, $build_id, $tests, $full_name) {
+ if (!array_key_exists('expected', $tests) and !array_key_exists('actual', $tests)) {
+ foreach ($tests as $name => $subtests)
+ recursively_add_test_results($db, $build_id, $subtests, $full_name . '/' . $name);
+ return;
+ }
+
+ $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'))));
+
+ // FIXME: Either use a transaction or check the return value.
+ $db->select_or_insert_row('results', NULL, array('test' => $test_id, 'build' => $build_id,
+ 'expected' => $tests['expected'], 'actual' => $tests['actual']));
+}
+
+?>
--- /dev/null
+<!DOCTYPE html>
+<html>
+<head>
+<title>WebKit Test Results</title>
+<link rel="stylesheet" href="/common.css">
+<script src="js/autocompleter.js"></script>
+<script src="js/build.js"></script>
+<script src="js/dom.js"></script>
+</head>
+<body>
+
+<header id="title">
+<h1><a href="/">WebKit Test Results</a></h1>
+<ul>
+ <li><a href="http://build.webkit.org/waterfall">Waterfall</a></li>
+</ul>
+</header>
+
+<form id="navigationBar" onsubmit="TestResultsView.fetchTest(this['testName'].value);
+ TestResultsView.updateLocationHash();
+ return false;">
+<input id="testName" type="text" size="150" onpaste="pasteHelper(this, event)"
+ placeholder="Type in a test name, or copy and paste test names on results.html or NRWT stdout (including junks)"></form>
+
+<div id="container"></div>
+<div id="tooltipContainer"></div>
+
+<style>
+
+#testName {
+ width: 99%;
+ font-size: 1em;
+ outline: none;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ padding: 5px;
+}
+
+.testResults {
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ padding: 5px;
+ margin: 10px 0px;
+ position: relative;
+}
+
+.closeButton {
+ position: absolute;
+ right: 5px;
+ top: 5px;
+ width: 1em;
+ height: 1em;
+ stroke: #999;
+}
+
+.resultsTable {
+ font-size: small;
+ border-collapse: collapse;
+ border: 2px solid #fff;
+ padding: 0;
+ margin: 0;
+}
+
+.resultsTable caption {
+ font-size: large;
+ font-weight: normal;
+ text-align: left;
+ margin-bottom: 0.3em;
+ white-space: pre;
+}
+
+.resultsTable td,
+.resultsTable th {
+ border: 2px solid #fff;
+ padding: 0;
+ margin: 0;
+}
+
+.resultsTable th,
+.resultsTable .passingRate {
+ font-weight: normal;
+ padding-right: 10px;
+}
+
+.resultsTable th {
+ width: 15em;
+}
+
+.resultsTable .passingRate {
+ width: 3em;
+}
+
+.resultsTable .resultCell {
+ display: inline-block;
+ padding: 0.2em 0.2em;
+}
+
+.resultsTable a {
+ display: block;
+ width: 1em;
+ height: 1.5em;
+ border-radius: 3px;
+}
+
+.resultsTable .PASS a {
+ background-color: #0c3;
+}
+
+.resultsTable .TEXT a {
+ background-color: #c33;
+}
+
+.resultsTable .IMAGE a {
+ background-color: #3cf;
+}
+
+.resultsTable .TEXT.PASS a {
+ background-color: #cf3;
+}
+
+.resultsTable .CRASH a {
+ background-color: #f00;
+}
+
+.candidateWindow {
+ z-index: 999;
+ position: absolute;
+ background: white;
+ color: black;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ margin: 5px 0 0 0;
+ padding: 5px;
+ font-size: 1em;
+ list-style: none;
+}
+
+.candidateWindow em {
+ background-color: #ccc;
+ font-style: normal;
+}
+
+.candidateWindow .selected {
+ background-color: #0cf;
+ color: white;
+}
+
+#tooltipContainer {
+ position: absolute;
+}
+
+.tooltip {
+ position: relative;
+ border-radius: 5px;
+ padding: 5px;
+ opacity: 0.9;
+ background: #333;
+ color: #eee;
+ font-size: small;
+ line-height: 130%;
+}
+
+.tooltip:after {
+ position: absolute;
+ width: 0;
+ height: 0;
+ left: 50%;
+ margin-left: -9px;
+ bottom: -19px;
+ content: "";
+ display: block;
+ border-style: solid;
+ border-width: 10px;
+ border-color: #333 transparent transparent transparent;
+}
+
+.tooltip ul,
+.tooltip li {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+}
+
+.tooltip a {
+ color: white;
+ text-shadow: none;
+ text-decoration: underline;
+}
+
+</style>
+<script>
+
+var TestResultsView = new (function () {
+ var tooltipContainer = document.getElementById('tooltipContainer');
+
+ tooltipContainer.onclick = function (event) { event.insideTooltip = true; }
+ document.addEventListener('click', function (event) {
+ if (!event.insideTooltip)
+ tooltipContainer.style.display = 'none';
+ });
+
+ window.addEventListener('hashchange', function (event) {
+ TestResultsView.locationHashChanged();
+ });
+
+ this._tooltipContainer = tooltipContainer;
+ this._tests = [];
+ this._oldHash = null;
+ this._builders = [];
+ this._slaves = [];
+ this._repositories = [];
+});
+
+TestResultsView.setBuilders = function (builders) {
+ this._builders = builders;
+}
+
+TestResultsView.setSlaves = function (slaves) {
+ this._slaves = slaves;
+}
+
+TestResultsView.setRepositories = function (repositories) {
+ this._repositories = repositories;
+}
+
+TestResultsView.showTooltip = function (anchor, contentElement) {
+ var tooltipContainer = this._tooltipContainer;
+ tooltipContainer.style.display = null;
+
+ while (tooltipContainer.firstChild)
+ tooltipContainer.removeChild(tooltipContainer.firstChild);
+ tooltipContainer.appendChild(contentElement);
+
+ var rect = anchor.getBoundingClientRect();
+ tooltipContainer.style.left = (rect.left - contentElement.offsetWidth / 2 + anchor.offsetWidth / 2) + 'px';
+ tooltipContainer.style.top = (rect.top - 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 slave = result['slave'];
+ var build = result['buildNumber'];
+ var actual = result['actual'];
+ var expected = result['expected'];
+ var anchor = element('a', {'href': this._urlFromBuilder('result', master, builder, revision, build)});
+ anchor.onmouseenter = function () {
+ var repositoryById = TestResultsView._repositories;
+ var formattedRevisions = result.build.formattedRevisions(previousResult ? previousResult.build : null);
+ var revisionDescription = '';
+ for (var repositoryName in formattedRevisions) {
+ var revision = formattedRevisions[repositoryName];
+ if (revisionDescription)
+ revisionDescription += ', ';
+ if (revision.url)
+ revisionDescription += repositoryName + ': ' + revision.url;
+ else
+ revisionDescription += 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]),
+ ])
+ ]));
+ }
+ var cell = element('span', {
+ 'class': actual + ' resultCell',
+ }, [anchor]);
+ return cell;
+}
+
+TestResultsView._populatePane = function(testName, results, section) {
+ var table = element('table', {'class': 'resultsTable'}, [element('caption', [testName])]);
+ var resultsByBuilder = results['builders'];
+ for (var builderId in resultsByBuilder) {
+ var results = resultsByBuilder[builderId];
+ for (var i = 0; i < results.length; i++) {
+ results[i].build = new TestBuild(this._repositories, this._builders, results[i]);
+ }
+
+ var sortedResults = results.sort(function (result1, result2) { return result1.build.time() - result2.build.time(); });
+ var cells = new Array(sortedResults.length);
+ var builder = this._builders[builderId];
+ for (var i = 0; i < sortedResults.length; i++)
+ cells[i] = this._createResultCell(builder.master, builder.name, sortedResults[i], sortedResults[i - 1]);
+
+ var passCount = cells.filter(function (cell) { return cell.className == 'PASS'; }).length;
+ var passingRate = Math.round(passCount / cells.length * 100) + '%';
+ table.appendChild(element('tr', [element('th', [builder.name]), element('td', {'class': 'passingRate'}, [passingRate]),
+ element('td', cells)]));
+ // FIXME: Add a master name if there is more than one.
+ }
+ section.appendChild(table);
+}
+
+TestResultsView.fetchTest = function (testName) {
+ if (this._tests.indexOf(testName) >= 0)
+ return;
+
+ var self = this;
+
+ var closeButton = element('div', {'class': 'closeButton'});
+ closeButton.innerHTML = '<svg viewBox="0 0 100 100"><g stroke-width="10">'
+ + '<circle cx="50" cy="50" r="45" fill="transparent"></circle><polygon points="30,30 70,70"></polygon>'
+ + '<polygon points="30,70 70,30"></polygon></g></svg>';
+ closeButton.addEventListener('click', function (event) {
+ self._removeTest(testName);
+ section.parentNode.removeChild(section);
+ event.preventDefault();
+ });
+ var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton]);
+
+ document.getElementById('container').appendChild(section);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", 'api/results.php?test=' + testName, true);
+ xhr.onload = function(event) {
+ var response = JSON.parse(xhr.response);
+ if (response['status'] != 'OK') {
+ section.appendChild(text('Failed to load results for ' + testName + ': ' + response['status']));
+ return;
+ }
+
+ self._populatePane(testName, response, section);
+ }
+ xhr.send();
+ this._tests.push(testName);
+}
+
+TestResultsView._removeTest = function (testName) {
+ var index = this._tests.indexOf(testName);
+ if (index < 0)
+ return;
+ this._tests.splice(index, 1);
+ this.updateLocationHash();
+}
+
+TestResultsView.fetchTests = function (testNames, doNotUpdateHash) {
+ for (var i = 0; i < testNames.length; i++)
+ this.fetchTest(testNames[i], doNotUpdateHash);
+ this.updateLocationHash();
+}
+
+TestResultsView.updateLocationHash = function () {
+ var params = {
+ 'tests': this._tests.join(',')
+ };
+ var hash = '';
+ for (var key in params)
+ hash += decodeURIComponent(key) + '=' + decodeURIComponent(params[key]);
+ location.hash = hash;
+ this._oldHash = location.hash;
+}
+
+TestResultsView.locationHashChanged = function () {
+ var newHash = location.hash;
+ if (newHash == this._oldHash)
+ return;
+ this._oldHash = newHash;
+ this._tests = [];
+ document.getElementById('container').innerHTML = '';
+ this.loadTestsFromLocationHash();
+}
+
+TestResultsView.loadTestsFromLocationHash = function () {
+ var parsed = {};
+ location.hash.substr(1).split('&').forEach(function (component) {
+ var equalPosition = component.indexOf('=');
+ if (equalPosition < 0)
+ return;
+ var name = decodeURIComponent(component.substr(0, equalPosition));
+ parsed[name] = decodeURIComponent(component.substr(equalPosition + 1));
+ });
+ if (!parsed['tests'])
+ return;
+ var doNotUpdateHash = true;
+ this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
+}
+
+function fetchManifest(callback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", 'api/manifest.php', true);
+ xhr.onload = function(event) {
+ var response = JSON.parse(xhr.response);
+ if (response['status'] != 'OK') {
+ alert('Failed to load manifest:' + response['status']);
+ console.log(response);
+ return;
+ }
+ callback(response);
+ }
+ xhr.send();
+}
+
+fetchManifest(function (response) {
+ var testNames = response['tests'].map(function (test) { return test['name']; })
+ var input = document.getElementById('testName');
+ input.autocompleter = new Autocompleter(input, testNames);
+
+ TestResultsView.setBuilders(response['builders']);
+ TestResultsView.setSlaves(response['slaves']);
+ TestResultsView.setRepositories(response['repositories']);
+ TestResultsView.loadTestsFromLocationHash();
+});
+
+function pasteHelper(input, event) {
+ function removeJunkFromNRWTStdout(input) {
+ return input.replace(/(\[[\w ]+\])|(.+\:.+)/g, '').replace(/^[ \t]+|[ \t]+$/gm, '');
+ }
+
+ function removeJunkFromResultsPage(input) {
+ return input.replace(/(^[^\/]+$)|(^\+)/gm, '').replace(/\([^)]+\)/g, '').replace(/\s+[A-Za-z]+(\s+[A-Za-z]+)*\s*$/gm, '');
+ }
+
+ var text = event.clipboardData.getData('text/plain');
+ if (text.indexOf('\n') < 0)
+ return;
+
+ var urls = removeJunkFromResultsPage(removeJunkFromNRWTStdout(text)).split('\n');
+ TestResultsView.fetchTests(urls.filter(function (url) { return url.length; }));
+ event.preventDefault();
+}
+
+</script>
+</body>
+</html>
\ No newline at end of file
--- /dev/null
+function Autocompleter(inputElement, list) {
+ this._inputElement = inputElement;
+ this._currentSelection = undefined;
+ this._list = list;
+ this._candidates = [];
+ this._currentFilter = null;
+ this._candidateWindow = null;
+
+ inputElement.addEventListener('focus', this.show.bind(this));
+ inputElement.addEventListener('blur', this.hide.bind(this));
+ inputElement.addEventListener('keyup', this.update.bind(this));
+ inputElement.addEventListener('keydown', this.navigate.bind(this));
+}
+
+Autocompleter.prototype._ensureCandidateWindow = function () {
+ if (this._candidateWindow)
+ return;
+
+ var container = element('ul');
+ container.className = 'candidateWindow';
+ container.style.position = 'absolute';
+ container.style.display = 'none';
+ this._inputElement.parentNode.appendChild(container);
+ this._candidateWindow = container;
+}
+
+Autocompleter.prototype._updateCandidates = function (filter) {
+ if (this._currentFilter == filter)
+ return false;
+
+ var candidates = this._list.filter(function (testName) { return testName.indexOf(filter) >= 0; });
+ if (candidates.length > 50 || candidates.length == 1)
+ candidates = [];
+ this._candidates = candidates;
+ this._currentFilter = filter;
+ this._currentSelection = undefined;
+ return true;
+}
+
+Autocompleter.prototype._showCandidateWindow = function () {
+ if (!this._candidateWindow)
+ return;
+ var style = this._candidateWindow.style;
+ style.display = 'block';
+ style.top = this._inputElement.offsetTop + this._inputElement.offsetHeight + 'px';
+ style.left = this._inputElement.offsetLeft + 'px';
+}
+
+Autocompleter.prototype._createItem = function (candidate) {
+ var tokens = candidate.split(this._currentFilter);
+ var children = [];
+ for (var i = 0; i < tokens.length; i++) {
+ children.push(text(tokens[i]));
+ if (i + 1 < tokens.length)
+ children.push(element('em', [this._currentFilter]));
+ }
+ return element('li', children);
+}
+
+Autocompleter.prototype.show = function () {
+ this.hide();
+ this._ensureCandidateWindow();
+
+ for (var i = 0; i < this._candidates.length; i++)
+ this._candidateWindow.appendChild(this._createItem(this._candidates[i]));
+ this._selectItem(this._currentSelection);
+
+ if (this._candidates.length)
+ this._showCandidateWindow();
+}
+
+Autocompleter.prototype.update = function () {
+ if (this._updateCandidates(this._inputElement.value))
+ this.show();
+}
+
+Autocompleter.prototype.hide = function () {
+ if (!this._candidateWindow)
+ return;
+ this._candidateWindow.style.display = 'none';
+ this._candidateWindow.innerHTML = '';
+}
+
+Autocompleter.prototype._selectItem = function (index) {
+ if (!this._candidateWindow || this._currentSelection == index)
+ return;
+
+ var item = this._candidateWindow.childNodes[index];
+ if (!item)
+ return;
+ item.classList.add('selected');
+
+ var oldItem = this._candidateWindow.childNodes[this._currentSelection];
+ if (oldItem)
+ oldItem.classList.remove('selected');
+
+ this._currentSelection = index;
+}
+
+Autocompleter.prototype.navigate = function (event) {
+ if (event.keyCode == 0x28 /* DOM_VK_DOWN */) {
+ this._selectItem(this._currentSelection === undefined ? 0 : Math.min(this._currentSelection + 1, this._candidates.length - 1));
+ event.preventDefault();
+ } else if (event.keyCode == 0x26 /* DOM_VK_UP */) {
+ this._selectItem(this._currentSelection === undefined ? this._candidates.length - 1 : Math.max(this._currentSelection - 1, 0));
+ event.preventDefault();
+ } else if (event.keyCode == 0x0D /* VK_RETURN */) {
+ if (this._currentSelection === undefined)
+ return;
+ this._inputElement.value = this._candidates[this._currentSelection];
+ } else if (event.keyCode == 0x1B /* DOM_VK_ESCAPE */) {
+ this.hide();
+ }
+}
--- /dev/null
+// FIXME: De-duplicate this code with perf.webkit.org
+function TestBuild(repositories, builders, rawRun) {
+ const revisions = rawRun.revisions;
+ var maxTime = 0;
+ var revisionCount = 0;
+ for (var repositoryName in revisions) {
+ maxTime = Math.max(maxTime, revisions[repositoryName][1]); // Revision is an pair (revision, time)
+ revisionCount++;
+ }
+ var maxTimeString = new Date(maxTime).toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
+ var buildTime = rawRun.buildTime;
+ var buildTimeString = new Date(buildTime).toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
+
+ this.time = function () { return maxTime; }
+ this.formattedTime = function () { return maxTimeString; }
+ this.buildTime = function () { return buildTime; }
+ this.formattedBuildTime = function () { return buildTimeString; }
+ 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;
+ }
+ this.revision = function(repositoryId) { return revisions[repositoryId][0]; }
+ this.formattedRevisions = function (previousBuild) {
+ var result = {};
+ for (var repositoryId in revisions) {
+ var repository = repositories[repositoryId];
+ var repositoryName = repository ? repository.name : 'Unknown repository ' + repositoryId;
+ var previousRevision = previousBuild ? previousBuild.revision(repositoryId) : undefined;
+ var currentRevision = this.revision(repositoryId);
+ if (previousRevision === currentRevision)
+ previousRevision = undefined;
+
+ var revisionPrefix = '';
+ if (currentRevision.length < 10) { // SVN-like revision.
+ revisionPrefix = 'r';
+ if (previousRevision)
+ previousRevision = (parseInt(previousRevision) + 1);
+ }
+
+ var labelForThisRepository = revisionCount ? repositoryName : '';
+ if (previousRevision) {
+ if (labelForThisRepository)
+ labelForThisRepository += ' ';
+ labelForThisRepository += revisionPrefix + previousRevision + '-' + revisionPrefix + currentRevision;
+ } else
+ labelForThisRepository += ' @ ' + revisionPrefix + currentRevision;
+
+ var url;
+ if (repository) {
+ if (previousRevision)
+ url = (repository['blameUrl'] || '').replace(/\$1/g, previousRevision).replace(/\$2/g, currentRevision);
+ else
+ url = (repository['url'] || '').replace(/\$1/g, currentRevision);
+ }
+
+ result[repositoryName] = {
+ 'label': labelForThisRepository,
+ 'currentRevision': currentRevision,
+ 'previousRevision': previousRevision,
+ 'url': url,
+ };
+ }
+ return result;
+ }
+}
--- /dev/null
+function text(text) {
+ return document.createTextNode(text);
+}
+
+function element(elementName, attributesOrChildNodes, childNodes) {
+ var element = document.createElement(elementName);
+
+ if (attributesOrChildNodes instanceof Array)
+ childNodes = attributesOrChildNodes;
+ else if (attributesOrChildNodes) {
+ for (var attributeName in attributesOrChildNodes)
+ element.setAttribute(attributeName, attributesOrChildNodes[attributeName]);
+ }
+
+ if (childNodes) {
+ for (var i = 0; i < childNodes.length; i++) {
+ if (typeof(childNodes[i]) === 'string')
+ element.appendChild(document.createTextNode(childNodes[i]));
+ else
+ element.appendChild(childNodes[i]);
+ }
+ }
+ return element;
+}