blob: dd89c6d8dc7d54f6a37ff872d7d0d9bba37e8c57 [file] [log] [blame]
epoger@google.com53953b42013-07-02 20:22:27 +00001#!/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.
epoger@google.com61822a22013-07-16 18:56:32 +000014
15TODO(epoger): Fix indentation in this file (2-space indents, not 4-space).
epoger@google.com53953b42013-07-02 20:22:27 +000016'''
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 filepath.startswith('http:') or filepath.startswith('https:'):
53 return urllib2.urlopen(filepath).read()
54 else:
55 return open(filepath, 'r').read()
56
57 def _GetExpectedResults(self, filepath):
58 """Returns the dictionary of expected results from a JSON file,
59 in this form:
60
61 {
62 'test1' : 14760033689012826769,
63 'test2' : 9151974350149210736,
64 ...
65 }
66
67 We make these simplifying assumptions:
68 1. Each test has either 0 or 1 allowed results.
69 2. All expectations are of type JSONKEY_HASHTYPE_BITMAP_64BITMD5.
70
71 Any tests which violate those assumptions will cause an exception to
72 be raised.
73
74 Any tests for which we have no expectations will be left out of the
75 returned dictionary.
76 """
77 result_dict = {}
78 contents = self._GetFileContentsAsString(filepath)
79 json_dict = gm_json.LoadFromString(contents)
80 all_expectations = json_dict[gm_json.JSONKEY_EXPECTEDRESULTS]
81 for test_name in all_expectations.keys():
82 test_expectations = all_expectations[test_name]
83 allowed_digests = test_expectations[
84 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS]
85 if allowed_digests:
86 num_allowed_digests = len(allowed_digests)
87 if num_allowed_digests > 1:
88 raise ValueError(
89 'test %s in file %s has %d allowed digests' % (
90 test_name, filepath, num_allowed_digests))
91 digest_pair = allowed_digests[0]
92 if digest_pair[0] != gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5:
93 raise ValueError(
94 'test %s in file %s has unsupported hashtype %s' % (
95 test_name, filepath, digest_pair[0]))
96 result_dict[test_name] = digest_pair[1]
97 return result_dict
98
99 def _GetActualResults(self, filepath):
100 """Returns the dictionary of actual results from a JSON file,
101 in this form:
102
103 {
104 'test1' : 14760033689012826769,
105 'test2' : 9151974350149210736,
106 ...
107 }
108
109 We make these simplifying assumptions:
110 1. All results are of type JSONKEY_HASHTYPE_BITMAP_64BITMD5.
111
112 Any tests which violate those assumptions will cause an exception to
113 be raised.
114
115 Any tests for which we have no actual results will be left out of the
116 returned dictionary.
117 """
118 result_dict = {}
119 contents = self._GetFileContentsAsString(filepath)
120 json_dict = gm_json.LoadFromString(contents)
121 all_result_types = json_dict[gm_json.JSONKEY_ACTUALRESULTS]
122 for result_type in all_result_types.keys():
123 results_of_this_type = all_result_types[result_type]
124 if results_of_this_type:
125 for test_name in results_of_this_type.keys():
126 digest_pair = results_of_this_type[test_name]
127 if digest_pair[0] != gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5:
128 raise ValueError(
129 'test %s in file %s has unsupported hashtype %s' % (
130 test_name, filepath, digest_pair[0]))
131 result_dict[test_name] = digest_pair[1]
132 return result_dict
133
134 def _DictionaryDiff(self, old_dict, new_dict):
135 """Generate a dictionary showing the diffs between old_dict and new_dict.
136 Any entries which are identical across them will be left out."""
137 diff_dict = {}
138 all_keys = set(old_dict.keys() + new_dict.keys())
139 for key in all_keys:
140 if old_dict.get(key) != new_dict.get(key):
141 new_entry = {}
142 new_entry['old'] = old_dict.get(key)
143 new_entry['new'] = new_dict.get(key)
144 diff_dict[key] = new_entry
145 return diff_dict
146
147 def GenerateDiffDict(self, oldfile, newfile=None):
148 """Generate a dictionary showing the diffs:
149 old = expectations within oldfile
150 new = expectations within newfile
151
152 If newfile is not specified, then 'new' is the actual results within
153 oldfile.
154 """
155 old_results = self._GetExpectedResults(oldfile)
156 if newfile:
157 new_results = self._GetExpectedResults(newfile)
158 else:
159 new_results = self._GetActualResults(oldfile)
160 return self._DictionaryDiff(old_results, new_results)
161
162
epoger@google.com61822a22013-07-16 18:56:32 +0000163def _Main():
164 parser = argparse.ArgumentParser()
165 parser.add_argument(
166 'old',
167 help='Path to JSON file whose expectations to display on ' +
168 'the "old" side of the diff. This can be a filepath on ' +
169 'local storage, or a URL.')
170 parser.add_argument(
171 'new', nargs='?',
172 help='Path to JSON file whose expectations to display on ' +
173 'the "new" side of the diff; if not specified, uses the ' +
174 'ACTUAL results from the "old" JSON file. This can be a ' +
175 'filepath on local storage, or a URL.')
176 args = parser.parse_args()
177 differ = GMDiffer()
178 diffs = differ.GenerateDiffDict(oldfile=args.old, newfile=args.new)
179 json.dump(diffs, sys.stdout, sort_keys=True, indent=2)
180
181
182if __name__ == '__main__':
183 _Main()