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