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 9Expectations on local disk that we can modify. 10""" 11 12# System-level imports 13import logging 14import os 15import re 16 17# Must fix up PYTHONPATH before importing from within Skia 18import rs_fixpypath # pylint: disable=W0611 19 20# Imports from within Skia 21from py.utils import git_utils 22import compare_rendered_pictures 23import gm_json 24import imagepair 25import results 26 27FILEPATH_RE = re.compile('.+/' + gm_json.IMAGE_FILENAME_PATTERN) 28 29SKIA_REPO = os.path.abspath(os.path.join( 30 os.path.dirname(__file__), os.pardir, os.pardir, '.git')) 31 32 33class WritableExpectations(git_utils.NewGitCheckout): 34 """Expectations on local disk that we can modify.""" 35 36 def __init__(self, set_descriptions): 37 """Creates a sandbox on local disk containing writable expectations. 38 39 You must use the 'with' statement to create this object in such a way that 40 it cleans up after itself: 41 42 with WritableExpectations(*args) as writable_expectations: 43 # make modifications 44 # use the modified results 45 # the sandbox on local disk is automatically cleaned up here 46 47 Args: 48 set_descriptions: SET_DESCRIPTIONS dict describing the set we want to 49 update expectations within; this tells us the subdirectory within the 50 Skia repo where we keep these expectations, and the commithash at 51 which the user evaluated new baselines. 52 """ 53 file_section = set_descriptions[results.KEY__SET_DESCRIPTIONS__SECTION] 54 assert file_section == gm_json.JSONKEY_EXPECTEDRESULTS 55 56 source_dir = _unicode_to_ascii( 57 set_descriptions[results.KEY__SET_DESCRIPTIONS__DIR]) 58 assert source_dir.startswith(compare_rendered_pictures.REPO_URL_PREFIX) 59 repo_subdir = source_dir[len(compare_rendered_pictures.REPO_URL_PREFIX):] 60 repo_revision = _unicode_to_ascii( 61 set_descriptions[results.KEY__SET_DESCRIPTIONS__REPO_REVISION]) 62 63 logging.info('Creating a writable Skia checkout at revision "%s"...' % 64 repo_revision) 65 super(WritableExpectations, self).__init__( 66 repository=SKIA_REPO, commit=repo_revision, subdir=repo_subdir) 67 68 def modify(self, modifications): 69 """Modify the contents of the checkout, using modifications from the UI. 70 71 Args: 72 modifications: data[KEY__LIVE_EDITS__MODIFICATIONS] coming back from the 73 rebaseline_server UI frontend 74 """ 75 logging.info('Reading in dicts from writable Skia checkout in %s ...' % 76 self.root) 77 dicts = results.BaseComparisons.read_dicts_from_root(self.root) 78 79 # Make sure we have expected-results sections in all our output dicts. 80 for pathname, adict in dicts.iteritems(): 81 if not adict: 82 adict = { 83 # TODO(stephana): These values should be defined as constants 84 # somewhere, to be kept in sync between this file and 85 # compare_rendered_pictures.py. 86 gm_json.JSONKEY_HEADER: { 87 gm_json.JSONKEY_HEADER_TYPE: 'ChecksummedImages', 88 gm_json.JSONKEY_HEADER_REVISION: 1, 89 } 90 } 91 if not adict.get(gm_json.JSONKEY_EXPECTEDRESULTS, None): 92 adict[gm_json.JSONKEY_EXPECTEDRESULTS] = {} 93 dicts[pathname] = adict 94 95 for modification in modifications: 96 expectations = modification[imagepair.KEY__IMAGEPAIRS__EXPECTATIONS] 97 _add_image_info_to_expectations( 98 expectations=expectations, 99 filepath=modification[imagepair.KEY__IMAGEPAIRS__IMAGE_B_URL]) 100 extra_columns = modification[imagepair.KEY__IMAGEPAIRS__EXTRACOLUMNS] 101 dictname = modification[imagepair.KEY__IMAGEPAIRS__SOURCE_JSON_FILE] 102 dict_to_modify = dicts[dictname][gm_json.JSONKEY_EXPECTEDRESULTS] 103 test_name = extra_columns[compare_rendered_pictures.COLUMN__SOURCE_SKP] 104 test_record = dict_to_modify.get(test_name, {}) 105 if (extra_columns[compare_rendered_pictures.COLUMN__TILED_OR_WHOLE] == 106 compare_rendered_pictures.COLUMN__TILED_OR_WHOLE__TILED): 107 test_tiles_list = test_record.get( 108 gm_json.JSONKEY_SOURCE_TILEDIMAGES, []) 109 tilenum = int(extra_columns[compare_rendered_pictures.COLUMN__TILENUM]) 110 _replace_list_item(test_tiles_list, tilenum, expectations) 111 test_record[gm_json.JSONKEY_SOURCE_TILEDIMAGES] = test_tiles_list 112 else: 113 test_record[gm_json.JSONKEY_SOURCE_WHOLEIMAGE] = expectations 114 dict_to_modify[test_name] = test_record 115 116 # Write the modified files back to disk. 117 self._write_dicts_to_root(meta_dict=dicts, root=self.root) 118 119 def get_diffs(self): 120 """Return patchfile describing any modifications to this checkout.""" 121 return self._run_in_git_root(args=[git_utils.GIT, 'diff']) 122 123 @staticmethod 124 def _write_dicts_to_root(meta_dict, root): 125 """Write out multiple dictionaries in JSON format. 126 127 Args: 128 meta_dict: a builder-keyed meta-dictionary containing all the JSON 129 dictionaries we want to write out 130 root: path to root of directory tree within which to write files 131 """ 132 if not os.path.isdir(root): 133 raise IOError('no directory found at path %s' % root) 134 135 for rel_path in meta_dict.keys(): 136 full_path = os.path.join(root, rel_path) 137 gm_json.WriteToFile(meta_dict[rel_path], full_path) 138 139 140def _unicode_to_ascii(unicode_string): 141 """Returns the plain ASCII form of a unicode string. 142 143 TODO(stephana): We created this because we get unicode strings out of the 144 JSON file, while the git filenames and revision tags are plain ASCII. 145 There may be a better way to handle this... maybe set the JSON util to just 146 return ASCII strings? 147 """ 148 return unicode_string.encode('ascii', 'ignore') 149 150 151def _replace_list_item(a_list, index, value): 152 """Replaces value at index "index" within a_list. 153 154 Args: 155 a_list: a list 156 index: index indicating which item in a_list to replace 157 value: value to set a_list[index] to 158 159 If a_list does not contain this index, it will be extended with None entries 160 to that length. 161 """ 162 length = len(a_list) 163 while index >= length: 164 a_list.append(None) 165 length += 1 166 a_list[index] = value 167 168 169def _add_image_info_to_expectations(expectations, filepath): 170 """Add JSONKEY_IMAGE_* info to an existing expectations dictionary. 171 172 TODO(stephana): This assumes that the checksumAlgorithm and checksumValue 173 can be derived from the filepath, which is currently true but may not always 174 be true. 175 176 Args: 177 expectations: the expectations dict to augment 178 filepath: relative path to the image file 179 """ 180 (checksum_algorithm, checksum_value) = FILEPATH_RE.match(filepath).groups() 181 expectations[gm_json.JSONKEY_IMAGE_CHECKSUMALGORITHM] = checksum_algorithm 182 expectations[gm_json.JSONKEY_IMAGE_CHECKSUMVALUE] = checksum_value 183 expectations[gm_json.JSONKEY_IMAGE_FILEPATH] = filepath 184