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 7739c4f..1fe0c6f 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 fca78f3..57ccd44 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 0ee6691..603d22b 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 76294b7..211c9d2 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 77bfc1d..5d71f20 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 384e137..0c0236b 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 ce822cf..30e12fd 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 {