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