Build fix. Update is_flaky when it's NULL.
[WebKit-https.git] / Websites / test-results / public / include / test-results.php
1 <?php
2
3 require_once('db.php');
4
5 function float_to_time($time_in_float) {
6     $time = new DateTime();
7     $time->setTimestamp(floatval($time_in_float));
8     return $time;
9 }
10
11 function add_builder($db, $master, $builder_name) {
12     if (!in_array($master, config('masters')))
13         return NULL;
14
15     return $db->select_or_insert_row('builders', NULL, array('master' => $master, 'name' => $builder_name));
16 }
17
18 function add_build($db, $builder_id, $build_number) {
19     return $db->select_or_insert_row('builds', NULL, array('builder' => $builder_id, 'number' => $build_number));
20 }
21
22 function add_slave($db, $name) {
23     return $db->select_or_insert_row('slaves', NULL, array('name' => $name));
24 }
25
26 function fetch_and_parse_test_results_json($url, $jsonp = FALSE) {
27     $json_contents = file_get_contents($url);
28     if (!$json_contents)
29         return NULL;
30
31     if ($jsonp)
32         $json_contents = preg_replace('/^\w+\(|\);$/', '', $json_contents);
33
34     return json_decode($json_contents, true);
35 }
36
37 function store_test_results($db, $test_results, $build_id, $start_time, $end_time, $slave_id) {
38     $db->begin_transaction();
39
40     try {
41         recursively_add_test_results($db, $build_id, $test_results['tests'], '');
42
43         $db->query_and_get_affected_rows(
44             'UPDATE builds SET (start_time, end_time, slave) = (least($1, start_time), greatest($2, end_time), $3) WHERE id = $4',
45             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));
46         $db->commit_transaction();
47     } catch (Exception $e) {
48         $db->rollback_transaction();
49         return FALSE;
50     }
51
52     return TRUE;
53 }
54
55 function recursively_add_test_results($db, $build_id, $tests, $full_name) {
56     if (!array_key_exists('expected', $tests) and !array_key_exists('actual', $tests)) {
57         $prefix = $full_name ? $full_name . '/' : '';
58         foreach ($tests as $name => $subtests) {
59             require_format('test_name', $name, '/^[A-Za-z0-9 +_\-\.]+$/');
60             recursively_add_test_results($db, $build_id, $subtests, $prefix . $name);
61         }
62         return;
63     }
64
65     require_format('expected_result', $tests['expected'], '/^[A-Za-z \+]+$/');
66     require_format('actual_result', $tests['actual'], '/^[A-Za-z \+]+$/');
67     require_format('test_time', $tests['time'], '/^\d*$/');
68     $modifiers = array_get($tests, 'modifiers');
69     if ($modifiers)
70         require_format('test_modifiers', $modifiers, '/^[A-Za-z0-9 \.\/]+$/');
71     else
72         $modifiers = NULL;
73     $category = 'LayoutTest'; // FIXME: Support other test categories.
74
75     $test_id = $db->select_or_insert_row('tests', NULL,
76         array('name' => $full_name),
77         array('name' => $full_name, 'reftest_type' => json_encode(array_get($tests, 'reftest_type')), 'category' => $category));
78
79     $db->insert_row('results', NULL, array('test' => $test_id, 'build' => $build_id,
80         'expected' => $tests['expected'], 'actual' => $tests['actual'],
81         'time' => $tests['time'], 'modifiers' => $tests['modifiers']));
82 }
83
84 date_default_timezone_set('UTC');
85 function parse_revisions_array($postgres_array) {
86     // e.g. {"(WebKit,131456,\"2012-10-16 14:53:00\")","(Safari,162004,)"}
87     $outer_array = json_decode('[' . trim($postgres_array, '{}') . ']');
88     $revisions = array();
89     foreach ($outer_array as $item) {
90         $name_and_revision = explode(',', trim($item, '()'));
91         $time = strtotime(trim($name_and_revision[2], '"')) * 1000;
92         $revisions[trim($name_and_revision[0], '"')] = array(trim($name_and_revision[1], '"'), $time);
93     }
94     return $revisions;
95 }
96
97 function format_result($result) {
98     return array('buildTime' => strtotime($result['start_time']) * 1000,
99         'revisions' => parse_revisions_array($result['revisions']),
100         'slave' => $result['slave'],
101         'buildNumber' => $result['number'],
102         'actual' => $result['actual'],
103         'expected' => $result['expected'],
104         'time' => $result['time'],
105         'modifiers' => $result['modifiers']);
106 }
107
108 class ResultsJSONWriter {
109     private $fp;
110     private $emitted_results;
111
112     public function __construct($fp) {
113         $this->fp = $fp;
114         $this->emitted_results = FALSE;
115     }
116
117     public function start($builder_id) {
118         fwrite($this->fp, "{\"status\": \"OK\", \"builders\": {\"$builder_id\":{");
119     }
120
121     public function end($total_time) {
122         fwrite($this->fp, "}}, \"totalGenerationTime\": $total_time}");
123     }
124
125     public function add_results_for_test($current_test, $current_results) {
126         if (!count($current_results))
127             return;
128         // FIXME: Why do we need to check the count?
129
130         $prefix = $this->emitted_results ? ",\n" : "";
131         fwrite($this->fp, "$prefix\"$current_test\":");
132         fwrite($this->fp, json_encode($current_results, true));
133
134         $this->emitted_results = TRUE;
135     }
136 }
137
138 class ResultsJSONGenerator {
139     private $db;
140     private $builder_id;
141
142     public function __construct($db, $builder_id)
143     {
144         $this->db = $db;
145         $this->builder_id = $builder_id;
146     }
147
148     public function generate($failure_type)
149     {
150         $start_time = microtime(true);
151
152         if (!$this->builder_id)
153             return FALSE;
154
155         switch ($failure_type) {
156         case 'flaky':
157             $test_rows = $this->db->query_and_fetch_all("SELECT DISTINCT(results.test) FROM results,
158                 (SELECT builds.id FROM builds WHERE builds.builder = $1 GROUP BY builds.id LIMIT 500) as builds
159                 WHERE results.build = builds.id AND results.is_flaky is TRUE",
160                 array($this->builder_id));
161             break;
162         case 'wrongexpectations':
163             // FIXME: three replace here shouldn't be necessary. Do it in webkitpy or report.php at latest.
164             $test_rows = $this->db->query_and_fetch_all("SELECT results.test FROM results WHERE results.build = $1
165                 AND NOT string_to_array(expected, ' ') >=
166                     string_to_array(replace(replace(replace(actual, 'TEXT', 'FAIL'), 'AUDIO', 'FAIL'), 'IMAGE+TEXT', 'FAIL'), ' ')",
167                 array($this->latest_build()));
168             break;
169         default:
170             return FALSE;
171         }
172
173         if (!$test_rows)
174             return TRUE;
175
176         $comma_separated_test_ids = '';
177         foreach ($test_rows as $row) {
178             if ($comma_separated_test_ids)
179                 $comma_separated_test_ids .= ', ';
180             $comma_separated_test_ids .= intval($row['test']);
181         }
182
183         $all_results = $this->db->query(
184         "SELECT results.*, builds.* FROM results
185             JOIN (SELECT builds.*, array_agg((build_revisions.repository, build_revisions.value, build_revisions.time)) AS revisions
186                     FROM builds, build_revisions
187                     WHERE build_revisions.build = builds.id AND builds.builder = $1
188                     GROUP BY builds.id LIMIT 500) as builds ON results.build = builds.id
189             WHERE results.test in ($comma_separated_test_ids)
190             ORDER BY results.test DESC", array($this->builder_id));
191         if (!$all_results)
192             return FALSE;
193
194         $json_fp = $this->open_json_for_failure_type($failure_type);
195         try {
196             return $this->write_jsons($all_results, new ResultsJSONWriter($json_fp), $start_time);
197         } catch (Exception $exception) {
198             fclose($json_fp);
199             throw $exception;
200         }
201         return FALSE;
202     }
203
204     private function latest_build() {
205         $results = $this->db->query_and_fetch_all('SELECT builds.id, max(build_revisions.time) AS latest_revision_time
206             FROM builds, build_revisions
207             WHERE build_revisions.build = builds.id AND builds.builder = $1
208             GROUP BY builds.id
209             ORDER BY latest_revision_time DESC LIMIT 1', array($this->builder_id));
210         if (!$results)
211             return NULL;
212         return $results[0]['id'];
213     }
214
215     private function open_json_for_failure_type($failure_type) {
216         $failing_json_path = configPath('dataDirectory', $this->builder_id . "-$failure_type.json");
217         if (!$failing_json_path)
218             exit_with_error('FailedToDetermineResultsJSONPath', array('builderId' => $this->builder_id, 'failureType' => $failure_type));
219         $fp = fopen($failing_json_path, 'w');
220         if (!$fp)
221             exit_with_error('FailedToOpenResultsJSON', array('builderId' => $this->builder_id, 'failureType' => $failure_type));
222         return $fp;
223     }
224
225     private function write_jsons($all_results, $writer, $start_time) {
226         $writer->start($this->builder_id);
227         $current_test = NULL;
228         $current_results = array();
229         while ($result = $this->db->fetch_next_row($all_results)) {
230             if ($result['test'] != $current_test) {
231                 if ($current_test)
232                     $writer->add_results_for_test($current_test, $current_results);
233                 $current_results = array();
234                 $current_test = $result['test'];
235             }
236             array_push($current_results, format_result($result));
237         }
238         $writer->end(microtime(true) - $start_time);
239         return TRUE;
240     }
241 }
242
243 function update_flakiness_for_build($db, $preceeding_build, $current_build, $succeeding_build) {
244     return $db->query_and_get_affected_rows("UPDATE results
245         SET is_flaky = preceeding_results.actual = succeeding_results.actual AND preceeding_results.actual != results.actual
246         FROM results preceeding_results, results succeeding_results
247         WHERE preceeding_results.build = $1 AND results.build = $2 AND succeeding_results.build = $3
248             AND preceeding_results.test = results.test AND succeeding_results.test = results.test
249             AND (results.is_flaky IS NULL OR results.is_flaky !=
250                     (preceeding_results.actual = succeeding_results.actual AND preceeding_results.actual != results.actual))",
251             array($preceeding_build['id'], $current_build['id'], $succeeding_build['id']));
252 }
253
254 function update_flakiness_after_inserting_build($db, $build_id) {
255     // 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.
256     $ordered_builds = $db->query_and_fetch_all("SELECT builds.id, max(build_revisions.time) AS latest_revision_time
257         FROM builds, build_revisions
258         WHERE build_revisions.build = builds.id AND builds.builder = (SELECT builds.builder FROM builds WHERE id = $1)
259         GROUP BY builds.id ORDER BY latest_revision_time, builds.start_time DESC", array($build_id));
260
261     $current_build = NULL;
262     for ($i = 0; $i < count($ordered_builds); $i++) {
263         if ($ordered_builds[$i]['id'] == $build_id) {
264             $current_build = $i;
265             break;
266         }
267     }
268     if ($current_build === NULL)
269         return NULL;
270
271     $affected_rows = 0;
272     if ($current_build >= 2)
273         $affected_rows += update_flakiness_for_build($db, $ordered_builds[$current_build - 2], $ordered_builds[$current_build - 1], $ordered_builds[$current_build]);
274
275     if ($current_build >= 1 && $current_build + 1 < count($ordered_builds))
276         $affected_rows += update_flakiness_for_build($db, $ordered_builds[$current_build - 1], $ordered_builds[$current_build], $ordered_builds[$current_build + 1]);
277
278     if ($current_build + 2 < count($ordered_builds))
279         $affected_rows += update_flakiness_for_build($db, $ordered_builds[$current_build], $ordered_builds[$current_build + 1], $ordered_builds[$current_build + 2]);
280
281     return $affected_rows;
282 }
283
284 ?>