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