From 9b7d21f8f5a175ac2db4b4d2d1fb14cdbb77adaa Mon Sep 17 00:00:00 2001 From: =?utf8?q?Jos=C3=A9=20Fonseca?= Date: Thu, 20 Jun 2013 19:40:47 +0100 Subject: [PATCH] tools/trace: Tool to compare json state dumps. Copied verbatim from apitrace's scripts/jsondiff.py Reviewed-by: Brian Paul --- src/gallium/tools/trace/diff_state.py | 324 ++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100755 src/gallium/tools/trace/diff_state.py diff --git a/src/gallium/tools/trace/diff_state.py b/src/gallium/tools/trace/diff_state.py new file mode 100755 index 00000000000..470aeda059b --- /dev/null +++ b/src/gallium/tools/trace/diff_state.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python +########################################################################## +# +# Copyright 2011 Jose Fonseca +# All Rights Reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +##########################################################################/ + + +import json +import optparse +import re +import sys + + +def strip_object_hook(obj): + if '__class__' in obj: + return None + for name in obj.keys(): + if name.startswith('__') and name.endswith('__'): + del obj[name] + return obj + + +class Visitor: + + def visit(self, node, *args, **kwargs): + if isinstance(node, dict): + return self.visitObject(node, *args, **kwargs) + elif isinstance(node, list): + return self.visitArray(node, *args, **kwargs) + else: + return self.visitValue(node, *args, **kwargs) + + def visitObject(self, node, *args, **kwargs): + pass + + def visitArray(self, node, *args, **kwargs): + pass + + def visitValue(self, node, *args, **kwargs): + pass + + +class Dumper(Visitor): + + def __init__(self, stream = sys.stdout): + self.stream = stream + self.level = 0 + + def _write(self, s): + self.stream.write(s) + + def _indent(self): + self._write(' '*self.level) + + def _newline(self): + self._write('\n') + + def visitObject(self, node): + self.enter_object() + + members = node.keys() + members.sort() + for i in range(len(members)): + name = members[i] + value = node[name] + self.enter_member(name) + self.visit(value) + self.leave_member(i == len(members) - 1) + self.leave_object() + + def enter_object(self): + self._write('{') + self._newline() + self.level += 1 + + def enter_member(self, name): + self._indent() + self._write('%s: ' % name) + + def leave_member(self, last): + if not last: + self._write(',') + self._newline() + + def leave_object(self): + self.level -= 1 + self._indent() + self._write('}') + if self.level <= 0: + self._newline() + + def visitArray(self, node): + self.enter_array() + for i in range(len(node)): + value = node[i] + self._indent() + self.visit(value) + if i != len(node) - 1: + self._write(',') + self._newline() + self.leave_array() + + def enter_array(self): + self._write('[') + self._newline() + self.level += 1 + + def leave_array(self): + self.level -= 1 + self._indent() + self._write(']') + + def visitValue(self, node): + self._write(json.dumps(node)) + + + +class Comparer(Visitor): + + def __init__(self, ignore_added = False, tolerance = 2.0 ** -24): + self.ignore_added = ignore_added + self.tolerance = tolerance + + def visitObject(self, a, b): + if not isinstance(b, dict): + return False + if len(a) != len(b) and not self.ignore_added: + return False + ak = a.keys() + bk = b.keys() + ak.sort() + bk.sort() + if ak != bk and not self.ignore_added: + return False + for k in ak: + ae = a[k] + try: + be = b[k] + except KeyError: + return False + if not self.visit(ae, be): + return False + return True + + def visitArray(self, a, b): + if not isinstance(b, list): + return False + if len(a) != len(b): + return False + for ae, be in zip(a, b): + if not self.visit(ae, be): + return False + return True + + def visitValue(self, a, b): + if isinstance(a, float) or isinstance(b, float): + if a == 0: + return abs(b) < self.tolerance + else: + return abs((b - a)/a) < self.tolerance + else: + return a == b + + +class Differ(Visitor): + + def __init__(self, stream = sys.stdout, ignore_added = False): + self.dumper = Dumper(stream) + self.comparer = Comparer(ignore_added = ignore_added) + + def visit(self, a, b): + if self.comparer.visit(a, b): + return + Visitor.visit(self, a, b) + + def visitObject(self, a, b): + if not isinstance(b, dict): + self.replace(a, b) + else: + self.dumper.enter_object() + names = set(a.keys()) + if not self.comparer.ignore_added: + names.update(b.keys()) + names = list(names) + names.sort() + + for i in range(len(names)): + name = names[i] + ae = a.get(name, None) + be = b.get(name, None) + if not self.comparer.visit(ae, be): + self.dumper.enter_member(name) + self.visit(ae, be) + self.dumper.leave_member(i == len(names) - 1) + + self.dumper.leave_object() + + def visitArray(self, a, b): + if not isinstance(b, list): + self.replace(a, b) + else: + self.dumper.enter_array() + max_len = max(len(a), len(b)) + for i in range(max_len): + try: + ae = a[i] + except IndexError: + ae = None + try: + be = b[i] + except IndexError: + be = None + self.dumper._indent() + if self.comparer.visit(ae, be): + self.dumper.visit(ae) + else: + self.visit(ae, be) + if i != max_len - 1: + self.dumper._write(',') + self.dumper._newline() + + self.dumper.leave_array() + + def visitValue(self, a, b): + if a != b: + self.replace(a, b) + + def replace(self, a, b): + self.dumper.visit(a) + self.dumper._write(' -> ') + self.dumper.visit(b) + + +# +# Unfortunately JSON standard does not include comments, but this is a quite +# useful feature to have on regressions tests +# + +_token_res = [ + r'//[^\r\n]*', # comment + r'"[^"\\]*(\\.[^"\\]*)*"', # string +] + +_tokens_re = re.compile(r'|'.join(['(' + token_re + ')' for token_re in _token_res]), re.DOTALL) + + +def _strip_comment(mo): + if mo.group(1): + return '' + else: + return mo.group(0) + + +def _strip_comments(data): + '''Strip (non-standard) JSON comments.''' + return _tokens_re.sub(_strip_comment, data) + + +assert _strip_comments('''// a comment +"// a comment in a string +"''') == ''' +"// a comment in a string +"''' + + +def load(stream, strip_images = True, strip_comments = True): + if strip_images: + object_hook = strip_object_hook + else: + object_hook = None + if strip_comments: + data = stream.read() + data = _strip_comments(data) + return json.loads(data, strict=False, object_hook = object_hook) + else: + return json.load(stream, strict=False, object_hook = object_hook) + + +def main(): + optparser = optparse.OptionParser( + usage="\n\t%prog [options] ") + optparser.add_option( + '--keep-images', + action="store_false", dest="strip_images", default=True, + help="compare images") + + (options, args) = optparser.parse_args(sys.argv[1:]) + + if len(args) != 2: + optparser.error('incorrect number of arguments') + + a = load(open(sys.argv[1], 'rt'), options.strip_images) + b = load(open(sys.argv[2], 'rt'), options.strip_images) + + if False: + dumper = Dumper() + dumper.visit(a) + + differ = Differ() + differ.visit(a, b) + + +if __name__ == '__main__': + main() -- 2.30.2