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