1bf215546Sopenharmony_ci#!/usr/bin/env python3 2bf215546Sopenharmony_ci########################################################################## 3bf215546Sopenharmony_ci# 4bf215546Sopenharmony_ci# Copyright 2011 Jose Fonseca 5bf215546Sopenharmony_ci# All Rights Reserved. 6bf215546Sopenharmony_ci# 7bf215546Sopenharmony_ci# Permission is hereby granted, free of charge, to any person obtaining a copy 8bf215546Sopenharmony_ci# of this software and associated documentation files (the "Software"), to deal 9bf215546Sopenharmony_ci# in the Software without restriction, including without limitation the rights 10bf215546Sopenharmony_ci# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11bf215546Sopenharmony_ci# copies of the Software, and to permit persons to whom the Software is 12bf215546Sopenharmony_ci# furnished to do so, subject to the following conditions: 13bf215546Sopenharmony_ci# 14bf215546Sopenharmony_ci# The above copyright notice and this permission notice shall be included in 15bf215546Sopenharmony_ci# all copies or substantial portions of the Software. 16bf215546Sopenharmony_ci# 17bf215546Sopenharmony_ci# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18bf215546Sopenharmony_ci# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19bf215546Sopenharmony_ci# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20bf215546Sopenharmony_ci# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21bf215546Sopenharmony_ci# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22bf215546Sopenharmony_ci# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23bf215546Sopenharmony_ci# THE SOFTWARE. 24bf215546Sopenharmony_ci# 25bf215546Sopenharmony_ci##########################################################################/ 26bf215546Sopenharmony_ci 27bf215546Sopenharmony_ci 28bf215546Sopenharmony_ciimport json 29bf215546Sopenharmony_ciimport argparse 30bf215546Sopenharmony_ciimport re 31bf215546Sopenharmony_ciimport difflib 32bf215546Sopenharmony_ciimport sys 33bf215546Sopenharmony_ci 34bf215546Sopenharmony_ci 35bf215546Sopenharmony_cidef strip_object_hook(obj): 36bf215546Sopenharmony_ci if '__class__' in obj: 37bf215546Sopenharmony_ci return None 38bf215546Sopenharmony_ci for name in obj.keys(): 39bf215546Sopenharmony_ci if name.startswith('__') and name.endswith('__'): 40bf215546Sopenharmony_ci del obj[name] 41bf215546Sopenharmony_ci return obj 42bf215546Sopenharmony_ci 43bf215546Sopenharmony_ci 44bf215546Sopenharmony_ciclass Visitor: 45bf215546Sopenharmony_ci 46bf215546Sopenharmony_ci def visit(self, node, *args, **kwargs): 47bf215546Sopenharmony_ci if isinstance(node, dict): 48bf215546Sopenharmony_ci return self.visitObject(node, *args, **kwargs) 49bf215546Sopenharmony_ci elif isinstance(node, list): 50bf215546Sopenharmony_ci return self.visitArray(node, *args, **kwargs) 51bf215546Sopenharmony_ci else: 52bf215546Sopenharmony_ci return self.visitValue(node, *args, **kwargs) 53bf215546Sopenharmony_ci 54bf215546Sopenharmony_ci def visitObject(self, node, *args, **kwargs): 55bf215546Sopenharmony_ci pass 56bf215546Sopenharmony_ci 57bf215546Sopenharmony_ci def visitArray(self, node, *args, **kwargs): 58bf215546Sopenharmony_ci pass 59bf215546Sopenharmony_ci 60bf215546Sopenharmony_ci def visitValue(self, node, *args, **kwargs): 61bf215546Sopenharmony_ci pass 62bf215546Sopenharmony_ci 63bf215546Sopenharmony_ci 64bf215546Sopenharmony_ciclass Dumper(Visitor): 65bf215546Sopenharmony_ci 66bf215546Sopenharmony_ci def __init__(self, stream = sys.stdout): 67bf215546Sopenharmony_ci self.stream = stream 68bf215546Sopenharmony_ci self.level = 0 69bf215546Sopenharmony_ci 70bf215546Sopenharmony_ci def _write(self, s): 71bf215546Sopenharmony_ci self.stream.write(s) 72bf215546Sopenharmony_ci 73bf215546Sopenharmony_ci def _indent(self): 74bf215546Sopenharmony_ci self._write(' '*self.level) 75bf215546Sopenharmony_ci 76bf215546Sopenharmony_ci def _newline(self): 77bf215546Sopenharmony_ci self._write('\n') 78bf215546Sopenharmony_ci 79bf215546Sopenharmony_ci def visitObject(self, node): 80bf215546Sopenharmony_ci self.enter_object() 81bf215546Sopenharmony_ci 82bf215546Sopenharmony_ci members = sorted(node) 83bf215546Sopenharmony_ci for i in range(len(members)): 84bf215546Sopenharmony_ci name = members[i] 85bf215546Sopenharmony_ci value = node[name] 86bf215546Sopenharmony_ci self.enter_member(name) 87bf215546Sopenharmony_ci self.visit(value) 88bf215546Sopenharmony_ci self.leave_member(i == len(members) - 1) 89bf215546Sopenharmony_ci self.leave_object() 90bf215546Sopenharmony_ci 91bf215546Sopenharmony_ci def enter_object(self): 92bf215546Sopenharmony_ci self._write('{') 93bf215546Sopenharmony_ci self._newline() 94bf215546Sopenharmony_ci self.level += 1 95bf215546Sopenharmony_ci 96bf215546Sopenharmony_ci def enter_member(self, name): 97bf215546Sopenharmony_ci self._indent() 98bf215546Sopenharmony_ci self._write('%s: ' % name) 99bf215546Sopenharmony_ci 100bf215546Sopenharmony_ci def leave_member(self, last): 101bf215546Sopenharmony_ci if not last: 102bf215546Sopenharmony_ci self._write(',') 103bf215546Sopenharmony_ci self._newline() 104bf215546Sopenharmony_ci 105bf215546Sopenharmony_ci def leave_object(self): 106bf215546Sopenharmony_ci self.level -= 1 107bf215546Sopenharmony_ci self._indent() 108bf215546Sopenharmony_ci self._write('}') 109bf215546Sopenharmony_ci if self.level <= 0: 110bf215546Sopenharmony_ci self._newline() 111bf215546Sopenharmony_ci 112bf215546Sopenharmony_ci def visitArray(self, node): 113bf215546Sopenharmony_ci self.enter_array() 114bf215546Sopenharmony_ci for i in range(len(node)): 115bf215546Sopenharmony_ci value = node[i] 116bf215546Sopenharmony_ci self._indent() 117bf215546Sopenharmony_ci self.visit(value) 118bf215546Sopenharmony_ci if i != len(node) - 1: 119bf215546Sopenharmony_ci self._write(',') 120bf215546Sopenharmony_ci self._newline() 121bf215546Sopenharmony_ci self.leave_array() 122bf215546Sopenharmony_ci 123bf215546Sopenharmony_ci def enter_array(self): 124bf215546Sopenharmony_ci self._write('[') 125bf215546Sopenharmony_ci self._newline() 126bf215546Sopenharmony_ci self.level += 1 127bf215546Sopenharmony_ci 128bf215546Sopenharmony_ci def leave_array(self): 129bf215546Sopenharmony_ci self.level -= 1 130bf215546Sopenharmony_ci self._indent() 131bf215546Sopenharmony_ci self._write(']') 132bf215546Sopenharmony_ci 133bf215546Sopenharmony_ci def visitValue(self, node): 134bf215546Sopenharmony_ci self._write(json.dumps(node, allow_nan=True)) 135bf215546Sopenharmony_ci 136bf215546Sopenharmony_ci 137bf215546Sopenharmony_ci 138bf215546Sopenharmony_ciclass Comparer(Visitor): 139bf215546Sopenharmony_ci 140bf215546Sopenharmony_ci def __init__(self, ignore_added = False, tolerance = 2.0 ** -24): 141bf215546Sopenharmony_ci self.ignore_added = ignore_added 142bf215546Sopenharmony_ci self.tolerance = tolerance 143bf215546Sopenharmony_ci 144bf215546Sopenharmony_ci def visitObject(self, a, b): 145bf215546Sopenharmony_ci if not isinstance(b, dict): 146bf215546Sopenharmony_ci return False 147bf215546Sopenharmony_ci if len(a) != len(b) and not self.ignore_added: 148bf215546Sopenharmony_ci return False 149bf215546Sopenharmony_ci ak = sorted(a) 150bf215546Sopenharmony_ci bk = sorted(b) 151bf215546Sopenharmony_ci if ak != bk and not self.ignore_added: 152bf215546Sopenharmony_ci return False 153bf215546Sopenharmony_ci for k in ak: 154bf215546Sopenharmony_ci ae = a[k] 155bf215546Sopenharmony_ci try: 156bf215546Sopenharmony_ci be = b[k] 157bf215546Sopenharmony_ci except KeyError: 158bf215546Sopenharmony_ci return False 159bf215546Sopenharmony_ci if not self.visit(ae, be): 160bf215546Sopenharmony_ci return False 161bf215546Sopenharmony_ci return True 162bf215546Sopenharmony_ci 163bf215546Sopenharmony_ci def visitArray(self, a, b): 164bf215546Sopenharmony_ci if not isinstance(b, list): 165bf215546Sopenharmony_ci return False 166bf215546Sopenharmony_ci if len(a) != len(b): 167bf215546Sopenharmony_ci return False 168bf215546Sopenharmony_ci for ae, be in zip(a, b): 169bf215546Sopenharmony_ci if not self.visit(ae, be): 170bf215546Sopenharmony_ci return False 171bf215546Sopenharmony_ci return True 172bf215546Sopenharmony_ci 173bf215546Sopenharmony_ci def visitValue(self, a, b): 174bf215546Sopenharmony_ci if isinstance(a, float) and isinstance(b, float): 175bf215546Sopenharmony_ci if a == 0: 176bf215546Sopenharmony_ci return abs(b) < self.tolerance 177bf215546Sopenharmony_ci else: 178bf215546Sopenharmony_ci return abs((b - a) / a) < self.tolerance 179bf215546Sopenharmony_ci else: 180bf215546Sopenharmony_ci return a == b 181bf215546Sopenharmony_ci 182bf215546Sopenharmony_ci 183bf215546Sopenharmony_ciclass Differ(Visitor): 184bf215546Sopenharmony_ci 185bf215546Sopenharmony_ci def __init__(self, stream = sys.stdout, ignore_added = False): 186bf215546Sopenharmony_ci self.dumper = Dumper(stream) 187bf215546Sopenharmony_ci self.comparer = Comparer(ignore_added = ignore_added) 188bf215546Sopenharmony_ci 189bf215546Sopenharmony_ci def visit(self, a, b): 190bf215546Sopenharmony_ci if self.comparer.visit(a, b): 191bf215546Sopenharmony_ci return 192bf215546Sopenharmony_ci Visitor.visit(self, a, b) 193bf215546Sopenharmony_ci 194bf215546Sopenharmony_ci def visitObject(self, a, b): 195bf215546Sopenharmony_ci if not isinstance(b, dict): 196bf215546Sopenharmony_ci self.replace(a, b) 197bf215546Sopenharmony_ci else: 198bf215546Sopenharmony_ci self.dumper.enter_object() 199bf215546Sopenharmony_ci names = set(a.keys()) 200bf215546Sopenharmony_ci if not self.comparer.ignore_added: 201bf215546Sopenharmony_ci names.update(b.keys()) 202bf215546Sopenharmony_ci names = list(names) 203bf215546Sopenharmony_ci names.sort() 204bf215546Sopenharmony_ci 205bf215546Sopenharmony_ci for i in range(len(names)): 206bf215546Sopenharmony_ci name = names[i] 207bf215546Sopenharmony_ci ae = a.get(name, None) 208bf215546Sopenharmony_ci be = b.get(name, None) 209bf215546Sopenharmony_ci if not self.comparer.visit(ae, be): 210bf215546Sopenharmony_ci self.dumper.enter_member(name) 211bf215546Sopenharmony_ci self.visit(ae, be) 212bf215546Sopenharmony_ci self.dumper.leave_member(i == len(names) - 1) 213bf215546Sopenharmony_ci 214bf215546Sopenharmony_ci self.dumper.leave_object() 215bf215546Sopenharmony_ci 216bf215546Sopenharmony_ci def visitArray(self, a, b): 217bf215546Sopenharmony_ci if not isinstance(b, list): 218bf215546Sopenharmony_ci self.replace(a, b) 219bf215546Sopenharmony_ci else: 220bf215546Sopenharmony_ci self.dumper.enter_array() 221bf215546Sopenharmony_ci max_len = max(len(a), len(b)) 222bf215546Sopenharmony_ci for i in range(max_len): 223bf215546Sopenharmony_ci try: 224bf215546Sopenharmony_ci ae = a[i] 225bf215546Sopenharmony_ci except IndexError: 226bf215546Sopenharmony_ci ae = None 227bf215546Sopenharmony_ci try: 228bf215546Sopenharmony_ci be = b[i] 229bf215546Sopenharmony_ci except IndexError: 230bf215546Sopenharmony_ci be = None 231bf215546Sopenharmony_ci self.dumper._indent() 232bf215546Sopenharmony_ci if self.comparer.visit(ae, be): 233bf215546Sopenharmony_ci self.dumper.visit(ae) 234bf215546Sopenharmony_ci else: 235bf215546Sopenharmony_ci self.visit(ae, be) 236bf215546Sopenharmony_ci if i != max_len - 1: 237bf215546Sopenharmony_ci self.dumper._write(',') 238bf215546Sopenharmony_ci self.dumper._newline() 239bf215546Sopenharmony_ci 240bf215546Sopenharmony_ci self.dumper.leave_array() 241bf215546Sopenharmony_ci 242bf215546Sopenharmony_ci def visitValue(self, a, b): 243bf215546Sopenharmony_ci if a != b: 244bf215546Sopenharmony_ci self.replace(a, b) 245bf215546Sopenharmony_ci 246bf215546Sopenharmony_ci def replace(self, a, b): 247bf215546Sopenharmony_ci if isinstance(a, str) and isinstance(b, str): 248bf215546Sopenharmony_ci if '\n' in a or '\n' in b: 249bf215546Sopenharmony_ci a = a.splitlines() 250bf215546Sopenharmony_ci b = b.splitlines() 251bf215546Sopenharmony_ci differ = difflib.Differ() 252bf215546Sopenharmony_ci result = differ.compare(a, b) 253bf215546Sopenharmony_ci self.dumper.level += 1 254bf215546Sopenharmony_ci for entry in result: 255bf215546Sopenharmony_ci self.dumper._newline() 256bf215546Sopenharmony_ci self.dumper._indent() 257bf215546Sopenharmony_ci tag = entry[:2] 258bf215546Sopenharmony_ci text = entry[2:] 259bf215546Sopenharmony_ci if tag == '? ': 260bf215546Sopenharmony_ci tag = ' ' 261bf215546Sopenharmony_ci prefix = ' ' 262bf215546Sopenharmony_ci text = text.rstrip() 263bf215546Sopenharmony_ci suffix = '' 264bf215546Sopenharmony_ci else: 265bf215546Sopenharmony_ci prefix = '"' 266bf215546Sopenharmony_ci suffix = '\\n"' 267bf215546Sopenharmony_ci line = tag + prefix + text + suffix 268bf215546Sopenharmony_ci self.dumper._write(line) 269bf215546Sopenharmony_ci self.dumper.level -= 1 270bf215546Sopenharmony_ci return 271bf215546Sopenharmony_ci self.dumper.visit(a) 272bf215546Sopenharmony_ci self.dumper._write(' -> ') 273bf215546Sopenharmony_ci self.dumper.visit(b) 274bf215546Sopenharmony_ci 275bf215546Sopenharmony_ci def isMultilineString(self, value): 276bf215546Sopenharmony_ci return isinstance(value, str) and '\n' in value 277bf215546Sopenharmony_ci 278bf215546Sopenharmony_ci def replaceMultilineString(self, a, b): 279bf215546Sopenharmony_ci self.dumper.visit(a) 280bf215546Sopenharmony_ci self.dumper._write(' -> ') 281bf215546Sopenharmony_ci self.dumper.visit(b) 282bf215546Sopenharmony_ci 283bf215546Sopenharmony_ci 284bf215546Sopenharmony_ci# 285bf215546Sopenharmony_ci# Unfortunately JSON standard does not include comments, but this is a quite 286bf215546Sopenharmony_ci# useful feature to have on regressions tests 287bf215546Sopenharmony_ci# 288bf215546Sopenharmony_ci 289bf215546Sopenharmony_ci_token_res = [ 290bf215546Sopenharmony_ci r'//[^\r\n]*', # comment 291bf215546Sopenharmony_ci r'"[^"\\]*(\\.[^"\\]*)*"', # string 292bf215546Sopenharmony_ci] 293bf215546Sopenharmony_ci 294bf215546Sopenharmony_ci_tokens_re = re.compile(r'|'.join(['(' + token_re + ')' for token_re in _token_res]), re.DOTALL) 295bf215546Sopenharmony_ci 296bf215546Sopenharmony_ci 297bf215546Sopenharmony_cidef _strip_comment(mo): 298bf215546Sopenharmony_ci if mo.group(1): 299bf215546Sopenharmony_ci return '' 300bf215546Sopenharmony_ci else: 301bf215546Sopenharmony_ci return mo.group(0) 302bf215546Sopenharmony_ci 303bf215546Sopenharmony_ci 304bf215546Sopenharmony_cidef _strip_comments(data): 305bf215546Sopenharmony_ci '''Strip (non-standard) JSON comments.''' 306bf215546Sopenharmony_ci return _tokens_re.sub(_strip_comment, data) 307bf215546Sopenharmony_ci 308bf215546Sopenharmony_ci 309bf215546Sopenharmony_ciassert _strip_comments('''// a comment 310bf215546Sopenharmony_ci"// a comment in a string 311bf215546Sopenharmony_ci"''') == ''' 312bf215546Sopenharmony_ci"// a comment in a string 313bf215546Sopenharmony_ci"''' 314bf215546Sopenharmony_ci 315bf215546Sopenharmony_ci 316bf215546Sopenharmony_cidef load(stream, strip_images = True, strip_comments = True): 317bf215546Sopenharmony_ci if strip_images: 318bf215546Sopenharmony_ci object_hook = strip_object_hook 319bf215546Sopenharmony_ci else: 320bf215546Sopenharmony_ci object_hook = None 321bf215546Sopenharmony_ci if strip_comments: 322bf215546Sopenharmony_ci data = stream.read() 323bf215546Sopenharmony_ci data = _strip_comments(data) 324bf215546Sopenharmony_ci return json.loads(data, strict=False, object_hook = object_hook) 325bf215546Sopenharmony_ci else: 326bf215546Sopenharmony_ci return json.load(stream, strict=False, object_hook = object_hook) 327bf215546Sopenharmony_ci 328bf215546Sopenharmony_ci 329bf215546Sopenharmony_cidef main(): 330bf215546Sopenharmony_ci optparser = argparse.ArgumentParser( 331bf215546Sopenharmony_ci description="Diff JSON format state dump files") 332bf215546Sopenharmony_ci optparser.add_argument("-k", "--keep-images", 333bf215546Sopenharmony_ci action="store_false", dest="strip_images", default=True, 334bf215546Sopenharmony_ci help="compare images") 335bf215546Sopenharmony_ci 336bf215546Sopenharmony_ci optparser.add_argument("ref_json", action="store", 337bf215546Sopenharmony_ci type=str, help="reference state file") 338bf215546Sopenharmony_ci optparser.add_argument("src_json", action="store", 339bf215546Sopenharmony_ci type=str, help="source state file") 340bf215546Sopenharmony_ci 341bf215546Sopenharmony_ci args = optparser.parse_args() 342bf215546Sopenharmony_ci 343bf215546Sopenharmony_ci a = load(open(args.ref_json, 'rt'), args.strip_images) 344bf215546Sopenharmony_ci b = load(open(args.src_json, 'rt'), args.strip_images) 345bf215546Sopenharmony_ci 346bf215546Sopenharmony_ci if False: 347bf215546Sopenharmony_ci dumper = Dumper() 348bf215546Sopenharmony_ci dumper.visit(a) 349bf215546Sopenharmony_ci 350bf215546Sopenharmony_ci differ = Differ() 351bf215546Sopenharmony_ci differ.visit(a, b) 352bf215546Sopenharmony_ci 353bf215546Sopenharmony_ci 354bf215546Sopenharmony_ciif __name__ == '__main__': 355bf215546Sopenharmony_ci main() 356