Fix JSON generations on new flakiness dashboard
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 4 Nov 2013 09:16:11 +0000 (09:16 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 4 Nov 2013 09:16:11 +0000 (09:16 +0000)
https://bugs.webkit.org/show_bug.cgi?id=123723

Reviewed by Andreas Kling.

Add is_flaky column on results table so that this column can be used to find flaky tests on a given builder
efficiently without having to through results for all tests in PHP. This column is updated in report.php
when a new build is added. Because is_flaky depends on the preceding and succeeding results, we must update
is_flaky flag on results for builds immediately before and after the new build as well.

To see why, suppose we had two consecutive results [PASS] [PASS]. If we were to insert [FAIL] result between
the two, those two results may also turn into flaky results if they were surrounded by [FAIL]. Similarly,
if we had [PASS] [FAIL] and the second result was marked flaky, inserting new [FAIL] must unmark it.

* init-database.sql: Added is_flaky column to results table with an index. Also added an index on
build_revisions.time as many queries filter results by this quantity. Also set the work_mem to 50MB avoid
disk thrashing while sorting results in various queries.

* public/api/failing-tests.php: Handle builder ids as well as names. Call generate() with failure types.
No longer generates *-failing.json since it's a subset of *-wrongexpectations.json to save time.

* public/api/report.php: Rewritten. Calls update_flakiness_after_inserting_build to update is_flaky flags
on the newly added results.
(store_results): Added.
(main): Added.

* public/include/test-results.php:
(ResultsJSONWriter):
(ResultsJSONWriter::add_results_for_test): Renamed from add_results_for_test_if_matches.
(ResultsJSONGenerator::generate): Takes the failure type. Instead of generating JSONs for all failure types
at once, generate one JSON for the specified type. We generate the list of test ids based on the failure type
and query results based on that. This dramatically cuts down the time spent in PHP.
(ResultsJSONGenerator::latest_build): Added.
(ResultsJSONGenerator::write_jsons): Takes single writer now.
(update_flakiness_for_build): Added.
(update_flakiness_after_inserting_build): Added.

* public/index.html:
(TestResultsView._populateBuilderPane): Emulate *-failing.json upon *-wrongexpectations.json.
(TestResultsView.fetchFailingTestsForBuilder): Ditto.

* public/main.css: Minor style tweaks.
(.testResults): Extend the border that wraps the test results as needed.
(.tooltip): Don't wrap text inside tooltips.

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

Websites/test-results/ChangeLog
Websites/test-results/init-database.sql
Websites/test-results/public/api/failing-tests.php
Websites/test-results/public/api/report.php
Websites/test-results/public/include/test-results.php
Websites/test-results/public/index.html
Websites/test-results/public/main.css

index 7739c4f9ec95cb483848c9cf5360a2289c5ce012..1fe0c6f60b14ff2c91021a274cc9e52df3b91ca0 100644 (file)
@@ -1,3 +1,51 @@
+2013-11-04  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Fix JSON generations on new flakiness dashboard
+        https://bugs.webkit.org/show_bug.cgi?id=123723
+
+        Reviewed by Andreas Kling.
+
+        Add is_flaky column on results table so that this column can be used to find flaky tests on a given builder
+        efficiently without having to through results for all tests in PHP. This column is updated in report.php
+        when a new build is added. Because is_flaky depends on the preceding and succeeding results, we must update
+        is_flaky flag on results for builds immediately before and after the new build as well.
+
+        To see why, suppose we had two consecutive results [PASS] [PASS]. If we were to insert [FAIL] result between
+        the two, those two results may also turn into flaky results if they were surrounded by [FAIL]. Similarly,
+        if we had [PASS] [FAIL] and the second result was marked flaky, inserting new [FAIL] must unmark it.
+
+
+        * init-database.sql: Added is_flaky column to results table with an index. Also added an index on
+        build_revisions.time as many queries filter results by this quantity. Also set the work_mem to 50MB avoid
+        disk thrashing while sorting results in various queries.
+
+        * public/api/failing-tests.php: Handle builder ids as well as names. Call generate() with failure types.
+        No longer generates *-failing.json since it's a subset of *-wrongexpectations.json to save time.
+
+        * public/api/report.php: Rewritten. Calls update_flakiness_after_inserting_build to update is_flaky flags
+        on the newly added results.
+        (store_results): Added.
+        (main): Added.
+
+        * public/include/test-results.php:
+        (ResultsJSONWriter):
+        (ResultsJSONWriter::add_results_for_test): Renamed from add_results_for_test_if_matches.
+        (ResultsJSONGenerator::generate): Takes the failure type. Instead of generating JSONs for all failure types
+        at once, generate one JSON for the specified type. We generate the list of test ids based on the failure type
+        and query results based on that. This dramatically cuts down the time spent in PHP.
+        (ResultsJSONGenerator::latest_build): Added.
+        (ResultsJSONGenerator::write_jsons): Takes single writer now.
+        (update_flakiness_for_build): Added.
+        (update_flakiness_after_inserting_build): Added.
+
+        * public/index.html:
+        (TestResultsView._populateBuilderPane): Emulate *-failing.json upon *-wrongexpectations.json.
+        (TestResultsView.fetchFailingTestsForBuilder): Ditto.
+
+        * public/main.css: Minor style tweaks.
+        (.testResults): Extend the border that wraps the test results as needed.
+        (.tooltip): Don't wrap text inside tooltips.
+
 2013-10-26  Ryosuke Niwa  <rniwa@webkit.org>
 
         Make new bug link in flakiness dashboard configurable
index fca78f38cb8f568d57eacfa2070e9a84519f022f..57ccd44bc60c148d87df218261ace0eb6079622e 100644 (file)
@@ -41,6 +41,7 @@ CREATE TABLE build_revisions (
     PRIMARY KEY (repository, build));
 CREATE INDEX revision_build_index ON build_revisions(build);
 CREATE INDEX revision_repository_index ON build_revisions(repository);
+CREATE INDEX revision_time_index ON build_revisions(time);
 
 CREATE TABLE tests (
     id serial PRIMARY KEY,
@@ -55,6 +56,10 @@ CREATE TABLE results (
     expected varchar(64) NOT NULL,
     actual varchar(64) NOT NULL,
     modifiers varchar(64) NOT NULL,
-    time integer);
+    time integer,
+    is_flaky boolean);
 CREATE INDEX results_test ON results(test);
 CREATE INDEX results_build ON results(build);
+CREATE INDEX results_is_flaky ON results(is_flaky);
+
+SET work_mem='50MB';
index 0ee6691106a9ab3db1b096d9c4d49a04f534016b..603d22b8f97062d5f3df855935fdd38f508b5764 100644 (file)
@@ -5,20 +5,25 @@ require_once('../include/test-results.php');
 
 function main() {
     require_existence_of($_GET, array('builder' => '/^[A-Za-z0-9 \(\)\-_]+$/'));
-    $builder_name = $_GET['builder'];
+    $builder_param = $_GET['builder'];
 
     $db = connect();
-    $builder_row = $db->select_first_row('builders', NULL, array('name' => $builder_name));
-    if (!$builder_row)
-        exit_with_error('BuilderNotFound');
+    $builder_row = $db->select_first_row('builders', NULL, array('name' => $builder_param));
+    if (!$builder_row) {
+        $builder_row = $db->select_first_row('builders', NULL, array('id' => $builder_param));
+        if (!$builder_row)
+            exit_with_error('BuilderNotFound');
+    }
     $builder_id = $builder_row['id'];
 
     $generator = new ResultsJSONGenerator($db, $builder_id);
 
-    if ($generator->generate())
-        exit_with_success();
+    if (!$generator->generate('wrongexpectations'))
+        exit_with_error('ResultsWithWrongExpectationsNotFound', array('builderId' => $builder_id));
+    else if (!$generator->generate('flaky'))
+        exit_with_error('FlakyResultsNotFound', array('builderId' => $builder_id));
     else
-        exit_with_error('ResultsNotFound');
+        exit_with_success();
 }
 
 main();
index 76294b7154e869f6617309c282302aaa0fe2f7a7..211c9d288574ffbb7ff20ec117185966ca758125 100644 (file)
@@ -3,73 +3,87 @@
 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]+)?$/'));
