1#!/usr/bin/python 2 3""" 4Copyright 2014 Google Inc. 5 6Use of this source code is governed by a BSD-style license that can be 7found in the LICENSE file. 8 9Compare results of two render_pictures runs. 10""" 11 12# System-level imports 13import logging 14import os 15import re 16import time 17 18# Imports from within Skia 19import fix_pythonpath # must do this first 20from pyutils import url_utils 21import gm_json 22import imagediffdb 23import imagepair 24import imagepairset 25import results 26 27# URL under which all render_pictures images can be found in Google Storage. 28# TODO(epoger): Move this default value into 29# https://skia.googlesource.com/buildbot/+/master/site_config/global_variables.json 30DEFAULT_IMAGE_BASE_URL = 'http://chromium-skia-gm.commondatastorage.googleapis.com/render_pictures/images' 31 32 33class RenderedPicturesComparisons(results.BaseComparisons): 34 """Loads results from two different render_pictures runs into an ImagePairSet. 35 """ 36 37 def __init__(self, subdirs, actuals_root, 38 generated_images_root=results.DEFAULT_GENERATED_IMAGES_ROOT, 39 image_base_url=DEFAULT_IMAGE_BASE_URL, 40 diff_base_url=None): 41 """ 42 Args: 43 actuals_root: root directory containing all render_pictures-generated 44 JSON files 45 subdirs: (string, string) tuple; pair of subdirectories within 46 actuals_root to compare 47 generated_images_root: directory within which to create all pixel diffs; 48 if this directory does not yet exist, it will be created 49 image_base_url: URL under which all render_pictures result images can 50 be found; this will be used to read images for comparison within 51 this code, and included in the ImagePairSet so its consumers know 52 where to download the images from 53 diff_base_url: base URL within which the client should look for diff 54 images; if not specified, defaults to a "file:///" URL representation 55 of generated_images_root 56 """ 57 time_start = int(time.time()) 58 self._image_diff_db = imagediffdb.ImageDiffDB(generated_images_root) 59 self._image_base_url = image_base_url 60 self._diff_base_url = ( 61 diff_base_url or 62 url_utils.create_filepath_url(generated_images_root)) 63 self._load_result_pairs(actuals_root, subdirs) 64 self._timestamp = int(time.time()) 65 logging.info('Results complete; took %d seconds.' % 66 (self._timestamp - time_start)) 67 68 def _load_result_pairs(self, actuals_root, subdirs): 69 """Loads all JSON files found within two subdirs in actuals_root, 70 compares across those two subdirs, and stores the summary in self._results. 71 72 Args: 73 actuals_root: root directory containing all render_pictures-generated 74 JSON files 75 subdirs: (string, string) tuple; pair of subdirectories within 76 actuals_root to compare 77 """ 78 logging.info( 79 'Reading actual-results JSON files from %s subdirs within %s...' % ( 80 subdirs, actuals_root)) 81 subdirA, subdirB = subdirs 82 subdirA_dicts = self._read_dicts_from_root( 83 os.path.join(actuals_root, subdirA)) 84 subdirB_dicts = self._read_dicts_from_root( 85 os.path.join(actuals_root, subdirB)) 86 logging.info('Comparing subdirs %s and %s...' % (subdirA, subdirB)) 87 88 all_image_pairs = imagepairset.ImagePairSet( 89 descriptions=subdirs, 90 diff_base_url=self._diff_base_url) 91 failing_image_pairs = imagepairset.ImagePairSet( 92 descriptions=subdirs, 93 diff_base_url=self._diff_base_url) 94 95 all_image_pairs.ensure_extra_column_values_in_summary( 96 column_id=results.KEY__EXTRACOLUMNS__RESULT_TYPE, values=[ 97 results.KEY__RESULT_TYPE__FAILED, 98 results.KEY__RESULT_TYPE__NOCOMPARISON, 99 results.KEY__RESULT_TYPE__SUCCEEDED, 100 ]) 101 failing_image_pairs.ensure_extra_column_values_in_summary( 102 column_id=results.KEY__EXTRACOLUMNS__RESULT_TYPE, values=[ 103 results.KEY__RESULT_TYPE__FAILED, 104 results.KEY__RESULT_TYPE__NOCOMPARISON, 105 ]) 106 107 common_dict_paths = sorted(set(subdirA_dicts.keys() + subdirB_dicts.keys())) 108 num_common_dict_paths = len(common_dict_paths) 109 dict_num = 0 110 for dict_path in common_dict_paths: 111 dict_num += 1 112 logging.info('Generating pixel diffs for dict #%d of %d, "%s"...' % 113 (dict_num, num_common_dict_paths, dict_path)) 114 dictA = subdirA_dicts[dict_path] 115 dictB = subdirB_dicts[dict_path] 116 self._validate_dict_version(dictA) 117 self._validate_dict_version(dictB) 118 dictA_results = dictA[gm_json.JSONKEY_ACTUALRESULTS] 119 dictB_results = dictB[gm_json.JSONKEY_ACTUALRESULTS] 120 skp_names = sorted(set(dictA_results.keys() + dictB_results.keys())) 121 for skp_name in skp_names: 122 imagepairs_for_this_skp = [] 123 124 whole_image_A = RenderedPicturesComparisons.get_multilevel( 125 dictA_results, skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE) 126 whole_image_B = RenderedPicturesComparisons.get_multilevel( 127 dictB_results, skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE) 128 imagepairs_for_this_skp.append(self._create_image_pair( 129 test=skp_name, config=gm_json.JSONKEY_SOURCE_WHOLEIMAGE, 130 image_dict_A=whole_image_A, image_dict_B=whole_image_B)) 131 132 tiled_images_A = RenderedPicturesComparisons.get_multilevel( 133 dictA_results, skp_name, gm_json.JSONKEY_SOURCE_TILEDIMAGES) 134 tiled_images_B = RenderedPicturesComparisons.get_multilevel( 135 dictB_results, skp_name, gm_json.JSONKEY_SOURCE_TILEDIMAGES) 136 # TODO(epoger): Report an error if we find tiles for A but not B? 137 if tiled_images_A and tiled_images_B: 138 # TODO(epoger): Report an error if we find a different number of tiles 139 # for A and B? 140 num_tiles = len(tiled_images_A) 141 for tile_num in range(num_tiles): 142 imagepairs_for_this_skp.append(self._create_image_pair( 143 test=skp_name, 144 config='%s-%d' % (gm_json.JSONKEY_SOURCE_TILEDIMAGES, tile_num), 145 image_dict_A=tiled_images_A[tile_num], 146 image_dict_B=tiled_images_B[tile_num])) 147 148 for imagepair in imagepairs_for_this_skp: 149 if imagepair: 150 all_image_pairs.add_image_pair(imagepair) 151 result_type = imagepair.extra_columns_dict\ 152 [results.KEY__EXTRACOLUMNS__RESULT_TYPE] 153 if result_type != results.KEY__RESULT_TYPE__SUCCEEDED: 154 failing_image_pairs.add_image_pair(imagepair) 155 156 self._results = { 157 results.KEY__HEADER__RESULTS_ALL: all_image_pairs.as_dict(), 158 results.KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(), 159 } 160 161 def _validate_dict_version(self, result_dict): 162 """Raises Exception if the dict is not the type/version we know how to read. 163 164 Args: 165 result_dict: dictionary holding output of render_pictures 166 """ 167 expected_header_type = 'ChecksummedImages' 168 expected_header_revision = 1 169 170 header = result_dict[gm_json.JSONKEY_HEADER] 171 header_type = header[gm_json.JSONKEY_HEADER_TYPE] 172 if header_type != expected_header_type: 173 raise Exception('expected header_type "%s", but got "%s"' % ( 174 expected_header_type, header_type)) 175 header_revision = header[gm_json.JSONKEY_HEADER_REVISION] 176 if header_revision != expected_header_revision: 177 raise Exception('expected header_revision %d, but got %d' % ( 178 expected_header_revision, header_revision)) 179 180 def _create_image_pair(self, test, config, image_dict_A, image_dict_B): 181 """Creates an ImagePair object for this pair of images. 182 183 Args: 184 test: string; name of the test 185 config: string; name of the config 186 image_dict_A: dict with JSONKEY_IMAGE_* keys, or None if no image 187 image_dict_B: dict with JSONKEY_IMAGE_* keys, or None if no image 188 189 Returns: 190 An ImagePair object, or None if both image_dict_A and image_dict_B are 191 None. 192 """ 193 if (not image_dict_A) and (not image_dict_B): 194 return None 195 196 def _checksum_and_relative_url(dic): 197 if dic: 198 return ((dic[gm_json.JSONKEY_IMAGE_CHECKSUMALGORITHM], 199 dic[gm_json.JSONKEY_IMAGE_CHECKSUMVALUE]), 200 dic[gm_json.JSONKEY_IMAGE_FILEPATH]) 201 else: 202 return None, None 203 204 imageA_checksum, imageA_relative_url = _checksum_and_relative_url( 205 image_dict_A) 206 imageB_checksum, imageB_relative_url = _checksum_and_relative_url( 207 image_dict_B) 208 209 if not imageA_checksum: 210 result_type = results.KEY__RESULT_TYPE__NOCOMPARISON 211 elif not imageB_checksum: 212 result_type = results.KEY__RESULT_TYPE__NOCOMPARISON 213 elif imageA_checksum == imageB_checksum: 214 result_type = results.KEY__RESULT_TYPE__SUCCEEDED 215 else: 216 result_type = results.KEY__RESULT_TYPE__FAILED 217 218 extra_columns_dict = { 219 results.KEY__EXTRACOLUMNS__CONFIG: config, 220 results.KEY__EXTRACOLUMNS__RESULT_TYPE: result_type, 221 results.KEY__EXTRACOLUMNS__TEST: test, 222 # TODO(epoger): Right now, the client UI crashes if it receives 223 # results that do not include this column. 224 # Until we fix that, keep the client happy. 225 results.KEY__EXTRACOLUMNS__BUILDER: 'TODO', 226 } 227 228 try: 229 return imagepair.ImagePair( 230 image_diff_db=self._image_diff_db, 231 base_url=self._image_base_url, 232 imageA_relative_url=imageA_relative_url, 233 imageB_relative_url=imageB_relative_url, 234 extra_columns=extra_columns_dict) 235 except (KeyError, TypeError): 236 logging.exception( 237 'got exception while creating ImagePair for' 238 ' test="%s", config="%s", urlPair=("%s","%s")' % ( 239 test, config, imageA_relative_url, imageB_relative_url)) 240 return None 241 242 243# TODO(epoger): Add main() so this can be called by vm_run_skia_try.sh 244