Home | History | Annotate | Download | only in common
      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):
    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 
    299     Returns:
    300       a list containing urls to all objects that started with
    301          the prefix.
    302     """
    303     return self.cloud_bucket.GetAllPaths(prefix)
    304 
    305