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
6# are met:
7#
8# 1.  Redistributions of source code must retain the above copyright
9#     notice, this list of conditions and the following disclaimer.
10# 2.  Redistributions in binary form must reproduce the above copyright
11#     notice, this list of conditions and the following disclaimer in the
12#     documentation and/or other materials provided with the distribution.
13#
14# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
15# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
18# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
21# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
23# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
25import os
26import re
27
28
29class NaiveImageDiffer(object):
30    def same_image(self, img1, img2):
31        return img1 == img2
32
33
34class TestOutput(object):
35    """Represents the output that a single layout test generates when it is run
36    on a particular platform.
37    Note that this is the raw output that is produced when the layout test is
38    run, not the results of the subsequent comparison between that output and
39    the expected output."""
40    def __init__(self, platform, output_type, files):
41        self._output_type = output_type
42        self._files = files
43        file = files[0]  # Pick some file to do test name calculation.
44        self._name = self._extract_test_name(file.name())
45        self._is_actual = '-actual.' in file.name()
46
47        self._platform = platform or self._extract_platform(file.name())
48
49    def _extract_platform(self, filename):
50        """Calculates the platform from the name of the file if it isn't known already"""
51        path = filename.split(os.path.sep)
52        if 'platform' in path:
53            return path[path.index('platform') + 1]
54        return None
55
56    def _extract_test_name(self, filename):
57        path = filename.split(os.path.sep)
58        if 'LayoutTests' in path:
59            path = path[1 + path.index('LayoutTests'):]
60        if 'layout-test-results' in path:
61            path = path[1 + path.index('layout-test-results'):]
62        if 'platform' in path:
63            path = path[2 + path.index('platform'):]
64
65        filename = path[-1]
66        filename = re.sub('-expected\..*$', '', filename)
67        filename = re.sub('-actual\..*$', '', filename)
68        path[-1] = filename
69        return os.path.sep.join(path)
70
71    def save_to(self, path):
72        """Have the files in this TestOutput write themselves to the disk at the specified location."""
73        for file in self._files:
74            file.save_to(path)
75
76    def is_actual(self):
77        """Is this output the actual output of a test? (As opposed to expected output.)"""
78        return self._is_actual
79
80    def name(self):
81        """The name of this test (doesn't include extension)"""
82        return self._name
83
84    def __eq__(self, other):
85        return (other != None and
86                self.name() == other.name() and
87                self.type() == other.type() and
88                self.platform() == other.platform() and
89                self.is_actual() == other.is_actual() and
90                self.same_content(other))
91
92    def __hash__(self):
93        return hash(str(self.name()) + str(self.type()) + str(self.platform()))
94
95    def is_new_baseline_for(self, other):
96        return (self.name() == other.name() and
97                self.type() == other.type() and
98                self.platform() == other.platform() and
99                self.is_actual() and
100                (not other.is_actual()))
101
102    def __str__(self):
103        actual_str = '[A] ' if self.is_actual() else ''
104        return "TestOutput[%s/%s] %s%s" % (self._platform, self._output_type, actual_str, self.name())
105
106    def type(self):
107        return self._output_type
108
109    def platform(self):
110        return self._platform
111
112    def _path_to_platform(self):
113        """Returns the path that tests for this platform are stored in."""
114        if self._platform is None:
115            return ""
116        else:
117            return os.path.join("self._platform", self._platform)
118
119    def _save_expected_result(self, file, path):
120        path = os.path.join(path, self._path_to_platform())
121        extension = os.path.splitext(file.name())[1]
122        filename = self.name() + '-expected' + extension
123        file.save_to(path, filename)
124
125    def save_expected_results(self, path_to_layout_tests):
126        """Save the files of this TestOutput to the appropriate directory
127        inside the LayoutTests directory. Typically this means that these files
128        will be saved in "LayoutTests/platform/<platform>/, or simply
129        LayoutTests if the platform is None."""
130        for file in self._files:
131            self._save_expected_result(file, path_to_layout_tests)
132
133    def delete(self):
134        """Deletes the files that comprise this TestOutput from disk. This
135        fails if the files are virtual files (eg: the files may reside inside a
136        remote zip file)."""
137        for file in self._files:
138            file.delete()
139
140
141class TextTestOutput(TestOutput):
142    """Represents a text output of a single test on a single platform"""
143    def __init__(self, platform, text_file):
144        self._text_file = text_file
145        TestOutput.__init__(self, platform, 'text', [text_file])
146
147    def same_content(self, other):
148        return self._text_file.contents() == other._text_file.contents()
149
150    def retarget(self, platform):
151        return TextTestOutput(platform, self._text_file)
152
153
154class ImageTestOutput(TestOutput):
155    image_differ = NaiveImageDiffer()
156    """Represents an image output of a single test on a single platform"""
157    def __init__(self, platform, image_file, checksum_file):
158        self._checksum_file = checksum_file
159        self._image_file = image_file
160        files = filter(bool, [self._checksum_file, self._image_file])
161        TestOutput.__init__(self, platform, 'image', files)
162
163    def has_checksum(self):
164        return self._checksum_file is not None
165
166    def same_content(self, other):
167        # FIXME This should not assume that checksums are up to date.
168        if self.has_checksum() and other.has_checksum():
169            return self._checksum_file.contents() == other._checksum_file.contents()
170        else:
171            self_contents = self._image_file.contents()
172            other_contents = other._image_file.contents()
173            return ImageTestOutput.image_differ.same_image(self_contents, other_contents)
174
175    def retarget(self, platform):
176        return ImageTestOutput(platform, self._image_file, self._checksum_file)
177
178    def checksum(self):
179        return self._checksum_file.contents()
180