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