-$master = $_POST['master'];
-$builder_name = $_POST['builder_name'];
-$build_number = intval($_POST['build_number']);
-
-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(str_replace('\\', '', $_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');
+function store_results($db, $master, $builder_name, $build_number, $start_time, $end_time, $revisions, $json_path) {
+    $test_results = fetch_and_parse_test_results_json($json_path);
+    if (!$test_results)
+        exit_with_error('InvalidResultsJSON');
+
+    $builder_id = add_builder($db, $master, $builder_name);
+    if (!$builder_id)
+        exit_with_error('FailedToInsertBuilder', array('master' => $master, 'builderName' => $builder_name));
+
+    $build_id = add_build($db, $builder_id, $build_number);
+    if (!$build_id)
+        exit_with_error('FailedToInsertBuild', array('builderId' => $builder_id, 'buildNumber' => $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));
+
+    return $build_id;
 }
 
-$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']);
-
-$builder_id = add_builder($db, $master, $builder_name);
-if (!$builder_id)
-    exit_with_error('FailedToInsertBuilder', array('master' => $master, 'builderName' => $builder_name));
-
-$build_id = add_build($db, $builder_id, $build_number);
-if (!$build_id)
-    exit_with_error('FailedToInsertBuild', array('builderId' => $builder_id, 'buildNumber' => $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));
+function main() {
+    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]+)?$/'));
+    $master = $_POST['master'];
+    $builder_name = $_POST['builder_name'];
+    $build_number = intval($_POST['build_number']);
+    $start_time = float_to_time($_POST['start_time']);
+    $end_time = float_to_time($_POST['end_time']);
+
+    $revisions = json_decode(str_replace('\\', '', $_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');
+    }
+
+    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');
+    $json_path = $_FILES['file']['tmp_name'];
+
+    $db = connect();
+    $build_id = store_results($db, $master, $builder_name, $build_number, $start_time, $end_time, $revisions, $json_path);
+    @ob_end_clean();
+    ignore_user_abort();
+    ob_start();
+
+    echo_success();
+
+    header('Connection: close');
+    header('Content-Length: ' . ob_get_length());
+
+    @ob_end_flush();
+    flush();
+    if (function_exists('fastcgi_finish_request'))
+        fastcgi_finish_request();
+
+    update_flakiness_after_inserting_build($build_id);
 }
 
-$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));
-
-echo_success();
-
-@ob_end_flush();
-flush();
-if (function_exists('fastcgi_finish_request'))
-    fastcgi_finish_request();
-
-$generator = new ResultsJSONGenerator($db, $builder_id);
-$generator->generate();
+main();
 
 ?>
