1 #!/usr/bin/python 2 3 ''' 4 Copyright 2013 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 10 ''' 11 Gathers diffs between 2 JSON expectations files, or between actual and 12 expected results within a single JSON actual-results file, 13 and generates an old-vs-new diff dictionary. 14 15 TODO(epoger): Fix indentation in this file (2-space indents, not 4-space). 16 ''' 17 18 # System-level imports 19 import argparse 20 import json 21 import os 22 import sys 23 import urllib2 24 25 # Imports from within Skia 26 # 27 # We need to add the 'gm' directory, so that we can import gm_json.py within 28 # that directory. That script allows us to parse the actual-results.json file 29 # written out by the GM tool. 30 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* 31 # so any dirs that are already in the PYTHONPATH will be preferred. 32 # 33 # This assumes that the 'gm' directory has been checked out as a sibling of 34 # the 'tools' directory containing this script, which will be the case if 35 # 'trunk' was checked out as a single unit. 36 GM_DIRECTORY = os.path.realpath( 37 os.path.join(os.path.dirname(os.path.dirname(__file__)), 'gm')) 38 if GM_DIRECTORY not in sys.path: 39 sys.path.append(GM_DIRECTORY) 40 import gm_json 41 42 43 # Object that generates diffs between two JSON gm result files. 44 class GMDiffer(object): 45 46 def __init__(self): 47 pass 48 49 def _GetFileContentsAsString(self, filepath): 50 """Returns the full contents of a file, as a single string. 51 If the filename looks like a URL, download its contents. 52 If the filename is None, return None.""" 53 if filepath is None: 54 return None 55 elif filepath.startswith('http:') or filepath.startswith('https:'): 56 return urllib2.urlopen(filepath).read() 57 else: 58 return open(filepath, 'r').read() 59 60 def _GetExpectedResults(self, contents): 61 """Returns the dictionary of expected results from a JSON string, 62 in this form: 63 64 { 65 'test1' : 14760033689012826769, 66 'test2' : 9151974350149210736, 67 ... 68 } 69 70 We make these simplifying assumptions: 71 1. Each test has either 0 or 1 allowed results. 72 2. All expectations are of type JSONKEY_HASHTYPE_BITMAP_64BITMD5. 73 74 Any tests which violate those assumptions will cause an exception to 75 be raised. 76 77 Any tests for which we have no expectations will be left out of the 78 returned dictionary. 79 """ 80 result_dict = {} 81 json_dict = gm_json.LoadFromString(contents) 82 all_expectations = json_dict[gm_json.JSONKEY_EXPECTEDRESULTS] 83 84 # Prevent https://code.google.com/p/skia/issues/detail?id=1588 85 # ('svndiff.py: 'NoneType' object has no attribute 'keys'') 86 if not all_expectations: 87 return result_dict 88 89 for test_name in all_expectations.keys(): 90 test_expectations = all_expectations[test_name] 91 allowed_digests = test_expectations[ 92 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] 93 if allowed_digests: 94 num_allowed_digests = len(allowed_digests) 95 if num_allowed_digests > 1: 96 raise ValueError( 97 'test %s has %d allowed digests' % ( 98 test_name, num_allowed_digests)) 99 digest_pair = allowed_digests[0] 100 if digest_pair[0] != gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5: 101 raise ValueError( 102 'test %s has unsupported hashtype %s' % ( 103 test_name, digest_pair[0])) 104 result_dict[test_name] = digest_pair[1] 105 return result_dict 106 107 def _GetActualResults(self, contents): 108 """Returns the dictionary of actual results from a JSON string, 109 in this form: 110 111 { 112 'test1' : 14760033689012826769, 113 'test2' : 9151974350149210736, 114 ... 115 } 116 117 We make these simplifying assumptions: 118 1. All results are of type JSONKEY_HASHTYPE_BITMAP_64BITMD5. 119 120 Any tests which violate those assumptions will cause an exception to 121 be raised. 122 123 Any tests for which we have no actual results will be left out of the 124 returned dictionary. 125 """ 126 result_dict = {} 127 json_dict = gm_json.LoadFromString(contents) 128 all_result_types = json_dict[gm_json.JSONKEY_ACTUALRESULTS] 129 for result_type in all_result_types.keys(): 130 results_of_this_type = all_result_types[result_type] 131 if results_of_this_type: 132 for test_name in results_of_this_type.keys(): 133 digest_pair = results_of_this_type[test_name] 134 if digest_pair[0] != gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5: 135 raise ValueError( 136 'test %s has unsupported hashtype %s' % ( 137 test_name, digest_pair[0])) 138 result_dict[test_name] = digest_pair[1] 139 return result_dict 140 141 def _DictionaryDiff(self, old_dict, new_dict): 142 """Generate a dictionary showing the diffs between old_dict and new_dict. 143 Any entries which are identical across them will be left out.""" 144 diff_dict = {} 145 all_keys = set(old_dict.keys() + new_dict.keys()) 146 for key in all_keys: 147 if old_dict.get(key) != new_dict.get(key): 148 new_entry = {} 149 new_entry['old'] = old_dict.get(key) 150 new_entry['new'] = new_dict.get(key) 151 diff_dict[key] = new_entry 152 return diff_dict 153 154 def GenerateDiffDict(self, oldfile, newfile=None): 155 """Generate a dictionary showing the diffs: 156 old = expectations within oldfile 157 new = expectations within newfile 158 159 If newfile is not specified, then 'new' is the actual results within 160 oldfile. 161 """ 162 return self.GenerateDiffDictFromStrings(self._GetFileContentsAsString(oldfile), 163 self._GetFileContentsAsString(newfile)) 164 165 def GenerateDiffDictFromStrings(self, oldjson, newjson=None): 166 """Generate a dictionary showing the diffs: 167 old = expectations within oldjson 168 new = expectations within newjson 169 170 If newfile is not specified, then 'new' is the actual results within 171 oldfile. 172 """ 173 old_results = self._GetExpectedResults(oldjson) 174 if newjson: 175 new_results = self._GetExpectedResults(newjson) 176 else: 177 new_results = self._GetActualResults(oldjson) 178 return self._DictionaryDiff(old_results, new_results) 179 180 181 def _Main(): 182 parser = argparse.ArgumentParser() 183 parser.add_argument( 184 'old', 185 help='Path to JSON file whose expectations to display on ' + 186 'the "old" side of the diff. This can be a filepath on ' + 187 'local storage, or a URL.') 188 parser.add_argument( 189 'new', nargs='?', 190 help='Path to JSON file whose expectations to display on ' + 191 'the "new" side of the diff; if not specified, uses the ' + 192 'ACTUAL results from the "old" JSON file. This can be a ' + 193 'filepath on local storage, or a URL.') 194 args = parser.parse_args() 195 differ = GMDiffer() 196 diffs = differ.GenerateDiffDict(oldfile=args.old, newfile=args.new) 197 json.dump(diffs, sys.stdout, sort_keys=True, indent=2) 198 199 200 if __name__ == '__main__': 201 _Main() 202