c9f4107998b1dd03f865036a6f00b77a00342c65
[WebKit.git] / WebKitTools / Scripts / webkitpy / layout_tests / test_types / image_diff.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 """Compares the image output of a test to the expected image output.
31
32 Compares hashes for the generated and expected images. If the output doesn't
33 match, returns FailureImageHashMismatch and outputs both hashes into the layout
34 test results directory.
35 """
36
37 from __future__ import with_statement
38
39 import codecs
40 import errno
41 import logging
42 import os
43 import shutil
44
45 from webkitpy.layout_tests.layout_package import test_failures
46 from webkitpy.layout_tests.test_types import test_type_base
47
48 # Cache whether we have the image_diff executable available.
49 _compare_available = True
50 _compare_msg_printed = False
51
52 _log = logging.getLogger("webkitpy.layout_tests.test_types.image_diff")
53
54
55 class ImageDiff(test_type_base.TestTypeBase):
56
57     def _copy_output_png(self, test_filename, source_image, extension):
58         """Copies result files into the output directory with appropriate
59         names.
60
61         Args:
62           test_filename: the test filename
63           source_file: path to the image file (either actual or expected)
64           extension: extension to indicate -actual.png or -expected.png
65         """
66         self._make_output_directory(test_filename)
67         dest_image = self.output_filename(test_filename, extension)
68
69         try:
70             shutil.copyfile(source_image, dest_image)
71         except IOError, e:
72             # A missing expected PNG has already been recorded as an error.
73             if errno.ENOENT != e.errno:
74                 raise
75
76     def _save_baseline_files(self, filename, png_path, checksum,
77                              generate_new_baseline):
78         """Saves new baselines for the PNG and checksum.
79
80         Args:
81           filename: test filename
82           png_path: path to the actual PNG result file
83           checksum: value of the actual checksum result
84           generate_new_baseline: whether to generate a new, platform-specific
85             baseline, or update the existing one
86         """
87         with open(png_path, "rb") as png_file:
88             png_data = png_file.read()
89         self._save_baseline_data(filename, png_data, ".png", encoding=None,
90                                  generate_new_baseline=generate_new_baseline)
91         self._save_baseline_data(filename, checksum, ".checksum",
92                                  encoding="ascii",
93                                  generate_new_baseline=generate_new_baseline)
94
95     def _create_image_diff(self, port, filename, configuration):
96         """Creates the visual diff of the expected/actual PNGs.
97
98         Args:
99           filename: the name of the test
100           configuration: Debug or Release
101         Returns True if the files are different, False if they match
102         """
103         diff_filename = self.output_filename(filename,
104           self.FILENAME_SUFFIX_COMPARE)
105         actual_filename = self.output_filename(filename,
106           self.FILENAME_SUFFIX_ACTUAL + '.png')
107         expected_filename = self.output_filename(filename,
108           self.FILENAME_SUFFIX_EXPECTED + '.png')
109
110         result = True
111         try:
112             _compare_available = True
113             result = port.diff_image(expected_filename, actual_filename,
114                                      diff_filename)
115         except ValueError:
116             _compare_available = False
117
118         global _compare_msg_printed
119         if not _compare_available and not _compare_msg_printed:
120             _compare_msg_printed = True
121             print('image_diff not found. Make sure you have a ' +
122                   configuration + ' build of the image_diff executable.')
123
124         return result
125
126     def compare_output(self, port, filename, output, test_args, configuration):
127         """Implementation of CompareOutput that checks the output image and
128         checksum against the expected files from the LayoutTest directory.
129         """
130         failures = []
131
132         # If we didn't produce a hash file, this test must be text-only.
133         if test_args.hash is None:
134             return failures
135
136         # If we're generating a new baseline, we pass.
137         if test_args.new_baseline or test_args.reset_results:
138             self._save_baseline_files(filename, test_args.png_path,
139                                       test_args.hash, test_args.new_baseline)
140             return failures
141
142         # Compare hashes.
143         expected_hash_file = self._port.expected_filename(filename,
144                                                           '.checksum')
145         expected_png_file = self._port.expected_filename(filename, '.png')
146
147         # FIXME: We repeat this pattern often, we should share code.
148         try:
149             with codecs.open(expected_hash_file, "r", "ascii") as file:
150                 expected_hash = file.read()
151         except IOError, e:
152             if errno.ENOENT != e.errno:
153                 raise
154             expected_hash = ''
155
156
157         if not os.path.isfile(expected_png_file):
158             # Report a missing expected PNG file.
159             self.write_output_files(port, filename, '.checksum',
160                                     test_args.hash, expected_hash,
161                                     encoding="ascii",
162                                     print_text_diffs=False)
163             self._copy_output_png(filename, test_args.png_path, '-actual.png')
164             failures.append(test_failures.FailureMissingImage(self))
165             return failures
166         elif test_args.hash == expected_hash:
167             # Hash matched (no diff needed, okay to return).
168             return failures
169
170         self.write_output_files(port, filename, '.checksum',
171                                 test_args.hash, expected_hash,
172                                 encoding="ascii",
173                                 print_text_diffs=False)
174         self._copy_output_png(filename, test_args.png_path, '-actual.png')
175         self._copy_output_png(filename, expected_png_file, '-expected.png')
176
177         # Even though we only use the result in one codepath below but we
178         # still need to call CreateImageDiff for other codepaths.
179         images_are_different = self._create_image_diff(port, filename, configuration)
180         if expected_hash == '':
181             failures.append(test_failures.FailureMissingImageHash(self))
182         elif test_args.hash != expected_hash:
183             if images_are_different:
184                 failures.append(test_failures.FailureImageHashMismatch(self))
185             else:
186                 failures.append(test_failures.FailureImageHashIncorrect(self))
187
188         return failures
189
190     def diff_files(self, port, file1, file2):
191         """Diff two image files exactly.
192
193         Args:
194           file1, file2: full paths of the files to compare.
195
196         Returns:
197           True if two files are different.
198           False otherwise.
199         """
200         try:
201             return port.diff_image(file1, file2, None, 0)
202         except ValueError, e:
203             return True