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
32import os
33import test_expectations
34
35
36def 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
70class 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 NotImplemented
77
78    def result_html_output(self, filename):
79        """Returns an HTML string to be included on the results.html page."""
80        raise NotImplemented
81
82    def should_kill_test_shell(self):
83        """Returns True if we should kill the test shell 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
104class 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, test_type):
112        TestFailure.__init__(self)
113        # TODO(ojan): This class no longer needs to know the test_type.
114        self._test_type = test_type
115
116    # Filename suffixes used by ResultHtmlOutput.
117    OUT_FILENAMES = []
118
119    def output_links(self, filename, out_names):
120        """Returns a string holding all applicable output file links.
121
122        Args:
123          filename: the test filename, used to construct the result file names
124          out_names: list of filename suffixes for the files. If three or more
125              suffixes are in the list, they should be [actual, expected, diff,
126              wdiff]. Two suffixes should be [actual, expected], and a
127              single item is the [actual] filename suffix.
128              If out_names is empty, returns the empty string.
129        """
130        links = ['']
131        uris = [self.relative_output_filename(filename, fn) for
132                fn in out_names]
133        if len(uris) > 1:
134            links.append("<a href='%s'>expected</a>" % uris[1])
135        if len(uris) > 0:
136            links.append("<a href='%s'>actual</a>" % uris[0])
137        if len(uris) > 2:
138            links.append("<a href='%s'>diff</a>" % uris[2])
139        if len(uris) > 3:
140            links.append("<a href='%s'>wdiff</a>" % uris[3])
141        return ' '.join(links)
142
143    def result_html_output(self, filename):
144        return self.message() + self.output_links(filename, self.OUT_FILENAMES)
145
146
147class FailureTimeout(TestFailure):
148    """Test timed out.  We also want to restart the test shell if this
149    happens."""
150
151    @staticmethod
152    def message():
153        return "Test timed out"
154
155    def result_html_output(self, filename):
156        return "<strong>%s</strong>" % self.message()
157
158    def should_kill_test_shell(self):
159        return True
160
161
162class FailureCrash(TestFailure):
163    """Test shell crashed."""
164
165    @staticmethod
166    def message():
167        return "Test shell crashed"
168
169    def result_html_output(self, filename):
170        # TODO(tc): create a link to the minidump file
171        stack = self.relative_output_filename(filename, "-stack.txt")
172        return "<strong>%s</strong> <a href=%s>stack</a>" % (self.message(),
173                                                             stack)
174
175    def should_kill_test_shell(self):
176        return True
177
178
179class FailureMissingResult(FailureWithType):
180    """Expected result was missing."""
181    OUT_FILENAMES = ["-actual.txt"]
182
183    @staticmethod
184    def message():
185        return "No expected results found"
186
187    def result_html_output(self, filename):
188        return ("<strong>%s</strong>" % self.message() +
189                self.output_links(filename, self.OUT_FILENAMES))
190
191
192class FailureTextMismatch(FailureWithType):
193    """Text diff output failed."""
194    # Filename suffixes used by ResultHtmlOutput.
195    OUT_FILENAMES = ["-actual.txt", "-expected.txt", "-diff.txt"]
196    OUT_FILENAMES_WDIFF = ["-actual.txt", "-expected.txt", "-diff.txt",
197                           "-wdiff.html"]
198
199    def __init__(self, test_type, has_wdiff):
200        FailureWithType.__init__(self, test_type)
201        if has_wdiff:
202            self.OUT_FILENAMES = self.OUT_FILENAMES_WDIFF
203
204    @staticmethod
205    def message():
206        return "Text diff mismatch"
207
208
209class FailureMissingImageHash(FailureWithType):
210    """Actual result hash was missing."""
211    # Chrome doesn't know to display a .checksum file as text, so don't bother
212    # putting in a link to the actual result.
213    OUT_FILENAMES = []
214
215    @staticmethod
216    def message():
217        return "No expected image hash found"
218
219    def result_html_output(self, filename):
220        return "<strong>%s</strong>" % self.message()
221
222
223class FailureMissingImage(FailureWithType):
224    """Actual result image was missing."""
225    OUT_FILENAMES = ["-actual.png"]
226
227    @staticmethod
228    def message():
229        return "No expected image found"
230
231    def result_html_output(self, filename):
232        return ("<strong>%s</strong>" % self.message() +
233                self.output_links(filename, self.OUT_FILENAMES))
234
235
236class FailureImageHashMismatch(FailureWithType):
237    """Image hashes didn't match."""
238    OUT_FILENAMES = ["-actual.png", "-expected.png", "-diff.png"]
239
240    @staticmethod
241    def message():
242        # We call this a simple image mismatch to avoid confusion, since
243        # we link to the PNGs rather than the checksums.
244        return "Image mismatch"
245
246
247class FailureFuzzyFailure(FailureWithType):
248    """Image hashes didn't match."""
249    OUT_FILENAMES = ["-actual.png", "-expected.png"]
250
251    @staticmethod
252    def message():
253        return "Fuzzy image match also failed"
254
255
256class FailureImageHashIncorrect(FailureWithType):
257    """Actual result hash is incorrect."""
258    # Chrome doesn't know to display a .checksum file as text, so don't bother
259    # putting in a link to the actual result.
260    OUT_FILENAMES = []
261
262    @staticmethod
263    def message():
264        return "Images match, expected image hash incorrect. "
265
266    def result_html_output(self, filename):
267        return "<strong>%s</strong>" % self.message()
268