diff options
Diffstat (limited to 'tools/jsondiff.py')
-rwxr-xr-x | tools/jsondiff.py | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/tools/jsondiff.py b/tools/jsondiff.py new file mode 100755 index 0000000000..a1ca257572 --- /dev/null +++ b/tools/jsondiff.py @@ -0,0 +1,175 @@ +#!/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. +''' + +# 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 filepath.startswith('http:') or filepath.startswith('https:'): + return urllib2.urlopen(filepath).read() + else: + return open(filepath, 'r').read() + + def _GetExpectedResults(self, filepath): + """Returns the dictionary of expected results from a JSON file, + 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 = {} + contents = self._GetFileContentsAsString(filepath) + json_dict = gm_json.LoadFromString(contents) + all_expectations = json_dict[gm_json.JSONKEY_EXPECTEDRESULTS] + 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 in file %s has %d allowed digests' % ( + test_name, filepath, num_allowed_digests)) + digest_pair = allowed_digests[0] + if digest_pair[0] != gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5: + raise ValueError( + 'test %s in file %s has unsupported hashtype %s' % ( + test_name, filepath, digest_pair[0])) + result_dict[test_name] = digest_pair[1] + return result_dict + + def _GetActualResults(self, filepath): + """Returns the dictionary of actual results from a JSON file, + 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 = {} + contents = self._GetFileContentsAsString(filepath) + 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 in file %s has unsupported hashtype %s' % ( + test_name, filepath, 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. + """ + old_results = self._GetExpectedResults(oldfile) + if newfile: + new_results = self._GetExpectedResults(newfile) + else: + new_results = self._GetActualResults(oldfile) + return self._DictionaryDiff(old_results, new_results) + + +# 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) |