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