Allow a port to run tests with a custom device setup
[WebKit-https.git] / Tools / Scripts / webkitpy / layout_tests / controllers / single_test_runner.py
1 # Copyright (C) 2011 Google Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
5 # met:
6 #
7 #     * Redistributions of source code must retain the above copyright
8 # notice, this list of conditions and the following disclaimer.
9 #     * Redistributions in binary form must reproduce the above
10 # copyright notice, this list of conditions and the following disclaimer
11 # in the documentation and/or other materials provided with the
12 # distribution.
13 #     * Neither the name of Google Inc. nor the names of its
14 # contributors may be used to endorse or promote products derived from
15 # this software without specific prior written permission.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29
30 import logging
31 import re
32 import time
33
34 from webkitpy.layout_tests.controllers import test_result_writer
35 from webkitpy.port.driver import DriverInput, DriverOutput
36 from webkitpy.layout_tests.models import test_expectations
37 from webkitpy.layout_tests.models import test_failures
38 from webkitpy.layout_tests.models.test_results import TestResult
39
40
41 _log = logging.getLogger(__name__)
42
43
44 def run_single_test(port, options, results_directory, worker_name, driver, test_input, stop_when_done):
45     runner = SingleTestRunner(port, options, results_directory, worker_name, driver, test_input, stop_when_done)
46     return runner.run()
47
48
49 class SingleTestRunner(object):
50     (ALONGSIDE_TEST, PLATFORM_DIR, VERSION_DIR, UPDATE) = ('alongside', 'platform', 'version', 'update')
51
52     def __init__(self, port, options, results_directory, worker_name, driver, test_input, stop_when_done):
53         self._port = port
54         self._filesystem = port.host.filesystem
55         self._options = options
56         self._results_directory = results_directory
57         self._driver = driver
58         self._worker_name = worker_name
59         self._test_name = test_input.test_name
60         self._should_run_pixel_test = test_input.should_run_pixel_test
61         self._reference_files = test_input.reference_files
62         self._stop_when_done = stop_when_done
63         self._timeout = test_input.timeout
64
65         if self._reference_files:
66             # Detect and report a test which has a wrong combination of expectation files.
67             # For example, if 'foo.html' has two expectation files, 'foo-expected.html' and
68             # 'foo-expected.txt', we should warn users. One test file must be used exclusively
69             # in either layout tests or reftests, but not in both.
70             for suffix in ('.txt', '.png', '.wav'):
71                 expected_filename = self._port.expected_filename(self._test_name, suffix)
72                 if self._filesystem.exists(expected_filename):
73                     _log.error('%s is a reftest, but has an unused expectation file. Please remove %s.', self._test_name, expected_filename)
74
75     def _expected_driver_output(self):
76         return DriverOutput(self._port.expected_text(self._test_name),
77                                  self._port.expected_image(self._test_name),
78                                  self._port.expected_checksum(self._test_name),
79                                  self._port.expected_audio(self._test_name))
80
81     def _should_fetch_expected_checksum(self):
82         return self._should_run_pixel_test and not (self._options.new_baseline or self._options.reset_results)
83
84     def _driver_input(self):
85         # The image hash is used to avoid doing an image dump if the
86         # checksums match, so it should be set to a blank value if we
87         # are generating a new baseline.  (Otherwise, an image from a
88         # previous run will be copied into the baseline."""
89         image_hash = None
90         if self._should_fetch_expected_checksum():
91             image_hash = self._port.expected_checksum(self._test_name)
92         return DriverInput(self._test_name, self._timeout, image_hash, self._should_run_pixel_test)
93
94     def run(self):
95         if self._reference_files:
96             if self._port.get_option('no_ref_tests') or self._options.reset_results:
97                 reftest_type = set([reference_file[0] for reference_file in self._reference_files])
98                 result = TestResult(self._test_name, reftest_type=reftest_type)
99                 result.type = test_expectations.SKIP
100                 return result
101             return self._run_reftest()
102         if self._options.reset_results:
103             return self._run_rebaseline()
104         return self._run_compare_test()
105
106     def _run_compare_test(self):
107         driver_output = self._driver.run_test(self._driver_input(), self._stop_when_done)
108         expected_driver_output = self._expected_driver_output()
109
110         if self._options.ignore_metrics:
111             expected_driver_output.strip_metrics()
112             driver_output.strip_metrics()
113
114         patterns = self._port.logging_patterns_to_strip()
115         expected_driver_output.strip_patterns(patterns)
116         driver_output.strip_patterns(patterns)
117
118         driver_output.strip_stderror_patterns(self._port.stderr_patterns_to_strip())
119
120         test_result = self._compare_output(expected_driver_output, driver_output)
121         if self._options.new_test_results:
122             self._add_missing_baselines(test_result, driver_output)
123         test_result_writer.write_test_result(self._filesystem, self._port, self._results_directory, self._test_name, driver_output, expected_driver_output, test_result.failures)
124         return test_result
125
126     def _run_rebaseline(self):
127         driver_output = self._driver.run_test(self._driver_input(), self._stop_when_done)
128         failures = self._handle_error(driver_output)
129         test_result_writer.write_test_result(self._filesystem, self._port, self._results_directory, self._test_name, driver_output, None, failures)
130         # FIXME: It the test crashed or timed out, it might be better to avoid
131         # to write new baselines.
132         self._overwrite_baselines(driver_output)
133         return TestResult(self._test_name, failures, driver_output.test_time, driver_output.has_stderr(), pid=driver_output.pid)
134
135     _render_tree_dump_pattern = re.compile(r"^layer at \(\d+,\d+\) size \d+x\d+\n")
136
137     def _add_missing_baselines(self, test_result, driver_output):
138         missingImage = test_result.has_failure_matching_types(test_failures.FailureMissingImage, test_failures.FailureMissingImageHash)
139         if test_result.has_failure_matching_types(test_failures.FailureMissingResult):
140             self._save_baseline_data(driver_output.text, '.txt', self._location_for_new_baseline(driver_output.text, '.txt'))
141         if test_result.has_failure_matching_types(test_failures.FailureMissingAudio):
142             self._save_baseline_data(driver_output.audio, '.wav', self._location_for_new_baseline(driver_output.audio, '.wav'))
143         if missingImage:
144             self._save_baseline_data(driver_output.image, '.png', self._location_for_new_baseline(driver_output.image, '.png'))
145
146     def _location_for_new_baseline(self, data, extension):
147         if self._options.add_platform_exceptions:
148             return self.VERSION_DIR
149         if extension == '.png':
150             return self.PLATFORM_DIR
151         if extension == '.wav':
152             return self.ALONGSIDE_TEST
153         if extension == '.txt' and self._render_tree_dump_pattern.match(data):
154             return self.PLATFORM_DIR
155         return self.ALONGSIDE_TEST
156
157     def _overwrite_baselines(self, driver_output):
158         location = self.VERSION_DIR if self._options.add_platform_exceptions else self.UPDATE
159         self._save_baseline_data(driver_output.text, '.txt', location)
160         self._save_baseline_data(driver_output.audio, '.wav', location)
161         if self._should_run_pixel_test:
162             self._save_baseline_data(driver_output.image, '.png', location)
163
164     def _save_baseline_data(self, data, extension, location):
165         if data is None:
166             return
167         port = self._port
168         fs = self._filesystem
169         if location == self.ALONGSIDE_TEST:
170             output_dir = fs.dirname(port.abspath_for_test(self._test_name))
171         elif location == self.VERSION_DIR:
172             output_dir = fs.join(port.baseline_version_dir(), fs.dirname(self._test_name))
173         elif location == self.PLATFORM_DIR:
174             output_dir = fs.join(port.baseline_platform_dir(), fs.dirname(self._test_name))
175         elif location == self.UPDATE:
176             output_dir = fs.dirname(port.expected_filename(self._test_name, extension))
177         else:
178             raise AssertionError('unrecognized baseline location: %s' % location)
179
180         fs.maybe_make_directory(output_dir)
181         output_basename = fs.basename(fs.splitext(self._test_name)[0] + "-expected" + extension)
182         output_path = fs.join(output_dir, output_basename)
183         _log.info('Writing new expected result "%s"' % port.relative_test_filename(output_path))
184         port.update_baseline(output_path, data)
185
186     def _handle_error(self, driver_output, reference_filename=None):
187         """Returns test failures if some unusual errors happen in driver's run.
188
189         Args:
190           driver_output: The output from the driver.
191           reference_filename: The full path to the reference file which produced the driver_output.
192               This arg is optional and should be used only in reftests until we have a better way to know
193               which html file is used for producing the driver_output.
194         """
195         failures = []
196         fs = self._filesystem
197         if driver_output.timeout:
198             failures.append(test_failures.FailureTimeout(bool(reference_filename)))
199
200         if reference_filename:
201             testname = self._port.relative_test_filename(reference_filename)
202         else:
203             testname = self._test_name
204
205         if driver_output.crash:
206             failures.append(test_failures.FailureCrash(bool(reference_filename),
207                                                        driver_output.crashed_process_name,
208                                                        driver_output.crashed_pid))
209             if driver_output.error:
210                 _log.debug("%s %s crashed, (stderr lines):" % (self._worker_name, testname))
211             else:
212                 _log.debug("%s %s crashed, (no stderr)" % (self._worker_name, testname))
213         elif driver_output.error:
214             _log.debug("%s %s output stderr lines:" % (self._worker_name, testname))
215         for line in driver_output.error.splitlines():
216             _log.debug("  %s" % line)
217         return failures
218
219     def _compare_output(self, expected_driver_output, driver_output):
220         failures = []
221         failures.extend(self._handle_error(driver_output))
222
223         if driver_output.crash:
224             # Don't continue any more if we already have a crash.
225             # In case of timeouts, we continue since we still want to see the text and image output.
226             return TestResult(self._test_name, failures, driver_output.test_time, driver_output.has_stderr(), pid=driver_output.pid)
227
228         failures.extend(self._compare_text(expected_driver_output.text, driver_output.text))
229         failures.extend(self._compare_audio(expected_driver_output.audio, driver_output.audio))
230         if self._should_run_pixel_test:
231             failures.extend(self._compare_image(expected_driver_output, driver_output))
232         return TestResult(self._test_name, failures, driver_output.test_time, driver_output.has_stderr(), pid=driver_output.pid)
233
234     def _compare_text(self, expected_text, actual_text):
235         failures = []
236         if (expected_text and actual_text and
237             # Assuming expected_text is already normalized.
238             self._port.do_text_results_differ(expected_text, self._get_normalized_output_text(actual_text))):
239             failures.append(test_failures.FailureTextMismatch())
240         elif actual_text and not expected_text:
241             failures.append(test_failures.FailureMissingResult())
242         return failures
243
244     def _compare_audio(self, expected_audio, actual_audio):
245         failures = []
246         if (expected_audio and actual_audio and
247             self._port.do_audio_results_differ(expected_audio, actual_audio)):
248             failures.append(test_failures.FailureAudioMismatch())
249         elif actual_audio and not expected_audio:
250             failures.append(test_failures.FailureMissingAudio())
251         return failures
252
253     def _get_normalized_output_text(self, output):
254         """Returns the normalized text output, i.e. the output in which
255         the end-of-line characters are normalized to "\n"."""
256         # Running tests on Windows produces "\r\n".  The "\n" part is helpfully
257         # changed to "\r\n" by our system (Python/Cygwin), resulting in
258         # "\r\r\n", when, in fact, we wanted to compare the text output with
259         # the normalized text expectation files.
260         return output.replace("\r\r\n", "\r\n").replace("\r\n", "\n")
261
262     # FIXME: This function also creates the image diff. Maybe that work should
263     # be handled elsewhere?
264     def _compare_image(self, expected_driver_output, driver_output):
265         failures = []
266         # If we didn't produce a hash file, this test must be text-only.
267         if driver_output.image_hash is None:
268             return failures
269         if not expected_driver_output.image:
270             failures.append(test_failures.FailureMissingImage())
271         elif not expected_driver_output.image_hash:
272             failures.append(test_failures.FailureMissingImageHash())
273         elif driver_output.image_hash != expected_driver_output.image_hash:
274             diff_result = self._port.diff_image(expected_driver_output.image, driver_output.image)
275             err_str = diff_result[2]
276             if err_str:
277                 _log.warning('  %s : %s' % (self._test_name, err_str))
278                 failures.append(test_failures.FailureImageHashMismatch())
279                 driver_output.error = (driver_output.error or '') + err_str
280             else:
281                 driver_output.image_diff = diff_result[0]
282                 if driver_output.image_diff:
283                     failures.append(test_failures.FailureImageHashMismatch(diff_result[1]))
284                 else:
285                     # See https://bugs.webkit.org/show_bug.cgi?id=69444 for why this isn't a full failure.
286                     _log.warning('  %s -> pixel hash failed (but diff passed)' % self._test_name)
287         return failures
288
289     def _run_reftest(self):
290         test_output = self._driver.run_test(self._driver_input(), self._stop_when_done)
291         total_test_time = 0
292         reference_output = None
293         test_result = None
294
295         # A reftest can have multiple match references and multiple mismatch references;
296         # the test fails if any mismatch matches and all of the matches don't match.
297         # To minimize the number of references we have to check, we run all of the mismatches first,
298         # then the matches, and short-circuit out as soon as we can.
299         # Note that sorting by the expectation sorts "!=" before "==" so this is easy to do.
300
301         putAllMismatchBeforeMatch = sorted
302         reference_test_names = []
303         for expectation, reference_filename in putAllMismatchBeforeMatch(self._reference_files):
304             reference_test_name = self._port.relative_test_filename(reference_filename)
305             reference_test_names.append(reference_test_name)
306             reference_output = self._driver.run_test(DriverInput(reference_test_name, self._timeout, None, should_run_pixel_test=True), self._stop_when_done)
307             test_result = self._compare_output_with_reference(reference_output, test_output, reference_filename, expectation == '!=')
308
309             if (expectation == '!=' and test_result.failures) or (expectation == '==' and not test_result.failures):
310                 break
311             total_test_time += test_result.test_run_time
312
313         assert(reference_output)
314         test_result_writer.write_test_result(self._filesystem, self._port, self._results_directory, self._test_name, test_output, reference_output, test_result.failures)
315         reftest_type = set([reference_file[0] for reference_file in self._reference_files])
316         return TestResult(self._test_name, test_result.failures, total_test_time + test_result.test_run_time, test_result.has_stderr, reftest_type=reftest_type, pid=test_result.pid, references=reference_test_names)
317
318     def _compare_output_with_reference(self, reference_driver_output, actual_driver_output, reference_filename, mismatch):
319         total_test_time = reference_driver_output.test_time + actual_driver_output.test_time
320         has_stderr = reference_driver_output.has_stderr() or actual_driver_output.has_stderr()
321         failures = []
322         failures.extend(self._handle_error(actual_driver_output))
323         if failures:
324             # Don't continue any more if we already have crash or timeout.
325             return TestResult(self._test_name, failures, total_test_time, has_stderr)
326         failures.extend(self._handle_error(reference_driver_output, reference_filename=reference_filename))
327         if failures:
328             return TestResult(self._test_name, failures, total_test_time, has_stderr, pid=actual_driver_output.pid)
329
330         if not reference_driver_output.image_hash and not actual_driver_output.image_hash:
331             failures.append(test_failures.FailureReftestNoImagesGenerated(reference_filename))
332         elif mismatch:
333             if reference_driver_output.image_hash == actual_driver_output.image_hash:
334                 diff_result = self._port.diff_image(reference_driver_output.image, actual_driver_output.image, tolerance=0)
335                 if not diff_result[0]:
336                     failures.append(test_failures.FailureReftestMismatchDidNotOccur(reference_filename))
337                 else:
338                     _log.warning("  %s -> ref test hashes matched but diff failed" % self._test_name)
339
340         elif reference_driver_output.image_hash != actual_driver_output.image_hash:
341             diff_result = self._port.diff_image(reference_driver_output.image, actual_driver_output.image, tolerance=0)
342             if diff_result[0]:
343                 failures.append(test_failures.FailureReftestMismatch(reference_filename))
344             else:
345                 _log.warning("  %s -> ref test hashes didn't match but diff passed" % self._test_name)
346
347         return TestResult(self._test_name, failures, total_test_time, has_stderr, pid=actual_driver_output.pid)