index 77bfc1df0e6e510df6315e610603d9e1c9f4af64..5d71f20460c097ec9e2169a37c31ec81397c64cc 100644 (file)
@@ -105,7 +105,7 @@ function format_result($result) {
         'modifiers' => $result['modifiers']);
 }
 
-abstract class ResultsJSONWriter {
+class ResultsJSONWriter {
     private $fp;
     private $emitted_results;
 
@@ -122,8 +122,8 @@ abstract class ResultsJSONWriter {
         fwrite($this->fp, "}}, \"totalGenerationTime\": $total_time}");
     }
 
-    public function add_results_for_test_if_matches($current_test, $current_results) {
-        if (!count($current_results) || $this->pass_for_failure_type($current_results))
+    public function add_results_for_test($current_test, $current_results) {
+        if (!count($current_results))
             return;
         // FIXME: Why do we need to check the count?
 
@@ -133,104 +133,86 @@ abstract class ResultsJSONWriter {
 
         $this->emitted_results = TRUE;
     }
-
-    abstract protected function pass_for_failure_type(&$results);
-}
-
-class FlakyResultsJSONWriter extends ResultsJSONWriter {
-    public function __construct($fp) { parent::__construct($fp); }
-    protected function pass_for_failure_type(&$results) {
-        $last_index = count($results) - 1;
-        for ($i = 1; $i < $last_index; $i++) {
-            $previous_actual = $results[$i - 1]['actual'];
-            $next_actual = $results[$i + 1]['actual'];
-            if ($previous_actual == $next_actual && $results[$i]['actual'] != $previous_actual) {
-                $results[$i]['oneOffChange'] = TRUE;
-                return FALSE;
-            }
-        }
-        return TRUE;
-    }
-}
-
-class WrongExpectationsResultsJSONWriter extends ResultsJSONWriter {
-    public function __construct($fp) { parent::__construct($fp); }
-    protected function pass_for_failure_type(&$results) {
-        $latest_expected_result = $results[0]['expected'];
-        $latest_actual_result = $results[0]['actual'];
-
-        if ($latest_expected_result == $latest_actual_result)
-            return TRUE;
-
-        $tokens = explode(' ', $latest_expected_result);
-        return array_search($latest_actual_result, $tokens) !== FALSE
-            || (($latest_actual_result == 'TEXT' || $latest_actual_result == 'TEXT+IMAGE') && array_search('FAIL', $tokens) !== FALSE);
-    }
-}
-
-class FailingResultsJSONWriter extends WrongExpectationsResultsJSONWriter {
-    public function __construct($fp) { parent::__construct($fp); }
-    protected function pass_for_failure_type(&$results) {
-        return $results[0]['actual'] == 'PASS' || parent::pass_for_failure_type($results);
-    }
 }
 
 class ResultsJSONGenerator {
     private $db;
     private $builder_id;
 
-    const MAXIMUM_NUMBER_OF_DAYS = 30;
-
     public function __construct($db, $builder_id)
     {
         $this->db = $db;
         $this->builder_id = $builder_id;
     }
 
-    public function generate()
+    public function generate($failure_type)
     {
         $start_time = microtime(true);
 
         if (!$this->builder_id)
             return FALSE;
 
-        $number_of_days = self::MAXIMUM_NUMBER_OF_DAYS;
+        switch ($failure_type) {
+        case 'flaky':
+            $test_rows = $this->db->query_and_fetch_all("SELECT DISTINCT(results.test) FROM results,
+                (SELECT builds.id FROM builds WHERE builds.builder = $1 GROUP BY builds.id LIMIT 500) as builds
+                WHERE results.build = builds.id AND results.is_flaky is TRUE",
+                array($this->builder_id));
+            break;
+        case 'wrongexpectations':
+            // FIXME: three replace here shouldn't be necessary. Do it in webkitpy or report.php at latest.
+            $test_rows = $this->db->query_and_fetch_all("SELECT results.test FROM results WHERE results.build = $1
+                AND NOT string_to_array(expected, ' ') >=
+                    string_to_array(replace(replace(replace(actual, 'TEXT', 'FAIL'), 'AUDIO', 'FAIL'), 'IMAGE+TEXT', 'FAIL'), ' ')",
+                array($this->latest_build()));
+            break;
+        default:
+            return FALSE;
+        }
+
+        if (!$test_rows)
+            return FALSE;
+
+        $comma_separated_test_ids = '';
+        foreach ($test_rows as $row) {
+            if ($comma_separated_test_ids)
+                $comma_separated_test_ids .= ', ';
+            $comma_separated_test_ids .= intval($row['test']);
+        }
+
         $all_results = $this->db->query(
         "SELECT results.*, builds.* FROM results
             JOIN (SELECT builds.*, array_agg((build_revisions.repository, build_revisions.value, build_revisions.time)) AS revisions,
                     max(build_revisions.time) AS latest_revision_time
                     FROM builds, build_revisions
-                    WHERE build_revisions.build = builds.id AND builds.builder = $1 AND builds.start_time > now() - interval '$number_of_days days'
-                    GROUP BY builds.id) as builds ON results.build = builds.id
+                    WHERE build_revisions.build = builds.id AND builds.builder = $1
+                    GROUP BY builds.id LIMIT 500) as builds ON results.build = builds.id
+            WHERE results.test in ($comma_separated_test_ids)
             ORDER BY results.test, latest_revision_time DESC", array($this->builder_id));
         if (!$all_results)
             return FALSE;
 
-        $failing_json_fp = $this->open_json_for_failure_type('failing');
+        $json_fp = $this->open_json_for_failure_type($failure_type);
         try {
-            $flaky_json_fp = $this->open_json_for_failure_type('flaky');
-            try {
-                $wrongexpectations_json_fp = $this->open_json_for_failure_type('wrongexpectations');
-                try {
-                    return $this->write_jsons($all_results, array(
-                        new FailingResultsJSONWriter($failing_json_fp),
-                        new FlakyResultsJSONWriter($flaky_json_fp),
-                        new WrongExpectationsResultsJSONWriter($wrongexpectations_json_fp)), $start_time);
-                } catch (Exception $exception) {
-                    fclose($wrongexpectations_json_fp);
-                    throw $exception;
-                }
-            } catch (Exception $exception) {
-                fclose($flaky_json_fp);
-                throw $exception;
-            }
+            return $this->write_jsons($all_results, new ResultsJSONWriter($json_fp), $start_time);
         } catch (Exception $exception) {
-            fclose($failing_json_fp);
+            fclose($json_fp);
             throw $exception;
         }
         return FALSE;
     }
 
+    private function latest_build() {
+        $results = $this->db->query_and_fetch_all('SELECT builds.id, max(build_revisions.time) AS latest_revision_time
+            FROM builds, build_revisions
+            WHERE build_revisions.build = builds.id AND builds.builder = $1
+            GROUP BY builds.id
+            ORDER BY latest_revision_time DESC LIMIT 1', array($this->builder_id));
+        if (!$results)
+            return NULL;
+        return $results[0]['id'];
+    }
+
     private function open_json_for_failure_type($failure_type) {
         $failing_json_path = configPath('dataDirectory', $this->builder_id . "-$failure_type.json");
         if (!$failing_json_path)
@@ -241,29 +223,61 @@ class ResultsJSONGenerator {
         return $fp;
     }
 
-    private function write_jsons($all_results, $writers, $start_time) {
-        foreach ($writers as $writer)
-            $writer->start($this->builder_id);
+    private function write_jsons($all_results, $writer, $start_time) {
+        $writer->start($this->builder_id);
         $current_test = NULL;
         $current_results = array();
         while ($result = $this->db->fetch_next_row($all_results)) {
             if ($result['test'] != $current_test) {
-                if ($current_test) {
-                    foreach ($writers as $writer)
-                        $writer->add_results_for_test_if_matches($current_test, $current_results);
-                }
+                if ($current_test)
+                    $writer->add_results_for_test($current_test, $current_results);
                 $current_results = array();
                 $current_test = $result['test'];
             }
             array_push($current_results, format_result($result));
         }
+        $writer->end(microtime(true) - $start_time);
+        return TRUE;
+    }
+}
 
-        $total_time = microtime(true) - $start_time;
-        foreach ($writers as $writer)
-            $writer->end($total_time);
+function update_flakiness_for_build($db, $preceeding_build, $current_build, $succeeding_build) {
+    return $db->query_and_get_affected_rows("UPDATE results
+        SET is_flaky = preceeding_results.actual = succeeding_results.actual AND preceeding_results.actual != results.actual
+        FROM results preceeding_results, results succeeding_results
+        WHERE preceeding_results.build = $1 AND results.build = $2 AND succeeding_results.build = $3
+            AND preceeding_results.test = results.test AND succeeding_results.test = results.test",
+            array($preceeding_build['id'], $current_build['id'], $succeeding_build['id']));
+}
 
-        return TRUE;
+function update_flakiness_after_inserting_build($db, $build_id) {
+    // FIXME: In theory, it's possible for new builds to be inserted between the time this select query is ran and quries are executed by update_flakiness_for_build.
+    $ordered_builds = $db->query_and_fetch_all("SELECT builds.id, max(build_revisions.time) AS latest_revision_time
+        FROM builds, build_revisions
+        WHERE build_revisions.build = builds.id AND builds.builder = (SELECT builds.builder FROM builds WHERE id = $1)
+        GROUP BY builds.id ORDER BY latest_revision_time, builds.start_time DESC", array($build_id));
+
+    $current_build = NULL;
+    for ($i = 0; $i < count($ordered_builds); $i++) {
+        if ($ordered_builds[$i]['id'] == $build_id) {
+            $current_build = $i;
+            break;
+        }
     }
+    if ($current_build === NULL)
+        return NULL;
+
+    $affected_rows = 0;
+    if ($current_build >= 2)
+        $affected_rows += update_flakiness_for_build($db, $ordered_builds[$current_build - 2], $ordered_builds[$current_build - 1], $ordered_builds[$current_build]);
+
+    if ($current_build >= 1 && $current_build + 1 < count($ordered_builds))
+        $affected_rows += update_flakiness_for_build($db, $ordered_builds[$current_build - 1], $ordered_builds[$current_build], $ordered_builds[$current_build + 1]);
+
+    if ($current_build + 2 < count($ordered_builds))
+        $affected_rows += update_flakiness_for_build($db, $ordered_builds[$current_build], $ordered_builds[$current_build + 1], $ordered_builds[$current_build + 2]);
+
+    return $affected_rows;
 }
 
 ?>
index 384e13735a2261e5f335bd2b3e6426eddbf99c54..0c0236b86c097d62f190be17f409645fd76cee11 100644 (file)
@@ -19,9 +19,7 @@
 </ul>
 </header>
 
-<form onsubmit="TestResultsView.fetchTest(this['testName'].value);
-    TestResultsView.updateLocationHash();
-    return false;">
+<form>
 <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="testView"></div>
@@ -399,7 +397,7 @@ TestResultsView._populateBuilderPane = function(builderName, failureType, result
     var self = this;
     this._sortObjectsByName(tests).forEach(function (test) {
         var results = resultsByTests[test.id];
-        if (!results.length)
+        if (!results.length || (failureType == 'failing' && results[0].actual == 'PASS'))
             return;
         self._createBuildsAndComputeSlownessOfResults(builderId, results);
         var externalTestLink = element('a', {'class': 'externalTestLink',
@@ -431,7 +429,7 @@ TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDay
     var self = this;
     var xhr = new XMLHttpRequest();
     var builderId = this._builderByName[builderName].id;
-    xhr.open('GET', 'data/' + builderId + '-' + failureType + '.json', true);  
+    xhr.open('GET', 'data/' + builderId + '-' + (failureType == 'failing' ? 'wrongexpectations' : failureType) + '.json', true);  
     xhr.onload = function(event) {
         if (xhr.status != 200) {
             section.appendChild(text('Failed to load results for ' + builderName + ': ' + xhr.status));
index ce822cf19fed0b583e7d17572991bf2d2c032136..30e12fd5e08a21f7f5cda5dd04b61b61e027d3dd 100644 (file)
@@ -17,6 +17,7 @@
     padding: 5px;
     margin: 1em 0px;
     position: relative;
+    display: inline-block;
 }
 
 #buildersView {
     color: #eee;
     font-size: small;
     line-height: 130%;
+    white-space: pre;
 }
 
 .tooltip:after {