Rename WebKitTools to Tools
[WebKit-https.git] / Tools / 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 import cPickle
36
37
38 def determine_result_type(failure_list):
39     """Takes a set of test_failures and returns which result type best fits
40     the list of failures. "Best fits" means we use the worst type of failure.
41
42     Returns:
43       one of the test_expectations result types - PASS, TEXT, CRASH, etc."""
44
45     if not failure_list or len(failure_list) == 0:
46         return test_expectations.PASS
47
48     failure_types = [type(f) for f in failure_list]
49     if FailureCrash in failure_types:
50         return test_expectations.CRASH
51     elif FailureTimeout in failure_types:
52         return test_expectations.TIMEOUT
53     elif (FailureMissingResult in failure_types or
54           FailureMissingImage in failure_types or
55           FailureMissingImageHash in failure_types):
56         return test_expectations.MISSING
57     else:
58         is_text_failure = FailureTextMismatch in failure_types
59         is_image_failure = (FailureImageHashIncorrect in failure_types or
60                             FailureImageHashMismatch in failure_types)
61         if is_text_failure and is_image_failure:
62             return test_expectations.IMAGE_PLUS_TEXT
63         elif is_text_failure:
64             return test_expectations.TEXT
65         elif is_image_failure:
66             return test_expectations.IMAGE
67         else:
68             raise ValueError("unclassifiable set of failures: "
69                              + str(failure_types))
70
71
72 class TestFailure(object):
73     """Abstract base class that defines the failure interface."""
74
75     @staticmethod
76     def loads(s):
77         """Creates a TestFailure object from the specified string."""
78         return cPickle.loads(s)
79
80     @staticmethod
81     def message():
82         """Returns a string describing the failure in more detail."""
83         raise NotImplementedError
84
85     def __eq__(self, other):
86         return self.__class__.__name__ == other.__class__.__name__
87
88     def __ne__(self, other):
89         return self.__class__.__name__ != other.__class__.__name__
90
91     def dumps(self):
92         """Returns the string/JSON representation of a TestFailure."""
93         return cPickle.dumps(self)
94
95     def result_html_output(self, filename):
96         """Returns an HTML string to be included on the results.html page."""
97         raise NotImplementedError
98
99     def should_kill_dump_render_tree(self):
100         """Returns True if we should kill DumpRenderTree before the next
101         test."""
102         return False
103
104     def relative_output_filename(self, filename, modifier):
105         """Returns a relative filename inside the output dir that contains
106         modifier.
107
108         For example, if filename is fast\dom\foo.html and modifier is
109         "-expected.txt", the return value is fast\dom\foo-expected.txt
110
111         Args:
112           filename: relative filename to test file
113           modifier: a string to replace the extension of filename with
114
115         Return:
116           The relative windows path to the output filename
117         """
118         return os.path.splitext(filename)[0] + modifier
119
120
121 class FailureWithType(TestFailure):
122     """Base class that produces standard HTML output based on the test type.
123
124     Subclasses may commonly choose to override the ResultHtmlOutput, but still
125     use the standard OutputLinks.
126     """
127
128     def __init__(self):
129         TestFailure.__init__(self)
130
131     # Filename suffixes used by ResultHtmlOutput.
132     OUT_FILENAMES = ()
133
134     def output_links(self, filename, out_names):
135         """Returns a string holding all applicable output file links.
136
137         Args:
138           filename: the test filename, used to construct the result file names
139           out_names: list of filename suffixes for the files. If three or more
140               suffixes are in the list, they should be [actual, expected, diff,
141               wdiff]. Two suffixes should be [actual, expected], and a
142               single item is the [actual] filename suffix.
143               If out_names is empty, returns the empty string.
144         """
145         # FIXME: Seems like a bad idea to separate the display name data
146         # from the path data by hard-coding the display name here
147         # and passing in the path information via out_names.
148         #
149         # FIXME: Also, we don't know for sure that these files exist,
150         # and we shouldn't be creating links to files that don't exist
151         # (for example, if we don't actually have wdiff output).
152         links = ['']
153         uris = [self.relative_output_filename(filename, fn) for
154                 fn in out_names]
155         if len(uris) > 1:
156             links.append("<a href='%s'>expected</a>" % uris[1])
157         if len(uris) > 0:
158             links.append("<a href='%s'>actual</a>" % uris[0])
159         if len(uris) > 2:
160             links.append("<a href='%s'>diff</a>" % uris[2])
161         if len(uris) > 3:
162             links.append("<a href='%s'>wdiff</a>" % uris[3])
163         if len(uris) > 4:
164             links.append("<a href='%s'>pretty diff</a>" % uris[4])
165         return ' '.join(links)
166
167     def result_html_output(self, filename):
168         return self.message() + self.output_links(filename, self.OUT_FILENAMES)
169
170
171 class FailureTimeout(TestFailure):
172     """Test timed out.  We also want to restart DumpRenderTree if this
173     happens."""
174
175     @staticmethod
176     def message():
177         return "Test timed out"
178
179     def result_html_output(self, filename):
180         return "<strong>%s</strong>" % self.message()
181
182     def should_kill_dump_render_tree(self):
183         return True
184
185
186 class FailureCrash(TestFailure):
187     """Test shell crashed."""
188
189     @staticmethod
190     def message():
191         return "Test shell crashed"
192
193     def result_html_output(self, filename):
194         # FIXME: create a link to the minidump file
195         stack = self.relative_output_filename(filename, "-stack.txt")
196         return "<strong>%s</strong> <a href=%s>stack</a>" % (self.message(),
197                                                              stack)
198
199     def should_kill_dump_render_tree(self):
200         return True
201
202
203 class FailureMissingResult(FailureWithType):
204     """Expected result was missing."""
205     OUT_FILENAMES = ("-actual.txt",)
206
207     @staticmethod
208     def message():
209         return "No expected results found"
210
211     def result_html_output(self, filename):
212         return ("<strong>%s</strong>" % self.message() +
213                 self.output_links(filename, self.OUT_FILENAMES))
214
215
216 class FailureTextMismatch(FailureWithType):
217     """Text diff output failed."""
218     # Filename suffixes used by ResultHtmlOutput.
219     # FIXME: Why don't we use the constants from TestTypeBase here?
220     OUT_FILENAMES = ("-actual.txt", "-expected.txt", "-diff.txt",
221                      "-wdiff.html", "-pretty-diff.html")
222
223     @staticmethod
224     def message():
225         return "Text diff mismatch"
226
227
228 class FailureMissingImageHash(FailureWithType):
229     """Actual result hash was missing."""
230     # Chrome doesn't know to display a .checksum file as text, so don't bother
231     # putting in a link to the actual result.
232
233     @staticmethod
234     def message():
235         return "No expected image hash found"
236
237     def result_html_output(self, filename):
238         return "<strong>%s</strong>" % self.message()
239
240
241 class FailureMissingImage(FailureWithType):
242     """Actual result image was missing."""
243     OUT_FILENAMES = ("-actual.png",)
244
245     @staticmethod
246     def message():
247         return "No expected image found"
248
249     def result_html_output(self, filename):
250         return ("<strong>%s</strong>" % self.message() +
251                 self.output_links(filename, self.OUT_FILENAMES))
252
253
254 class FailureImageHashMismatch(FailureWithType):
255     """Image hashes didn't match."""
256     OUT_FILENAMES = ("-actual.png", "-expected.png", "-diff.png")
257
258     @staticmethod
259     def message():
260         # We call this a simple image mismatch to avoid confusion, since
261         # we link to the PNGs rather than the checksums.
262         return "Image mismatch"
263
264
265 class FailureImageHashIncorrect(FailureWithType):
266     """Actual result hash is incorrect."""
267     # Chrome doesn't know to display a .checksum file as text, so don't bother
268     # putting in a link to the actual result.
269
270     @staticmethod
271     def message():
272         return "Images match, expected image hash incorrect. "
273
274     def result_html_output(self, filename):
275         return "<strong>%s</strong>" % self.message()
276
277 # Convenient collection of all failure classes for anything that might
278 # need to enumerate over them all.
279 ALL_FAILURE_CLASSES = (FailureTimeout, FailureCrash, FailureMissingResult,
280                        FailureTextMismatch, FailureMissingImageHash,
281                        FailureMissingImage, FailureImageHashMismatch,
282                        FailureImageHashIncorrect)