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