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.
5 """Internal utilities for managing I-Spy test results in Google Cloud Storage.
7 See the ispy.ispy_api module for the external API.
19 _INVALID_EXPECTATION_CHARS
= ['/', '\\', ' ', '"', '\'']
22 def IsValidExpectationName(expectation_name
):
23 return not any(c
in _INVALID_EXPECTATION_CHARS
for c
in expectation_name
)
26 def GetExpectationPath(expectation
, file_name
=''):
27 """Get the path to a test file in the given test run and expectation.
30 expectation: name of the expectation.
31 file_name: name of the file.
34 the path as a string relative to the bucket.
36 return 'expectations/%s/%s' % (expectation
, file_name
)
39 def GetFailurePath(test_run
, expectation
, file_name
=''):
40 """Get the path to a failure file in the given test run and test.
43 test_run: name of the test run.
44 expectation: name of the expectation.
45 file_name: name of the file.
48 the path as a string relative to the bucket.
50 return GetTestRunPath(test_run
, '%s/%s' % (expectation
, file_name
))
53 def GetTestRunPath(test_run
, file_name
=''):
54 """Get the path to a the given test run.
57 test_run: name of the test run.
58 file_name: name of the file.
61 the path as a string relative to the bucket.
63 return 'failures/%s/%s' % (test_run
, file_name
)
66 class ISpyUtils(object):
67 """Utility functions for working with an I-Spy google storage bucket."""
69 def __init__(self
, cloud_bucket
):
70 """Initialize with a cloud bucket instance to supply GS functionality.
73 cloud_bucket: An object implementing the cloud_bucket.BaseCloudBucket
76 self
.cloud_bucket
= cloud_bucket
78 def UploadImage(self
, full_path
, image
):
79 """Uploads an image to a location in GS.
82 full_path: the path to the file in GS including the file extension.
83 image: a RGB PIL.Image to be uploaded.
85 self
.cloud_bucket
.UploadFile(
86 full_path
, image_tools
.EncodePNG(image
), 'image/png')
88 def DownloadImage(self
, full_path
):
89 """Downloads an image from a location in GS.
92 full_path: the path to the file in GS including the file extension.
95 The downloaded RGB PIL.Image.
98 cloud_bucket.NotFoundError: if the path to the image is not valid.
100 return image_tools
.DecodePNG(self
.cloud_bucket
.DownloadFile(full_path
))
102 def UpdateImage(self
, full_path
, image
):
103 """Updates an existing image in GS, preserving permissions and metadata.
106 full_path: the path to the file in GS including the file extension.
107 image: a RGB PIL.Image.
109 self
.cloud_bucket
.UpdateFile(full_path
, image_tools
.EncodePNG(image
))
111 def GenerateExpectation(self
, expectation
, images
):
112 """Creates and uploads an expectation to GS from a set of images and name.
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.
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
123 ValueError: if the expectation name is invalid.
125 if not IsValidExpectationName(expectation
):
126 raise ValueError("Expectation name contains an illegal character: %s." %
127 str(_INVALID_EXPECTATION_CHARS
))
129 mask
= image_tools
.InflateMask(image_tools
.CreateMask(images
), 7)
131 GetExpectationPath(expectation
, 'expected.png'), images
[0])
132 self
.UploadImage(GetExpectationPath(expectation
, 'mask.png'), mask
)
134 def PerformComparison(self
, test_run
, expectation
, actual
):
135 """Runs an image comparison, and uploads discrepancies to GS.
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.
143 cloud_bucket.NotFoundError: if the given expectation is not found.
144 ValueError: if the expectation name is invalid.
146 if not IsValidExpectationName(expectation
):
147 raise ValueError("Expectation name contains an illegal character: %s." %
148 str(_INVALID_EXPECTATION_CHARS
))
150 expectation_tuple
= self
.GetExpectation(expectation
)
151 if not image_tools
.SameImage(
152 actual
, expectation_tuple
.expected
, mask
=expectation_tuple
.mask
):
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'),
161 'different_pixels': diff_pxls
,
162 'fraction_different':
163 diff_pxls
/ float(actual
.size
[0] * actual
.size
[1])}),
166 def GetExpectation(self
, expectation
):
167 """Returns the given expectation from GS.
170 expectation: the name of the expectation to get.
173 A named tuple: 'Expectation', containing two images: expected and mask.
176 cloud_bucket.NotFoundError: if the test is not found in GS.
178 Expectation
= collections
.namedtuple('Expectation', ['expected', 'mask'])
179 return Expectation(self
.DownloadImage(GetExpectationPath(expectation
,
181 self
.DownloadImage(GetExpectationPath(expectation
,
184 def ExpectationExists(self
, expectation
):
185 """Returns whether the given expectation exists in GS.
188 expectation: the name of the expectation to check.
191 A boolean indicating whether the test exists.
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
199 def FailureExists(self
, test_run
, expectation
):
200 """Returns whether a failure for the expectation exists for the given run.
203 test_run: the name of the test_run.
204 expectation: the name of the expectation that failed.
207 A boolean indicating whether the failure exists.
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
216 def RemoveExpectation(self
, expectation
):
217 """Removes an expectation and all associated failures with that test.
220 expectation: the name of the expectation to remove.
222 test_paths
= self
.cloud_bucket
.GetAllPaths(
223 GetExpectationPath(expectation
))
224 for path
in test_paths
:
225 self
.cloud_bucket
.RemoveFile(path
)
227 def GenerateExpectationPinkOut(self
, expectation
, images
, pint_out
, rgb
):
228 """Uploads an ispy-test to GS with the pink_out workaround.
231 expectation: the name of the expectation to be uploaded.
232 images: a json encoded list of base64 encoded png images.
234 RGB: a json list representing the RGB values of a color to mask out.
237 ValueError: if expectation name is invalid.
239 if not IsValidExpectationName(expectation
):
240 raise ValueError("Expectation name contains an illegal character: %s." %
241 str(_INVALID_EXPECTATION_CHARS
))
243 # convert the pink_out into a mask
244 black
= (0, 0, 0, 255)
245 white
= (255, 255, 255, 255)
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
)
255 def RemoveFailure(self
, test_run
, expectation
):
256 """Removes a failure from GS.
259 test_run: the name of the test_run.
260 expectation: the expectation on which the failure to be removed occured.
262 failure_paths
= self
.cloud_bucket
.GetAllPaths(
263 GetFailurePath(test_run
, expectation
))
264 for path
in failure_paths
:
265 self
.cloud_bucket
.RemoveFile(path
)
267 def GetFailure(self
, test_run
, expectation
):
268 """Returns a given test failure's expected, diff, and actual images.
271 test_run: the name of the test_run.
272 expectation: the name of the expectation the result corresponds to.
275 A named tuple: Failure containing three images: expected, diff, and
279 cloud_bucket.NotFoundError: if the result is not found in GS.
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
)
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.
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
305 a list containing urls to all objects that started with
308 return self
.cloud_bucket
.GetAllPaths(
309 prefix
, max_keys
=max_keys
, marker
=marker
, delimiter
=delimiter
)