blob: ed4731ad3a1afca7e2c6601542c38863e634596a [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.
zachr@google.com6f8e2c52013-08-07 15:43:04 +000051 If the filename looks like a URL, download its contents.
52 If the filename is None, return None."""
53 if filepath is None:
54 return None
55 elif filepath.startswith('http:') or filepath.startswith('https:'):
epoger@google.com53953b42013-07-02 20:22:27 +000056 return urllib2.urlopen(filepath).read()
57 else:
58 return open(filepath, 'r').read()
59
zachr@google.com6f8e2c52013-08-07 15:43:04 +000060 def _GetExpectedResults(self, contents):
61 """Returns the dictionary of expected results from a JSON string,
epoger@google.com53953b42013-07-02 20:22:27 +000062 in this form:
63
64 {
65 'test1' : 14760033689012826769,
66 'test2' : 9151974350149210736,
67 ...
68 }
69
70 We make these simplifying assumptions:
71 1. Each test has either 0 or 1 allowed results.
72 2. All expectations are of type JSONKEY_HASHTYPE_BITMAP_64BITMD5.
73
74 Any tests which violate those assumptions will cause an exception to
75 be raised.
76
77 Any tests for which we have no expectations will be left out of the
78 returned dictionary.
79 """
80 result_dict = {}
epoger@google.com53953b42013-07-02 20:22:27 +000081 json_dict = gm_json.LoadFromString(contents)
82 all_expectations = json_dict[gm_json.JSONKEY_EXPECTEDRESULTS]
epoger@google.comd73531a2013-09-04 16:27:16 +000083
84 # Prevent https://code.google.com/p/skia/issues/detail?id=1588
epoger@google.comd73531a2013-09-04 16:27:16 +000085 if not all_expectations:
86 return result_dict
87
epoger@google.com53953b42013-07-02 20:22:27 +000088 for test_name in all_expectations.keys():
89 test_expectations = all_expectations[test_name]
90 allowed_digests = test_expectations[
91 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS]
92 if allowed_digests:
93 num_allowed_digests = len(allowed_digests)
94 if num_allowed_digests > 1:
95 raise ValueError(
zachr@google.com6f8e2c52013-08-07 15:43:04 +000096 'test %s has %d allowed digests' % (
97 test_name, num_allowed_digests))
epoger@google.com53953b42013-07-02 20:22:27 +000098 digest_pair = allowed_digests[0]
99 if digest_pair[0] != gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5:
100 raise ValueError(
zachr@google.com6f8e2c52013-08-07 15:43:04 +0000101 'test %s has unsupported hashtype %s' % (
102 test_name, digest_pair[0]))
epoger@google.com53953b42013-07-02 20:22:27 +0000103 result_dict[test_name] = digest_pair[1]
104 return result_dict
105
zachr@google.com6f8e2c52013-08-07 15:43:04 +0000106 def _GetActualResults(self, contents):
107 """Returns the dictionary of actual results from a JSON string,
epoger@google.com53953b42013-07-02 20:22:27 +0000108 in this form:
109
110 {
111 'test1' : 14760033689012826769,
112 'test2' : 9151974350149210736,
113 ...
114 }
115
116 We make these simplifying assumptions:
117 1. All results are of type JSONKEY_HASHTYPE_BITMAP_64BITMD5.
118
119 Any tests which violate those assumptions will cause an exception to
120 be raised.
121
122 Any tests for which we have no actual results will be left out of the
123 returned dictionary.
124 """
125 result_dict = {}
epoger@google.com53953b42013-07-02 20:22:27 +0000126 json_dict = gm_json.LoadFromString(contents)
127 all_result_types = json_dict[gm_json.JSONKEY_ACTUALRESULTS]
128 for result_type in all_result_types.keys():
129 results_of_this_type = all_result_types[result_type]
130 if results_of_this_type:
131 for test_name in results_of_this_type.keys():
132 digest_pair = results_of_this_type[test_name]
Eric Boren5e090972018-06-22 10:13:52 -0400133 if (digest_pair[0] !=
134 gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5):
epoger@google.com53953b42013-07-02 20:22:27 +0000135 raise ValueError(
zachr@google.com6f8e2c52013-08-07 15:43:04 +0000136 'test %s has unsupported hashtype %s' % (
137 test_name, digest_pair[0]))
epoger@google.com53953b42013-07-02 20:22:27 +0000138 result_dict[test_name] = digest_pair[1]
139 return result_dict
140
141 def _DictionaryDiff(self, old_dict, new_dict):
Eric Boren5e090972018-06-22 10:13:52 -0400142 """Generate a dictionary showing diffs between old_dict and new_dict.
epoger@google.com53953b42013-07-02 20:22:27 +0000143 Any entries which are identical across them will be left out."""
144 diff_dict = {}
145 all_keys = set(old_dict.keys() + new_dict.keys())
146 for key in all_keys:
147 if old_dict.get(key) != new_dict.get(key):
148 new_entry = {}
149 new_entry['old'] = old_dict.get(key)
150 new_entry['new'] = new_dict.get(key)
151 diff_dict[key] = new_entry
152 return diff_dict
153
154 def GenerateDiffDict(self, oldfile, newfile=None):
155 """Generate a dictionary showing the diffs:
156 old = expectations within oldfile
157 new = expectations within newfile
158
159 If newfile is not specified, then 'new' is the actual results within
160 oldfile.
161 """
Eric Boren5e090972018-06-22 10:13:52 -0400162 return self.GenerateDiffDictFromStrings(
163 self._GetFileContentsAsString(oldfile),
164 self._GetFileContentsAsString(newfile))
zachr@google.com6f8e2c52013-08-07 15:43:04 +0000165
166 def GenerateDiffDictFromStrings(self, oldjson, newjson=None):
167 """Generate a dictionary showing the diffs:
168 old = expectations within oldjson
169 new = expectations within newjson
170
171 If newfile is not specified, then 'new' is the actual results within
172 oldfile.
173 """
174 old_results = self._GetExpectedResults(oldjson)
175 if newjson:
176 new_results = self._GetExpectedResults(newjson)
epoger@google.com53953b42013-07-02 20:22:27 +0000177 else:
zachr@google.com6f8e2c52013-08-07 15:43:04 +0000178 new_results = self._GetActualResults(oldjson)
epoger@google.com53953b42013-07-02 20:22:27 +0000179 return self._DictionaryDiff(old_results, new_results)
180
181
epoger@google.com61822a22013-07-16 18:56:32 +0000182def _Main():
183 parser = argparse.ArgumentParser()
184 parser.add_argument(
185 'old',
186 help='Path to JSON file whose expectations to display on ' +
187 'the "old" side of the diff. This can be a filepath on ' +
188 'local storage, or a URL.')
189 parser.add_argument(
190 'new', nargs='?',
191 help='Path to JSON file whose expectations to display on ' +
192 'the "new" side of the diff; if not specified, uses the ' +
193 'ACTUAL results from the "old" JSON file. This can be a ' +
194 'filepath on local storage, or a URL.')
195 args = parser.parse_args()
196 differ = GMDiffer()
197 diffs = differ.GenerateDiffDict(oldfile=args.old, newfile=args.new)
198 json.dump(diffs, sys.stdout, sort_keys=True, indent=2)
199
200
201if __name__ == '__main__':
202 _Main()