|  | #!/usr/bin/python | 
|  |  | 
|  | ''' | 
|  | Copyright 2013 Google Inc. | 
|  |  | 
|  | Use of this source code is governed by a BSD-style license that can be | 
|  | found in the LICENSE file. | 
|  | ''' | 
|  |  | 
|  | ''' | 
|  | Gathers diffs between 2 JSON expectations files, or between actual and | 
|  | expected results within a single JSON actual-results file, | 
|  | and generates an old-vs-new diff dictionary. | 
|  |  | 
|  | TODO(epoger): Fix indentation in this file (2-space indents, not 4-space). | 
|  | ''' | 
|  |  | 
|  | # System-level imports | 
|  | import argparse | 
|  | import json | 
|  | import os | 
|  | import sys | 
|  | import urllib2 | 
|  |  | 
|  | # Imports from within Skia | 
|  | # | 
|  | # We need to add the 'gm' directory, so that we can import gm_json.py within | 
|  | # that directory.  That script allows us to parse the actual-results.json file | 
|  | # written out by the GM tool. | 
|  | # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* | 
|  | # so any dirs that are already in the PYTHONPATH will be preferred. | 
|  | # | 
|  | # This assumes that the 'gm' directory has been checked out as a sibling of | 
|  | # the 'tools' directory containing this script, which will be the case if | 
|  | # 'trunk' was checked out as a single unit. | 
|  | GM_DIRECTORY = os.path.realpath( | 
|  | os.path.join(os.path.dirname(os.path.dirname(__file__)), 'gm')) | 
|  | if GM_DIRECTORY not in sys.path: | 
|  | sys.path.append(GM_DIRECTORY) | 
|  | import gm_json | 
|  |  | 
|  |  | 
|  | # Object that generates diffs between two JSON gm result files. | 
|  | class GMDiffer(object): | 
|  |  | 
|  | def __init__(self): | 
|  | pass | 
|  |  | 
|  | def _GetFileContentsAsString(self, filepath): | 
|  | """Returns the full contents of a file, as a single string. | 
|  | If the filename looks like a URL, download its contents. | 
|  | If the filename is None, return None.""" | 
|  | if filepath is None: | 
|  | return None | 
|  | elif filepath.startswith('http:') or filepath.startswith('https:'): | 
|  | return urllib2.urlopen(filepath).read() | 
|  | else: | 
|  | return open(filepath, 'r').read() | 
|  |  | 
|  | def _GetExpectedResults(self, contents): | 
|  | """Returns the dictionary of expected results from a JSON string, | 
|  | in this form: | 
|  |  | 
|  | { | 
|  | 'test1' : 14760033689012826769, | 
|  | 'test2' : 9151974350149210736, | 
|  | ... | 
|  | } | 
|  |  | 
|  | We make these simplifying assumptions: | 
|  | 1. Each test has either 0 or 1 allowed results. | 
|  | 2. All expectations are of type JSONKEY_HASHTYPE_BITMAP_64BITMD5. | 
|  |  | 
|  | Any tests which violate those assumptions will cause an exception to | 
|  | be raised. | 
|  |  | 
|  | Any tests for which we have no expectations will be left out of the | 
|  | returned dictionary. | 
|  | """ | 
|  | result_dict = {} | 
|  | json_dict = gm_json.LoadFromString(contents) | 
|  | all_expectations = json_dict[gm_json.JSONKEY_EXPECTEDRESULTS] | 
|  |  | 
|  | # Prevent https://code.google.com/p/skia/issues/detail?id=1588 | 
|  | if not all_expectations: | 
|  | return result_dict | 
|  |  | 
|  | for test_name in all_expectations.keys(): | 
|  | test_expectations = all_expectations[test_name] | 
|  | allowed_digests = test_expectations[ | 
|  | gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] | 
|  | if allowed_digests: | 
|  | num_allowed_digests = len(allowed_digests) | 
|  | if num_allowed_digests > 1: | 
|  | raise ValueError( | 
|  | 'test %s has %d allowed digests' % ( | 
|  | test_name, num_allowed_digests)) | 
|  | digest_pair = allowed_digests[0] | 
|  | if digest_pair[0] != gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5: | 
|  | raise ValueError( | 
|  | 'test %s has unsupported hashtype %s' % ( | 
|  | test_name, digest_pair[0])) | 
|  | result_dict[test_name] = digest_pair[1] | 
|  | return result_dict | 
|  |  | 
|  | def _GetActualResults(self, contents): | 
|  | """Returns the dictionary of actual results from a JSON string, | 
|  | in this form: | 
|  |  | 
|  | { | 
|  | 'test1' : 14760033689012826769, | 
|  | 'test2' : 9151974350149210736, | 
|  | ... | 
|  | } | 
|  |  | 
|  | We make these simplifying assumptions: | 
|  | 1. All results are of type JSONKEY_HASHTYPE_BITMAP_64BITMD5. | 
|  |  | 
|  | Any tests which violate those assumptions will cause an exception to | 
|  | be raised. | 
|  |  | 
|  | Any tests for which we have no actual results will be left out of the | 
|  | returned dictionary. | 
|  | """ | 
|  | result_dict = {} | 
|  | json_dict = gm_json.LoadFromString(contents) | 
|  | all_result_types = json_dict[gm_json.JSONKEY_ACTUALRESULTS] | 
|  | for result_type in all_result_types.keys(): | 
|  | results_of_this_type = all_result_types[result_type] | 
|  | if results_of_this_type: | 
|  | for test_name in results_of_this_type.keys(): | 
|  | digest_pair = results_of_this_type[test_name] | 
|  | if digest_pair[0] != gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5: | 
|  | raise ValueError( | 
|  | 'test %s has unsupported hashtype %s' % ( | 
|  | test_name, digest_pair[0])) | 
|  | result_dict[test_name] = digest_pair[1] | 
|  | return result_dict | 
|  |  | 
|  | def _DictionaryDiff(self, old_dict, new_dict): | 
|  | """Generate a dictionary showing the diffs between old_dict and new_dict. | 
|  | Any entries which are identical across them will be left out.""" | 
|  | diff_dict = {} | 
|  | all_keys = set(old_dict.keys() + new_dict.keys()) | 
|  | for key in all_keys: | 
|  | if old_dict.get(key) != new_dict.get(key): | 
|  | new_entry = {} | 
|  | new_entry['old'] = old_dict.get(key) | 
|  | new_entry['new'] = new_dict.get(key) | 
|  | diff_dict[key] = new_entry | 
|  | return diff_dict | 
|  |  | 
|  | def GenerateDiffDict(self, oldfile, newfile=None): | 
|  | """Generate a dictionary showing the diffs: | 
|  | old = expectations within oldfile | 
|  | new = expectations within newfile | 
|  |  | 
|  | If newfile is not specified, then 'new' is the actual results within | 
|  | oldfile. | 
|  | """ | 
|  | return self.GenerateDiffDictFromStrings(self._GetFileContentsAsString(oldfile), | 
|  | self._GetFileContentsAsString(newfile)) | 
|  |  | 
|  | def GenerateDiffDictFromStrings(self, oldjson, newjson=None): | 
|  | """Generate a dictionary showing the diffs: | 
|  | old = expectations within oldjson | 
|  | new = expectations within newjson | 
|  |  | 
|  | If newfile is not specified, then 'new' is the actual results within | 
|  | oldfile. | 
|  | """ | 
|  | old_results = self._GetExpectedResults(oldjson) | 
|  | if newjson: | 
|  | new_results = self._GetExpectedResults(newjson) | 
|  | else: | 
|  | new_results = self._GetActualResults(oldjson) | 
|  | return self._DictionaryDiff(old_results, new_results) | 
|  |  | 
|  |  | 
|  | def _Main(): | 
|  | parser = argparse.ArgumentParser() | 
|  | parser.add_argument( | 
|  | 'old', | 
|  | help='Path to JSON file whose expectations to display on ' + | 
|  | 'the "old" side of the diff. This can be a filepath on ' + | 
|  | 'local storage, or a URL.') | 
|  | parser.add_argument( | 
|  | 'new', nargs='?', | 
|  | help='Path to JSON file whose expectations to display on ' + | 
|  | 'the "new" side of the diff; if not specified, uses the ' + | 
|  | 'ACTUAL results from the "old" JSON file. This can be a ' + | 
|  | 'filepath on local storage, or a URL.') | 
|  | args = parser.parse_args() | 
|  | differ = GMDiffer() | 
|  | diffs = differ.GenerateDiffDict(oldfile=args.old, newfile=args.new) | 
|  | json.dump(diffs, sys.stdout, sort_keys=True, indent=2) | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | _Main() |