jsondiff.py 7.3 KB

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