1#!/usr/bin/python 2 3''' 4Copyright 2013 Google Inc. 5 6Use of this source code is governed by a BSD-style license that can be 7found in the LICENSE file. 8''' 9 10''' 11Gathers diffs between 2 JSON expectations files, or between actual and 12expected results within a single JSON actual-results file, 13and generates an old-vs-new diff dictionary. 14 15TODO(epoger): Fix indentation in this file (2-space indents, not 4-space). 16''' 17 18# System-level imports 19import argparse 20import json 21import os 22import sys 23import 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. 36GM_DIRECTORY = os.path.realpath( 37 os.path.join(os.path.dirname(os.path.dirname(__file__)), 'gm')) 38if GM_DIRECTORY not in sys.path: 39 sys.path.append(GM_DIRECTORY) 40import gm_json 41 42 43# Object that generates diffs between two JSON gm result files. 44class 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 for test_name in all_expectations.keys(): 84 test_expectations = all_expectations[test_name] 85 allowed_digests = test_expectations[ 86 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] 87 if allowed_digests: 88 num_allowed_digests = len(allowed_digests) 89 if num_allowed_digests > 1: 90 raise ValueError( 91 'test %s has %d allowed digests' % ( 92 test_name, num_allowed_digests)) 93 digest_pair = allowed_digests[0] 94 if digest_pair[0] != gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5: 95 raise ValueError( 96 'test %s has unsupported hashtype %s' % ( 97 test_name, digest_pair[0])) 98 result_dict[test_name] = digest_pair[1] 99 return result_dict 100 101 def _GetActualResults(self, contents): 102 """Returns the dictionary of actual results from a JSON string, 103 in this form: 104 105 { 106 'test1' : 14760033689012826769, 107 'test2' : 9151974350149210736, 108 ... 109 } 110 111 We make these simplifying assumptions: 112 1. All results are of type JSONKEY_HASHTYPE_BITMAP_64BITMD5. 113 114 Any tests which violate those assumptions will cause an exception to 115 be raised. 116 117 Any tests for which we have no actual results will be left out of the 118 returned dictionary. 119 """ 120 result_dict = {} 121 json_dict = gm_json.LoadFromString(contents) 122 all_result_types = json_dict[gm_json.JSONKEY_ACTUALRESULTS] 123 for result_type in all_result_types.keys(): 124 results_of_this_type = all_result_types[result_type] 125 if results_of_this_type: 126 for test_name in results_of_this_type.keys(): 127 digest_pair = results_of_this_type[test_name] 128 if digest_pair[0] != gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5: 129 raise ValueError( 130 'test %s has unsupported hashtype %s' % ( 131 test_name, digest_pair[0])) 132 result_dict[test_name] = digest_pair[1] 133 return result_dict 134 135 def _DictionaryDiff(self, old_dict, new_dict): 136 """Generate a dictionary showing the diffs between old_dict and new_dict. 137 Any entries which are identical across them will be left out.""" 138 diff_dict = {} 139 all_keys = set(old_dict.keys() + new_dict.keys()) 140 for key in all_keys: 141 if old_dict.get(key) != new_dict.get(key): 142 new_entry = {} 143 new_entry['old'] = old_dict.get(key) 144 new_entry['new'] = new_dict.get(key) 145 diff_dict[key] = new_entry 146 return diff_dict 147 148 def GenerateDiffDict(self, oldfile, newfile=None): 149 """Generate a dictionary showing the diffs: 150 old = expectations within oldfile 151 new = expectations within newfile 152 153 If newfile is not specified, then 'new' is the actual results within 154 oldfile. 155 """ 156 return self.GenerateDiffDictFromStrings(self._GetFileContentsAsString(oldfile), 157 self._GetFileContentsAsString(newfile)) 158 159 def GenerateDiffDictFromStrings(self, oldjson, newjson=None): 160 """Generate a dictionary showing the diffs: 161 old = expectations within oldjson 162 new = expectations within newjson 163 164 If newfile is not specified, then 'new' is the actual results within 165 oldfile. 166 """ 167 old_results = self._GetExpectedResults(oldjson) 168 if newjson: 169 new_results = self._GetExpectedResults(newjson) 170 else: 171 new_results = self._GetActualResults(oldjson) 172 return self._DictionaryDiff(old_results, new_results) 173 174 175def _Main(): 176 parser = argparse.ArgumentParser() 177 parser.add_argument( 178 'old', 179 help='Path to JSON file whose expectations to display on ' + 180 'the "old" side of the diff. This can be a filepath on ' + 181 'local storage, or a URL.') 182 parser.add_argument( 183 'new', nargs='?', 184 help='Path to JSON file whose expectations to display on ' + 185 'the "new" side of the diff; if not specified, uses the ' + 186 'ACTUAL results from the "old" JSON file. This can be a ' + 187 'filepath on local storage, or a URL.') 188 args = parser.parse_args() 189 differ = GMDiffer() 190 diffs = differ.GenerateDiffDict(oldfile=args.old, newfile=args.new) 191 json.dump(diffs, sys.stdout, sort_keys=True, indent=2) 192 193 194if __name__ == '__main__': 195 _Main() 196