blob: 07e6389d6b4d52e06fd2315176eb2d836041af68 [file] [log] [blame]
# Copyright (C) 2011 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import logging
import os
from webkitpy.layout_tests.layout_package import test_failures
_log = logging.getLogger(__name__)
def write_test_result(port, filename, driver_output,
expected_driver_output, failures):
"""Write the test result to the result output directory."""
root_output_dir = port.results_directory()
checksums_mismatch_but_images_are_same = False
imagehash_mismatch_failure = None
writer = TestResultWriter(port, root_output_dir, filename)
if driver_output.error:
writer.write_stderr(driver_output.error)
for failure in failures:
# FIXME: Instead of this long 'if' block, each failure class might
# have a responsibility for writing a test result.
if isinstance(failure, (test_failures.FailureMissingResult,
test_failures.FailureTextMismatch)):
writer.write_text_files(driver_output.text, expected_driver_output.text)
writer.create_text_diff_and_write_result(driver_output.text, expected_driver_output.text)
elif isinstance(failure, test_failures.FailureMissingImage):
writer.write_image_files(driver_output.image, expected_image=None)
writer.write_image_hashes(driver_output.image_hash, expected_driver_output.image_hash)
elif isinstance(failure, test_failures.FailureMissingImageHash):
writer.write_image_files(driver_output.image, expected_driver_output.image)
writer.write_image_hashes(driver_output.image_hash, expected_image_hash=None)
elif isinstance(failure, test_failures.FailureImageHashMismatch):
writer.write_image_files(driver_output.image, expected_driver_output.image)
writer.write_image_hashes(driver_output.image_hash, expected_driver_output.image_hash)
images_are_different = writer.create_image_diff_and_write_result(
driver_output.image, expected_driver_output.image)
if not images_are_different:
checksums_mismatch_but_images_are_same = True
imagehash_mismatch_failure = failure
elif isinstance(failure, (test_failures.FailureAudioMismatch,
test_failures.FailureMissingAudio)):
writer.write_audio_files(driver_output.audio, expected_driver_output.audio)
elif isinstance(failure, test_failures.FailureCrash):
if failure.is_reftest:
writer.write_crash_report(expected_driver_output.error)
else:
writer.write_crash_report(driver_output.error)
elif isinstance(failure, test_failures.FailureReftestMismatch):
writer.write_image_files(driver_output.image, expected_driver_output.image)
writer.create_image_diff_and_write_result(driver_output.image, expected_driver_output.image)
writer.copy_file(port.reftest_expected_filename(filename), '-expected.html')
elif isinstance(failure, test_failures.FailureReftestMismatchDidNotOccur):
writer.write_image_files(driver_output.image, expected_image=None)
writer.copy_file(port.reftest_expected_mismatch_filename(filename), '-expected-mismatch.html')
else:
assert isinstance(failure, (test_failures.FailureTimeout,))
# FIXME: This is an ugly hack to handle FailureImageHashIncorrect case.
# Ideally, FailureImageHashIncorrect case should be detected before this
# function is called. But it requires calling create_diff_image() to detect
# whether two images are same or not. So we need this hack until we have a better approach.
if checksums_mismatch_but_images_are_same:
# Replace FailureImageHashMismatch with FailureImageHashIncorrect.
failures.remove(imagehash_mismatch_failure)
failures.append(test_failures.FailureImageHashIncorrect())
class TestResultWriter(object):
"""A class which handles all writing operations to the result directory."""
# Filename pieces when writing failures to the test results directory.
FILENAME_SUFFIX_ACTUAL = "-actual"
FILENAME_SUFFIX_EXPECTED = "-expected"
FILENAME_SUFFIX_DIFF = "-diff"
FILENAME_SUFFIX_WDIFF = "-wdiff.html"
FILENAME_SUFFIX_PRETTY_PATCH = "-pretty-diff.html"
FILENAME_SUFFIX_IMAGE_DIFF = "-diff.png"
def __init__(self, port, root_output_dir, filename):
self._port = port
self._root_output_dir = root_output_dir
self._filename = filename
self._testname = port.relative_test_filename(filename)
def _make_output_directory(self):
"""Creates the output directory (if needed) for a given test filename."""
fs = self._port._filesystem
output_filename = fs.join(self._root_output_dir, self._testname)
self._port.maybe_make_directory(fs.dirname(output_filename))
def output_filename(self, modifier):
"""Returns a filename inside the output dir that contains modifier.
For example, if test name is "fast/dom/foo.html" and modifier is "-expected.txt",
the return value is "/<path-to-root-output-dir>/fast/dom/foo-expected.txt".
Args:
modifier: a string to replace the extension of filename with
Return:
The absolute path to the output filename
"""
fs = self._port._filesystem
output_filename = fs.join(self._root_output_dir, self._testname)
return fs.splitext(output_filename)[0] + modifier
def write_output_files(self, file_type, output, expected):
"""Writes the test output, the expected output in the results directory.
The full output filename of the actual, for example, will be
<filename>-actual<file_type>
For instance,
my_test-actual.txt
Args:
file_type: A string describing the test output file type, e.g. ".txt"
output: A string containing the test output
expected: A string containing the expected test output
"""
self._make_output_directory()
actual_filename = self.output_filename(self.FILENAME_SUFFIX_ACTUAL + file_type)
expected_filename = self.output_filename(self.FILENAME_SUFFIX_EXPECTED + file_type)
fs = self._port._filesystem
if output is not None:
fs.write_binary_file(actual_filename, output)
if expected is not None:
fs.write_binary_file(expected_filename, expected)
def write_stderr(self, error):
fs = self._port._filesystem
filename = self.output_filename("-stderr.txt")
fs.maybe_make_directory(fs.dirname(filename))
fs.write_text_file(filename, error)
def write_crash_report(self, error):
"""Write crash information."""
fs = self._port._filesystem
filename = self.output_filename("-stack.txt")
fs.maybe_make_directory(fs.dirname(filename))
fs.write_text_file(filename, error)
def write_text_files(self, actual_text, expected_text):
self.write_output_files(".txt", actual_text, expected_text)
def create_text_diff_and_write_result(self, actual_text, expected_text):
# FIXME: This function is actually doing the diffs as well as writing results.
# It might be better to extract code which does 'diff' and make it a separate function.
if not actual_text or not expected_text:
return
self._make_output_directory()
file_type = '.txt'
actual_filename = self.output_filename(self.FILENAME_SUFFIX_ACTUAL + file_type)
expected_filename = self.output_filename(self.FILENAME_SUFFIX_EXPECTED + file_type)
fs = self._port._filesystem
# We treat diff output as binary. Diff output may contain multiple files
# in conflicting encodings.
diff = self._port.diff_text(expected_text, actual_text, expected_filename, actual_filename)
diff_filename = self.output_filename(self.FILENAME_SUFFIX_DIFF + file_type)
fs.write_binary_file(diff_filename, diff)
# Shell out to wdiff to get colored inline diffs.
wdiff = self._port.wdiff_text(expected_filename, actual_filename)
wdiff_filename = self.output_filename(self.FILENAME_SUFFIX_WDIFF)
fs.write_binary_file(wdiff_filename, wdiff)
# Use WebKit's PrettyPatch.rb to get an HTML diff.
pretty_patch = self._port.pretty_patch_text(diff_filename)
pretty_patch_filename = self.output_filename(self.FILENAME_SUFFIX_PRETTY_PATCH)
fs.write_binary_file(pretty_patch_filename, pretty_patch)
def write_audio_files(self, actual_audio, expected_audio):
self.write_output_files('.wav', actual_audio, expected_audio)
def write_image_files(self, actual_image, expected_image):
self.write_output_files('.png', actual_image, expected_image)
def write_image_hashes(self, actual_image_hash, expected_image_hash):
self.write_output_files('.checksum', actual_image_hash, expected_image_hash)
def create_image_diff_and_write_result(self, actual_image, expected_image):
"""Writes the visual diff of the expected/actual PNGs.
Returns True if the images are different.
"""
# FIXME: This function is actually doing the diff as well as writing a result.
# It might be better to extract 'diff' code and make it a separate function.
# To do so, we have to change port.diff_image() as well.
diff_filename = self.output_filename(self.FILENAME_SUFFIX_IMAGE_DIFF)
return self._port.diff_image(actual_image, expected_image, diff_filename)
def copy_file(self, src_filepath, dst_extension):
fs = self._port._filesystem
assert fs.exists(src_filepath), 'src_filepath: %s' % src_filepath
dst_filename = self.output_filename(dst_extension)
self._make_output_directory()
fs.copyfile(src_filepath, dst_filename)