Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / chrome / test / ispy / common / ispy_utils.py
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
7 See the ispy.ispy_api module for the external API.
8 """
9
10 import collections
11 import itertools
12 import json
13 import os
14 import sys
15
16 import image_tools
17
18
19 _INVALID_EXPECTATION_CHARS = ['/', '\\', ' ', '"', '\'']
20
21
22 def IsValidExpectationName(expectation_name):
23   return not any(c in _INVALID_EXPECTATION_CHARS for c in expectation_name)
24
25
26 def 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
39 def 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
53 def 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
66 class 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)