2010-08-11 Tony Chang <tony@chromium.org>
[WebKit-https.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         if test_args.show_sources:
148             _log.debug('Using %s' % expected_hash_file)
149             _log.debug('Using %s' % expected_png_file)
150
151         # FIXME: We repeat this pattern often, we should share code.
152         try:
153             with codecs.open(expected_hash_file, "r", "ascii") as file:
154                 expected_hash = file.read()
155         except IOError, e:
156             if errno.ENOENT != e.errno:
157                 raise
158             expected_hash = ''
159
160
161         if not os.path.isfile(expected_png_file):
162             # Report a missing expected PNG file.
163             self.write_output_files(port, filename, '.checksum',
164                                     test_args.hash, expected_hash,
165                                     encoding="ascii",
166                                     print_text_diffs=False)
167             self._copy_output_png(filename, test_args.png_path, '-actual.png')
168             failures.append(test_failures.FailureMissingImage(self))
169             return failures
170         elif test_args.hash == expected_hash:
171             # Hash matched (no diff needed, okay to return).
172             return failures
173
174         self.write_output_files(port, filename, '.checksum',
175                                 test_args.hash, expected_hash,
176                                 encoding="ascii",
177                                 print_text_diffs=False)
178         self._copy_output_png(filename, test_args.png_path, '-actual.png')
179         self._copy_output_png(filename, expected_png_file, '-expected.png')
180
181         # Even though we only use the result in one codepath below but we
182         # still need to call CreateImageDiff for other codepaths.
183         images_are_different = self._create_image_diff(port, filename, configuration)
184         if expected_hash == '':
185             failures.append(test_failures.FailureMissingImageHash(self))
186         elif test_args.hash != expected_hash:
187             if images_are_different:
188                 failures.append(test_failures.FailureImageHashMismatch(self))
189             else:
190                 failures.append(test_failures.FailureImageHashIncorrect(self))
191
192         return failures
193
194     def diff_files(self, port, file1, file2):
195         """Diff two image files exactly.
196
197         Args:
198           file1, file2: full paths of the files to compare.
199
200         Returns:
201           True if two files are different.
202           False otherwise.
203         """
204         try:
205             return port.diff_image(file1, file2, None, 0)
206         except ValueError, e:
207             return True