• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Internal utilities for managing I-Spy test results in Google Cloud Storage.
6
7See the ispy.ispy_api module for the external API.
8"""
9
10import collections
11import itertools
12import json
13import os
14import sys
15
16import image_tools
17
18
19_INVALID_EXPECTATION_CHARS = ['/', '\\', ' ', '"', '\'']
20
21
22def IsValidExpectationName(expectation_name):
23  return not any(c in _INVALID_EXPECTATION_CHARS for c in expectation_name)
24
25
26def GetExpectationPath(expectation, file_name=''):
27  """Get the path to a test file in the given test run and expectation.
28
29  Args:
30    expectation: name of the expectation.
31    file_name: name of the file.
32
33  Returns:
34    the path as a string relative to the bucket.
35  """
36  return 'expectations/%s/%s' % (expectation, file_name)
37
38
39def GetFailurePath(test_run, expectation, file_name=''):
40  """Get the path to a failure file in the given test run and test.
41
42  Args:
43    test_run: name of the test run.
44    expectation: name of the expectation.
45    file_name: name of the file.
46
47  Returns:
48    the path as a string relative to the bucket.
49  """
50  return GetTestRunPath(test_run, '%s/%s' % (expectation, file_name))
51
52
53def GetTestRunPath(test_run, file_name=''):
54  """Get the path to a the given test run.
55
56  Args:
57    test_run: name of the test run.
58    file_name: name of the file.
59
60  Returns:
61    the path as a string relative to the bucket.
62  """
63  return 'failures/%s/%s' % (test_run, file_name)
64
65
66class ISpyUtils(object):
67  """Utility functions for working with an I-Spy google storage bucket."""
68
69  def __init__(self, cloud_bucket):
70    """Initialize with a cloud bucket instance to supply GS functionality.
71
72    Args:
73      cloud_bucket: An object implementing the cloud_bucket.BaseCloudBucket
74        interface.
75    """
76    self.cloud_bucket = cloud_bucket
77
78  def UploadImage(self, full_path, image):
79    """Uploads an image to a location in GS.
80
81    Args:
82      full_path: the path to the file in GS including the file extension.
83      image: a RGB PIL.Image to be uploaded.
84    """
85    self.cloud_bucket.UploadFile(
86        full_path, image_tools.EncodePNG(image), 'image/png')
87
88  def DownloadImage(self, full_path):
89    """Downloads an image from a location in GS.
90
91    Args:
92      full_path: the path to the file in GS including the file extension.
93
94    Returns:
95      The downloaded RGB PIL.Image.
96
97    Raises:
98      cloud_bucket.NotFoundError: if the path to the image is not valid.
99    """
100    return image_tools.DecodePNG(self.cloud_bucket.DownloadFile(full_path))
101
102  def UpdateImage(self, full_path, image):
103    """Updates an existing image in GS, preserving permissions and metadata.
104
105    Args:
106      full_path: the path to the file in GS including the file extension.
107      image: a RGB PIL.Image.
108    """
109    self.cloud_bucket.UpdateFile(full_path, image_tools.EncodePNG(image))
110
111  def GenerateExpectation(self, expectation, images):
112    """Creates and uploads an expectation to GS from a set of images and name.
113
114    This method generates a mask from the uploaded images, then
115      uploads the mask and first of the images to GS as a expectation.
116
117    Args:
118      expectation: name for this expectation, any existing expectation with the
119        name will be replaced.
120      images: a list of RGB encoded PIL.Images
121
122    Raises:
123      ValueError: if the expectation name is invalid.
124    """
125    if not IsValidExpectationName(expectation):
126      raise ValueError("Expectation name contains an illegal character: %s." %
127                       str(_INVALID_EXPECTATION_CHARS))
128
129    mask = image_tools.InflateMask(image_tools.CreateMask(images), 7)
130    self.UploadImage(
131        GetExpectationPath(expectation, 'expected.png'), images[0])
132    self.UploadImage(GetExpectationPath(expectation, 'mask.png'), mask)
133
134  def PerformComparison(self, test_run, expectation, actual):
135    """Runs an image comparison, and uploads discrepancies to GS.
136
137    Args:
138      test_run: the name of the test_run.
139      expectation: the name of the expectation to use for comparison.
140      actual: an RGB-encoded PIL.Image that is the actual result.
141
142    Raises:
143      cloud_bucket.NotFoundError: if the given expectation is not found.
144      ValueError: if the expectation name is invalid.
145    """
146    if not IsValidExpectationName(expectation):
147      raise ValueError("Expectation name contains an illegal character: %s." %
148                       str(_INVALID_EXPECTATION_CHARS))
149
150    expectation_tuple = self.GetExpectation(expectation)
151    if not image_tools.SameImage(
152        actual, expectation_tuple.expected, mask=expectation_tuple.mask):
153      self.UploadImage(
154          GetFailurePath(test_run, expectation, 'actual.png'), actual)
155      diff, diff_pxls = image_tools.VisualizeImageDifferences(
156          expectation_tuple.expected, actual, mask=expectation_tuple.mask)
157      self.UploadImage(GetFailurePath(test_run, expectation, 'diff.png'), diff)
158      self.cloud_bucket.UploadFile(
159          GetFailurePath(test_run, expectation, 'info.txt'),
160          json.dumps({
161            'different_pixels': diff_pxls,
162            'fraction_different':
163                diff_pxls / float(actual.size[0] * actual.size[1])}),
164          'application/json')
165
166  def GetExpectation(self, expectation):
167    """Returns the given expectation from GS.
168
169    Args:
170      expectation: the name of the expectation to get.
171
172    Returns:
173      A named tuple: 'Expectation', containing two images: expected and mask.
174
175    Raises:
176      cloud_bucket.NotFoundError: if the test is not found in GS.
177    """
178    Expectation = collections.namedtuple('Expectation', ['expected', 'mask'])
179    return Expectation(self.DownloadImage(GetExpectationPath(expectation,
180                                                             'expected.png')),
181                       self.DownloadImage(GetExpectationPath(expectation,
182                                                             'mask.png')))
183
184  def ExpectationExists(self, expectation):
185    """Returns whether the given expectation exists in GS.
186
187    Args:
188      expectation: the name of the expectation to check.
189
190    Returns:
191      A boolean indicating whether the test exists.
192    """
193    expected_image_exists = self.cloud_bucket.FileExists(
194        GetExpectationPath(expectation, 'expected.png'))
195    mask_image_exists = self.cloud_bucket.FileExists(
196        GetExpectationPath(expectation, 'mask.png'))
197    return expected_image_exists and mask_image_exists
198
199  def FailureExists(self, test_run, expectation):
200    """Returns whether a failure for the expectation exists for the given run.
201
202    Args:
203      test_run: the name of the test_run.
204      expectation: the name of the expectation that failed.
205
206    Returns:
207      A boolean indicating whether the failure exists.
208    """
209    actual_image_exists = self.cloud_bucket.FileExists(
210        GetFailurePath(test_run, expectation, 'actual.png'))
211    test_exists = self.ExpectationExists(expectation)
212    info_exists = self.cloud_bucket.FileExists(
213        GetFailurePath(test_run, expectation, 'info.txt'))
214    return test_exists and actual_image_exists and info_exists
215
216  def RemoveExpectation(self, expectation):
217    """Removes an expectation and all associated failures with that test.
218
219    Args:
220      expectation: the name of the expectation to remove.
221    """
222    test_paths = self.cloud_bucket.GetAllPaths(
223        GetExpectationPath(expectation))
224    for path in test_paths:
225      self.cloud_bucket.RemoveFile(path)
226
227  def GenerateExpectationPinkOut(self, expectation, images, pint_out, rgb):
228    """Uploads an ispy-test to GS with the pink_out workaround.
229
230    Args:
231      expectation: the name of the expectation to be uploaded.
232      images: a json encoded list of base64 encoded png images.
233      pink_out: an image.
234      RGB: a json list representing the RGB values of a color to mask out.
235
236    Raises:
237      ValueError: if expectation name is invalid.
238    """
239    if not IsValidExpectationName(expectation):
240      raise ValueError("Expectation name contains an illegal character: %s." %
241                       str(_INVALID_EXPECTATION_CHARS))
242
243    # convert the pink_out into a mask
244    black = (0, 0, 0, 255)
245    white = (255, 255, 255, 255)
246    pink_out.putdata(
247        [black if px == (rgb[0], rgb[1], rgb[2], 255) else white
248         for px in pink_out.getdata()])
249    mask = image_tools.CreateMask(images)
250    mask = image_tools.InflateMask(image_tools.CreateMask(images), 7)
251    combined_mask = image_tools.AddMasks([mask, pink_out])
252    self.UploadImage(GetExpectationPath(expectation, 'expected.png'), images[0])
253    self.UploadImage(GetExpectationPath(expectation, 'mask.png'), combined_mask)
254
255  def RemoveFailure(self, test_run, expectation):
256    """Removes a failure from GS.
257
258    Args:
259      test_run: the name of the test_run.
260      expectation: the expectation on which the failure to be removed occured.
261    """
262    failure_paths = self.cloud_bucket.GetAllPaths(
263        GetFailurePath(test_run, expectation))
264    for path in failure_paths:
265      self.cloud_bucket.RemoveFile(path)
266
267  def GetFailure(self, test_run, expectation):
268    """Returns a given test failure's expected, diff, and actual images.
269
270    Args:
271      test_run: the name of the test_run.
272      expectation: the name of the expectation the result corresponds to.
273
274    Returns:
275      A named tuple: Failure containing three images: expected, diff, and
276        actual.
277
278    Raises:
279      cloud_bucket.NotFoundError: if the result is not found in GS.
280    """
281    expected = self.DownloadImage(
282        GetExpectationPath(expectation, 'expected.png'))
283    actual = self.DownloadImage(
284        GetFailurePath(test_run, expectation, 'actual.png'))
285    diff = self.DownloadImage(
286        GetFailurePath(test_run, expectation, 'diff.png'))
287    info = json.loads(self.cloud_bucket.DownloadFile(
288        GetFailurePath(test_run, expectation, 'info.txt')))
289    Failure = collections.namedtuple(
290        'Failure', ['expected', 'diff', 'actual', 'info'])
291    return Failure(expected, diff, actual, info)
292
293  def GetAllPaths(self, prefix, max_keys=None, marker=None, delimiter=None):
294    """Gets urls to all files in GS whose path starts with a given prefix.
295
296    Args:
297      prefix: the prefix to filter files in GS by.
298      max_keys: Integer. Specifies the maximum number of objects returned
299      marker: String. Only objects whose fullpath starts lexicographically
300        after marker (exclusively) will be returned
301      delimiter: String. Turns on directory mode, specifies characters
302        to be used as directory separators
303
304    Returns:
305      a list containing urls to all objects that started with
306         the prefix.
307    """
308    return self.cloud_bucket.GetAllPaths(
309        prefix, max_keys=max_keys, marker=marker, delimiter=delimiter)
310