2010-08-24 Dirk Pranke <dpranke@chromium.org>
[WebKit.git] / WebKitTools / Scripts / webkitpy / layout_tests / layout_package / test_failures.py
1 #!/usr/bin/env python
2 # Copyright (C) 2010 Google Inc. All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 #     * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 #     * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 #     * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 """Classes for failures that occur during tests."""
31
32 import os
33 import test_expectations
34
35
36 def determine_result_type(failure_list):
37     """Takes a set of test_failures and returns which result type best fits
38     the list of failures. "Best fits" means we use the worst type of failure.
39
40     Returns:
41       one of the test_expectations result types - PASS, TEXT, CRASH, etc."""
42
43     if not failure_list or len(failure_list) == 0:
44         return test_expectations.PASS
45
46     failure_types = [type(f) for f in failure_list]
47     if FailureCrash in failure_types:
48         return test_expectations.CRASH
49     elif FailureTimeout in failure_types:
50         return test_expectations.TIMEOUT
51     elif (FailureMissingResult in failure_types or
52           FailureMissingImage in failure_types or
53           FailureMissingImageHash in failure_types):
54         return test_expectations.MISSING
55     else:
56         is_text_failure = FailureTextMismatch in failure_types
57         is_image_failure = (FailureImageHashIncorrect in failure_types or
58                             FailureImageHashMismatch in failure_types)
59         if is_text_failure and is_image_failure:
60             return test_expectations.IMAGE_PLUS_TEXT
61         elif is_text_failure:
62             return test_expectations.TEXT
63         elif is_image_failure:
64             return test_expectations.IMAGE
65         else:
66             raise ValueError("unclassifiable set of failures: "
67                              + str(failure_types))
68
69
70 class TestFailure(object):
71     """Abstract base class that defines the failure interface."""
72
73     @staticmethod
74     def message():
75         """Returns a string describing the failure in more detail."""
76         raise NotImplementedError
77
78     def result_html_output(self, filename):
79         """Returns an HTML string to be included on the results.html page."""
80         raise NotImplementedError
81
82     def should_kill_dump_render_tree(self):
83         """Returns True if we should kill DumpRenderTree before the next
84         test."""
85         return False
86
87     def relative_output_filename(self, filename, modifier):
88         """Returns a relative filename inside the output dir that contains
89         modifier.
90
91         For example, if filename is fast\dom\foo.html and modifier is
92         "-expected.txt", the return value is fast\dom\foo-expected.txt
93
94         Args:
95           filename: relative filename to test file
96           modifier: a string to replace the extension of filename with
97
98         Return:
99           The relative windows path to the output filename
100         """
101         return os.path.splitext(filename)[0] + modifier
102
103
104 class FailureWithType(TestFailure):
105     """Base class that produces standard HTML output based on the test type.
106
107     Subclasses may commonly choose to override the ResultHtmlOutput, but still
108     use the standard OutputLinks.
109     """
110
111     def __init__(self):
112         TestFailure.__init__(self)
113
114     # Filename suffixes used by ResultHtmlOutput.
115     OUT_FILENAMES = []
116
117     def output_links(self, filename, out_names):
118         """Returns a string holding all applicable output file links.
119
120         Args:
121           filename: the test filename, used to construct the result file names
122           out_names: list of filename suffixes for the files. If three or more
123               suffixes are in the list, they should be [actual, expected, diff,
124               wdiff]. Two suffixes should be [actual, expected], and a
125               single item is the [actual] filename suffix.
126               If out_names is empty, returns the empty string.
127         """
128         # FIXME: Seems like a bad idea to separate the display name data
129         # from the path data by hard-coding the display name here
130         # and passing in the path information via out_names.
131         links = ['']
132         uris = [self.relative_output_filename(filename, fn) for
133                 fn in out_names]
134         if len(uris) > 1:
135             links.append("<a href='%s'>expected</a>" % uris[1])
136         if len(uris) > 0:
137             links.append("<a href='%s'>actual</a>" % uris[0])
138         if len(uris) > 2:
139             links.append("<a href='%s'>diff</a>" % uris[2])
140         if len(uris) > 3:
141             links.append("<a href='%s'>wdiff</a>" % uris[3])
142         if len(uris) > 4:
143             links.append("<a href='%s'>pretty diff</a>" % uris[4])
144         return ' '.join(links)
145
146     def result_html_output(self, filename):
147         return self.message() + self.output_links(filename, self.OUT_FILENAMES)
148
149
150 class FailureTimeout(TestFailure):
151     """Test timed out.  We also want to restart DumpRenderTree if this
152     happens."""
153
154     @staticmethod
155     def message():
156         return "Test timed out"
157
158     def result_html_output(self, filename):
159         return "<strong>%s</strong>" % self.message()
160
161     def should_kill_dump_render_tree(self):
162         return True
163
164
165 class FailureCrash(TestFailure):
166     """Test shell crashed."""
167
168     @staticmethod
169     def message():
170         return "Test shell crashed"
171
172     def result_html_output(self, filename):
173         # TODO(tc): create a link to the minidump file
174         stack = self.relative_output_filename(filename, "-stack.txt")
175         return "<strong>%s</strong> <a href=%s>stack</a>" % (self.message(),
176                                                              stack)
177
178     def should_kill_dump_render_tree(self):
179         return True
180
181
182 class FailureMissingResult(FailureWithType):
183     """Expected result was missing."""
184     OUT_FILENAMES = ["-actual.txt"]
185
186     @staticmethod
187     def message():
188         return "No expected results found"
189
190     def result_html_output(self, filename):
191         return ("<strong>%s</strong>" % self.message() +
192                 self.output_links(filename, self.OUT_FILENAMES))
193
194
195 class FailureTextMismatch(FailureWithType):
196     """Text diff output failed."""
197     # Filename suffixes used by ResultHtmlOutput.
198     # FIXME: Why don't we use the constants from TestTypeBase here?
199     OUT_FILENAMES = ["-actual.txt", "-expected.txt", "-diff.txt"]
200     OUT_FILENAMES_WDIFF = ["-actual.txt", "-expected.txt", "-diff.txt",
201                            "-wdiff.html", "-pretty-diff.html"]
202
203     def __init__(self, has_wdiff):
204         FailureWithType.__init__(self)
205         if has_wdiff:
206             self.OUT_FILENAMES = self.OUT_FILENAMES_WDIFF
207
208     @staticmethod
209     def message():
210         return "Text diff mismatch"
211
212
213 class FailureMissingImageHash(FailureWithType):
214     """Actual result hash was missing."""
215     # Chrome doesn't know to display a .checksum file as text, so don't bother
216     # putting in a link to the actual result.
217     OUT_FILENAMES = []
218
219     @staticmethod
220     def message():
221         return "No expected image hash found"
222
223     def result_html_output(self, filename):
224         return "<strong>%s</strong>" % self.message()
225
226
227 class FailureMissingImage(FailureWithType):
228     """Actual result image was missing."""
229     OUT_FILENAMES = ["-actual.png"]
230
231     @staticmethod
232     def message():
233         return "No expected image found"
234
235     def result_html_output(self, filename):
236         return ("<strong>%s</strong>" % self.message() +
237                 self.output_links(filename, self.OUT_FILENAMES))
238
239
240 class FailureImageHashMismatch(FailureWithType):
241     """Image hashes didn't match."""
242     OUT_FILENAMES = ["-actual.png", "-expected.png", "-diff.png"]
243
244     @staticmethod
245     def message():
246         # We call this a simple image mismatch to avoid confusion, since
247         # we link to the PNGs rather than the checksums.
248         return "Image mismatch"
249
250
251 class FailureImageHashIncorrect(FailureWithType):
252     """Actual result hash is incorrect."""
253     # Chrome doesn't know to display a .checksum file as text, so don't bother
254     # putting in a link to the actual result.
255     OUT_FILENAMES = []
256
257     @staticmethod
258     def message():
259         return "Images match, expected image hash incorrect. "
260
261     def result_html_output(self, filename):
262         return "<strong>%s</strong>" % self.message()