1 #!/usr/bin/python 2 3 """ 4 Copyright 2014 Google Inc. 5 6 Use of this source code is governed by a BSD-style license that can be 7 found in the LICENSE file. 8 9 ImagePair class (see class docstring for details) 10 """ 11 12 import posixpath 13 14 15 # Keys used within ImagePair dictionary representations. 16 # NOTE: Keep these in sync with static/constants.js 17 KEY__IMAGEPAIRS__DIFFERENCES = 'differenceData' 18 KEY__IMAGEPAIRS__EXPECTATIONS = 'expectations' 19 KEY__IMAGEPAIRS__EXTRACOLUMNS = 'extraColumns' 20 KEY__IMAGEPAIRS__IMAGE_A_URL = 'imageAUrl' 21 KEY__IMAGEPAIRS__IMAGE_B_URL = 'imageBUrl' 22 KEY__IMAGEPAIRS__IS_DIFFERENT = 'isDifferent' 23 KEY__IMAGEPAIRS__SOURCE_JSON_FILE = 'sourceJsonFile' 24 25 # If self._diff_record is set to this, we haven't asked ImageDiffDB for the 26 # image diff details yet. 27 _DIFF_RECORD_STILL_LOADING = 'still_loading' 28 29 30 class ImagePair(object): 31 """Describes a pair of images, pixel difference info, and optional metadata. 32 """ 33 34 def __init__(self, image_diff_db, 35 imageA_base_url, imageB_base_url, 36 imageA_relative_url, imageB_relative_url, 37 expectations=None, extra_columns=None, source_json_file=None, 38 download_all_images=False): 39 """ 40 Args: 41 image_diff_db: ImageDiffDB instance we use to generate/store image diffs 42 imageA_base_url: string; base URL for image A 43 imageB_base_url: string; base URL for image B 44 imageA_relative_url: string; URL pointing at an image, relative to 45 imageA_base_url; or None, if this image is missing 46 imageB_relative_url: string; URL pointing at an image, relative to 47 imageB_base_url; or None, if this image is missing 48 expectations: optional dictionary containing expectations-specific 49 metadata (ignore-failure, bug numbers, etc.) 50 extra_columns: optional dictionary containing more metadata (test name, 51 builder name, etc.) 52 source_json_file: relative path of the JSON file where each image came 53 from; this will be the same for both imageA and imageB, within their 54 respective directories 55 download_all_images: if True, download any images associated with this 56 image pair, even if we don't need them to generate diffs 57 (imageA == imageB, or one of them is missing) 58 """ 59 self._image_diff_db = image_diff_db 60 self.imageA_base_url = imageA_base_url 61 self.imageB_base_url = imageB_base_url 62 self.imageA_relative_url = imageA_relative_url 63 self.imageB_relative_url = imageB_relative_url 64 self.expectations_dict = expectations 65 self.extra_columns_dict = extra_columns 66 self.source_json_file = source_json_file 67 if not imageA_relative_url or not imageB_relative_url: 68 self._is_different = True 69 self._diff_record = None 70 elif imageA_relative_url == imageB_relative_url: 71 self._is_different = False 72 self._diff_record = None 73 else: 74 # Tell image_diff_db to add an entry for this diff asynchronously. 75 # Later on, we will call image_diff_db.get_diff_record() to find it. 76 self._is_different = True 77 self._diff_record = _DIFF_RECORD_STILL_LOADING 78 79 if self._diff_record != None or download_all_images: 80 image_diff_db.add_image_pair( 81 expected_image_locator=imageA_relative_url, 82 expected_image_url=self.posixpath_join(imageA_base_url, 83 imageA_relative_url), 84 actual_image_locator=imageB_relative_url, 85 actual_image_url=self.posixpath_join(imageB_base_url, 86 imageB_relative_url)) 87 88 def as_dict(self): 89 """Returns a dictionary describing this ImagePair. 90 91 Uses the KEY__IMAGEPAIRS__* constants as keys. 92 """ 93 asdict = { 94 KEY__IMAGEPAIRS__IMAGE_A_URL: self.imageA_relative_url, 95 KEY__IMAGEPAIRS__IMAGE_B_URL: self.imageB_relative_url, 96 } 97 asdict[KEY__IMAGEPAIRS__IS_DIFFERENT] = self._is_different 98 if self.expectations_dict: 99 asdict[KEY__IMAGEPAIRS__EXPECTATIONS] = self.expectations_dict 100 if self.extra_columns_dict: 101 asdict[KEY__IMAGEPAIRS__EXTRACOLUMNS] = self.extra_columns_dict 102 if self.source_json_file: 103 asdict[KEY__IMAGEPAIRS__SOURCE_JSON_FILE] = self.source_json_file 104 if self._diff_record is _DIFF_RECORD_STILL_LOADING: 105 # We have waited as long as we can to ask ImageDiffDB for details of 106 # this image diff. Now we must block until ImageDiffDB can provide 107 # those details. 108 # 109 # TODO(epoger): Is it wasteful for every imagepair to have its own 110 # reference to image_diff_db? If so, we could pass an image_diff_db 111 # reference into this method call instead... 112 self._diff_record = self._image_diff_db.get_diff_record( 113 expected_image_locator=self.imageA_relative_url, 114 actual_image_locator=self.imageB_relative_url) 115 if self._diff_record != None: 116 asdict[KEY__IMAGEPAIRS__DIFFERENCES] = self._diff_record.as_dict() 117 return asdict 118 119 @staticmethod 120 def posixpath_join(*args): 121 """Wrapper around posixpath.join(). 122 123 Returns posixpath.join(*args), or None if any arg is None. 124 """ 125 for arg in args: 126 if arg == None: 127 return None 128 return posixpath.join(*args) 129