blob: 68d967764d5ab0f1983ddf370d3d85968c74c077 [file] [log] [blame]
José Fonsecaa65795f2012-02-18 18:15:18 +00001#!/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
José Fonseca8f9116e2012-11-16 22:19:10 +000028import difflib
29import itertools
José Fonsecaa65795f2012-02-18 18:15:18 +000030import optparse
José Fonseca3cf1e9a2012-03-16 15:46:26 +000031import os.path
José Fonseca8f9116e2012-11-16 22:19:10 +000032import platform
José Fonsecaa65795f2012-02-18 18:15:18 +000033import shutil
34import subprocess
35import sys
36import tempfile
37
38
José Fonseca8f9116e2012-11-16 22:19:10 +000039##########################################################################/
40#
41# Abstract interface
42#
José Fonseca3cf1e9a2012-03-16 15:46:26 +000043
José Fonseca8f9116e2012-11-16 22:19:10 +000044
45class Differ:
46
47 def __init__(self, apitrace):
48 self.apitrace = apitrace
49 self.isatty = sys.stdout.isatty()
50
51 def setRefTrace(self, ref_trace, ref_calls):
52 raise NotImplementedError
53
54 def setSrcTrace(self, src_trace, src_calls):
55 raise NotImplementedError
56
57 def diff(self):
58 raise NotImplementedError
59
60
61##########################################################################/
62#
63# External diff tool
64#
65
66
67class AsciiDumper:
68
69 def __init__(self, apitrace, trace, calls):
José Fonseca3cf1e9a2012-03-16 15:46:26 +000070 self.output = tempfile.NamedTemporaryFile()
71
72 dump_args = [
José Fonseca8f9116e2012-11-16 22:19:10 +000073 apitrace,
José Fonsecaa65795f2012-02-18 18:15:18 +000074 'dump',
75 '--color=never',
José Fonseca3cf1e9a2012-03-16 15:46:26 +000076 '--call-nos=no',
José Fonsecaa65795f2012-02-18 18:15:18 +000077 '--arg-names=no',
José Fonseca3cf1e9a2012-03-16 15:46:26 +000078 '--calls=' + calls,
José Fonsecaa65795f2012-02-18 18:15:18 +000079 trace
José Fonseca3cf1e9a2012-03-16 15:46:26 +000080 ]
José Fonsecaa65795f2012-02-18 18:15:18 +000081
José Fonseca3cf1e9a2012-03-16 15:46:26 +000082 self.dump = subprocess.Popen(
83 args = dump_args,
84 stdout = self.output,
85 universal_newlines = True,
86 )
José Fonsecadf1a1812012-03-14 11:06:44 +000087
José Fonsecaa65795f2012-02-18 18:15:18 +000088
José Fonseca8f9116e2012-11-16 22:19:10 +000089class ExternalDiffer(Differ):
José Fonsecac6998962012-03-16 09:56:25 +000090
José Fonseca8f9116e2012-11-16 22:19:10 +000091 if platform.system() == 'Windows':
92 start_delete = ''
93 end_delete = ''
94 start_insert = ''
95 end_insert = ''
96 else:
97 start_delete = '\33[9m\33[31m'
98 end_delete = '\33[0m'
99 start_insert = '\33[32m'
100 end_insert = '\33[0m'
José Fonsecac6998962012-03-16 09:56:25 +0000101
José Fonseca8f9116e2012-11-16 22:19:10 +0000102 def __init__(self, apitrace, tool, width=None):
103 Differ.__init__(self, apitrace)
104 self.diff_args = [tool]
105 if tool == 'diff':
106 self.diff_args += [
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000107 '--speed-large-files',
Carl Worthf45f1e22012-08-13 13:24:03 -0700108 ]
José Fonseca8f9116e2012-11-16 22:19:10 +0000109 if self.isatty:
110 self.diff_args += [
111 '--old-line-format=' + self.start_delete + '%l' + self.end_delete + '\n',
112 '--new-line-format=' + self.start_insert + '%l' + self.end_insert + '\n',
113 ]
114 elif tool == 'sdiff':
115 if width is None:
116 import curses
117 curses.setupterm()
118 width = curses.tigetnum('cols')
119 self.diff_args += [
120 '--width=%u' % width,
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000121 '--speed-large-files',
122 ]
José Fonseca8f9116e2012-11-16 22:19:10 +0000123 elif tool == 'wdiff':
124 self.diff_args += [
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000125 #'--terminal',
126 '--avoid-wraps',
Carl Worthf45f1e22012-08-13 13:24:03 -0700127 ]
José Fonseca8f9116e2012-11-16 22:19:10 +0000128 if self.isatty:
129 self.diff_args += [
130 '--start-delete=' + self.start_delete,
131 '--end-delete=' + self.end_delete,
132 '--start-insert=' + self.start_insert,
133 '--end-insert=' + self.end_insert,
134 ]
135 else:
136 assert False
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000137
José Fonseca8f9116e2012-11-16 22:19:10 +0000138 def setRefTrace(self, ref_trace, ref_calls):
139 self.ref_dumper = AsciiDumper(self.apitrace, ref_trace, ref_calls)
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000140
José Fonseca8f9116e2012-11-16 22:19:10 +0000141 def setSrcTrace(self, src_trace, src_calls):
142 self.src_dumper = AsciiDumper(self.apitrace, src_trace, src_calls)
143
144 def diff(self):
145 diff_args = self.diff_args + [
146 self.ref_dumper.output.name,
147 self.src_dumper.output.name,
148 ]
149
150 self.ref_dumper.dump.wait()
151 self.src_dumper.dump.wait()
152
153 less = None
154 if self.isatty:
155 less = subprocess.Popen(
156 args = ['less', '-FRXn'],
157 stdin = subprocess.PIPE
158 )
159
160 diff_stdout = less.stdin
161 else:
162 diff_stdout = None
163
164 diff = subprocess.Popen(
165 args = diff_args,
166 stdout = diff_stdout,
167 universal_newlines = True,
José Fonsecaa65795f2012-02-18 18:15:18 +0000168 )
169
José Fonseca8f9116e2012-11-16 22:19:10 +0000170 diff.wait()
José Fonsecaa65795f2012-02-18 18:15:18 +0000171
José Fonseca8f9116e2012-11-16 22:19:10 +0000172 if less is not None:
173 less.stdin.close()
174 less.wait()
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000175
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000176
José Fonseca8f9116e2012-11-16 22:19:10 +0000177##########################################################################/
178#
179# Python diff
180#
181
182from unpickle import Unpickler, Dumper, Rebuilder
183from highlight import ColorHighlighter, LessHighlighter
184
185
186ignoredFunctionNames = set([
187 'glGetString',
188 'glXGetClientString',
189 'glXGetCurrentDisplay',
190 'glXGetCurrentContext',
191 'glXGetProcAddress',
192 'glXGetProcAddressARB',
193 'wglGetProcAddress',
194])
195
196
197class Blob:
198 '''Data-less proxy for bytearrays, to save memory.'''
199
200 def __init__(self, size, hash):
201 self.size = size
202 self.hash = hash
203
204 def __repr__(self):
205 return 'blob(%u)' % self.size
206
207 def __eq__(self, other):
208 return isinstance(other, Blob) and self.size == other.size and self.hash == other.hash
209
210 def __hash__(self):
211 return self.hash
212
213
214class BlobReplacer(Rebuilder):
215 '''Replace blobs with proxys.'''
216
217 def visitByteArray(self, obj):
218 return Blob(len(obj), hash(str(obj)))
219
220 def visitCall(self, call):
221 call.args = map(self.visit, call.args)
222 call.ret = self.visit(call.ret)
223
224
225class Loader(Unpickler):
226
227 def __init__(self, stream):
228 Unpickler.__init__(self, stream)
229 self.calls = []
230 self.rebuilder = BlobReplacer()
231
232 def handleCall(self, call):
233 if call.functionName not in ignoredFunctionNames:
234 self.rebuilder.visitCall(call)
235 self.calls.append(call)
236
237
238class PythonDiffer(Differ):
239
240 def __init__(self, apitrace, callNos = False):
241 Differ.__init__(self, apitrace)
242 self.a = None
243 self.b = None
244 if self.isatty:
245 self.highlighter = LessHighlighter()
246 else:
247 self.highlighter = ColorHighlighter()
248 self.delete_color = self.highlighter.red
249 self.insert_color = self.highlighter.green
250 self.callNos = callNos
251 self.aSpace = 0
252 self.bSpace = 0
253 self.dumper = Dumper()
254
255 def setRefTrace(self, ref_trace, ref_calls):
256 self.a = self.readTrace(ref_trace, ref_calls)
257
258 def setSrcTrace(self, src_trace, src_calls):
259 self.b = self.readTrace(src_trace, src_calls)
260
261 def readTrace(self, trace, calls):
262 p = subprocess.Popen(
263 args = [
264 self.apitrace,
265 'pickle',
266 '--symbolic',
267 '--calls=' + calls,
268 trace
269 ],
270 stdout = subprocess.PIPE,
271 )
272
273 parser = Loader(p.stdout)
274 parser.parse()
275 return parser.calls
276
277 def diff(self):
278 try:
279 self._diff()
280 except IOError:
281 pass
282
283 def _diff(self):
284 matcher = difflib.SequenceMatcher(self.isjunk, self.a, self.b)
285 for tag, alo, ahi, blo, bhi in matcher.get_opcodes():
286 if tag == 'replace':
287 self.replace(alo, ahi, blo, bhi)
288 elif tag == 'delete':
289 self.delete(alo, ahi, blo, bhi)
290 elif tag == 'insert':
291 self.insert(alo, ahi, blo, bhi)
292 elif tag == 'equal':
293 self.equal(alo, ahi, blo, bhi)
294 else:
295 raise ValueError, 'unknown tag %s' % (tag,)
296
297 def isjunk(self, call):
298 return call.functionName == 'glGetError' and call.ret in ('GL_NO_ERROR', 0)
299
300 def replace(self, alo, ahi, blo, bhi):
301 assert alo < ahi and blo < bhi
302
303 a_names = [call.functionName for call in self.a[alo:ahi]]
304 b_names = [call.functionName for call in self.b[blo:bhi]]
305
306 matcher = difflib.SequenceMatcher(None, a_names, b_names)
307 for tag, _alo, _ahi, _blo, _bhi in matcher.get_opcodes():
308 _alo += alo
309 _ahi += alo
310 _blo += blo
311 _bhi += blo
312 if tag == 'replace':
313 self.replace_dissimilar(_alo, _ahi, _blo, _bhi)
314 elif tag == 'delete':
315 self.delete(_alo, _ahi, _blo, _bhi)
316 elif tag == 'insert':
317 self.insert(_alo, _ahi, _blo, _bhi)
318 elif tag == 'equal':
319 self.replace_similar(_alo, _ahi, _blo, _bhi)
320 else:
321 raise ValueError, 'unknown tag %s' % (tag,)
322
323 def replace_similar(self, alo, ahi, blo, bhi):
324 assert alo < ahi and blo < bhi
325 assert ahi - alo == bhi - blo
326 for i in xrange(0, bhi - blo):
327 self.highlighter.write('| ')
328 a_call = self.a[alo + i]
329 b_call = self.b[blo + i]
330 assert a_call.functionName == b_call.functionName
331 self.dumpCallNos(a_call.no, b_call.no)
332 self.highlighter.bold(True)
333 self.highlighter.write(b_call.functionName)
334 self.highlighter.bold(False)
335 self.highlighter.write('(')
336 sep = ''
337 numArgs = max(len(a_call.args), len(b_call.args))
338 for j in xrange(numArgs):
339 self.highlighter.write(sep)
340 try:
341 a_arg = a_call.args[j]
342 except IndexError:
343 pass
344 try:
345 b_arg = b_call.args[j]
346 except IndexError:
347 pass
348 self.replace_value(a_arg, b_arg)
349 sep = ', '
350 self.highlighter.write(')')
351 if a_call.ret is not None or b_call.ret is not None:
352 self.highlighter.write(' = ')
353 self.replace_value(a_call.ret, b_call.ret)
354 self.highlighter.write('\n')
355
356 def replace_dissimilar(self, alo, ahi, blo, bhi):
357 assert alo < ahi and blo < bhi
358 if bhi - blo < ahi - alo:
359 self.insert(alo, alo, blo, bhi)
360 self.delete(alo, ahi, bhi, bhi)
361 else:
362 self.delete(alo, ahi, blo, blo)
363 self.insert(ahi, ahi, blo, bhi)
364
365 def replace_value(self, a, b):
366 if b == a:
367 self.highlighter.write(self.dumper.visit(b))
368 else:
369 self.highlighter.strike()
370 self.highlighter.color(self.delete_color)
371 self.highlighter.write(self.dumper.visit(a))
372 self.highlighter.normal()
373 self.highlighter.write(" ")
374 self.highlighter.color(self.insert_color)
375 self.highlighter.write(self.dumper.visit(b))
376 self.highlighter.normal()
377
378 escape = "\33["
379
380 def delete(self, alo, ahi, blo, bhi):
381 assert alo < ahi
382 assert blo == bhi
383 for i in xrange(alo, ahi):
384 call = self.a[i]
385 self.highlighter.write('- ')
386 self.dumpCallNos(call.no, None)
387 self.highlighter.strike()
388 self.highlighter.color(self.delete_color)
389 self.dumpCall(call)
390
391 def insert(self, alo, ahi, blo, bhi):
392 assert alo == ahi
393 assert blo < bhi
394 for i in xrange(blo, bhi):
395 call = self.b[i]
396 self.highlighter.write('+ ')
397 self.dumpCallNos(None, call.no)
398 self.highlighter.color(self.insert_color)
399 self.dumpCall(call)
400
401 def equal(self, alo, ahi, blo, bhi):
402 assert alo < ahi and blo < bhi
403 assert ahi - alo == bhi - blo
404 for i in xrange(0, bhi - blo):
405 self.highlighter.write(' ')
406 a_call = self.a[alo + i]
407 b_call = self.b[blo + i]
408 assert a_call.functionName == b_call.functionName
409 assert len(a_call.args) == len(b_call.args)
410 self.dumpCallNos(a_call.no, b_call.no)
411 self.dumpCall(b_call)
412
413 def dumpCallNos(self, aNo, bNo):
414 if not self.callNos:
415 return
416
417 if aNo is None:
418 self.highlighter.write(' '*self.aSpace)
419 else:
420 aNoStr = str(aNo)
421 self.highlighter.strike()
422 self.highlighter.color(self.delete_color)
423 self.highlighter.write(aNoStr)
424 self.highlighter.normal()
425 self.aSpace = len(aNoStr)
426 self.highlighter.write(' ')
427 if bNo is None:
428 self.highlighter.write(' '*self.bSpace)
429 else:
430 bNoStr = str(bNo)
431 self.highlighter.color(self.insert_color)
432 self.highlighter.write(bNoStr)
433 self.highlighter.normal()
434 self.bSpace = len(bNoStr)
435 self.highlighter.write(' ')
436
437 def dumpCall(self, call):
438 self.highlighter.bold(True)
439 self.highlighter.write(call.functionName)
440 self.highlighter.bold(False)
441 self.highlighter.write('(' + ', '.join(itertools.imap(self.dumper.visit, call.args)) + ')')
442 if call.ret is not None:
443 self.highlighter.write(' = ' + self.dumper.visit(call.ret))
444 self.highlighter.normal()
445 self.highlighter.write('\n')
446
447
448
449##########################################################################/
450#
451# Main program
452#
José Fonsecaa65795f2012-02-18 18:15:18 +0000453
454
José Fonsecac6998962012-03-16 09:56:25 +0000455def which(executable):
456 '''Search for the executable on the PATH.'''
457
458 if platform.system() == 'Windows':
459 exts = ['.exe']
460 else:
461 exts = ['']
462 dirs = os.environ['PATH'].split(os.path.pathsep)
463 for dir in dirs:
464 path = os.path.join(dir, executable)
465 for ext in exts:
466 if os.path.exists(path + ext):
467 return True
468 return False
469
470
José Fonsecaa65795f2012-02-18 18:15:18 +0000471def main():
472 '''Main program.
473 '''
474
José Fonsecaa65795f2012-02-18 18:15:18 +0000475 # Parse command line options
476 optparser = optparse.OptionParser(
José Fonseca8f9116e2012-11-16 22:19:10 +0000477 usage='\n\t%prog [options] TRACE TRACE',
José Fonsecaa65795f2012-02-18 18:15:18 +0000478 version='%%prog')
479 optparser.add_option(
480 '-a', '--apitrace', metavar='PROGRAM',
481 type='string', dest='apitrace', default='apitrace',
482 help='apitrace command [default: %default]')
483 optparser.add_option(
José Fonsecac6998962012-03-16 09:56:25 +0000484 '-d', '--diff',
José Fonseca8f9116e2012-11-16 22:19:10 +0000485 type="choice", choices=('diff', 'sdiff', 'wdiff', 'python'),
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000486 dest="diff", default=None,
487 help="diff program: wdiff, sdiff, or diff [default: auto]")
José Fonsecac6998962012-03-16 09:56:25 +0000488 optparser.add_option(
José Fonsecaa65795f2012-02-18 18:15:18 +0000489 '-c', '--calls', metavar='CALLSET',
José Fonseca8f9116e2012-11-16 22:19:10 +0000490 type="string", dest="calls", default='0-10000',
José Fonsecaa65795f2012-02-18 18:15:18 +0000491 help="calls to compare [default: %default]")
492 optparser.add_option(
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000493 '--ref-calls', metavar='CALLSET',
494 type="string", dest="ref_calls", default=None,
495 help="calls to compare from reference trace")
496 optparser.add_option(
497 '--src-calls', metavar='CALLSET',
498 type="string", dest="src_calls", default=None,
499 help="calls to compare from source trace")
500 optparser.add_option(
José Fonseca8f9116e2012-11-16 22:19:10 +0000501 '--call-nos',
502 action="store_true",
503 dest="call_nos", default=False,
504 help="dump call numbers")
505 optparser.add_option(
José Fonsecaa65795f2012-02-18 18:15:18 +0000506 '-w', '--width', metavar='NUM',
José Fonseca8f9116e2012-11-16 22:19:10 +0000507 type="int", dest="width",
508 help="columns [default: auto]")
José Fonsecaa65795f2012-02-18 18:15:18 +0000509
José Fonsecaa65795f2012-02-18 18:15:18 +0000510 (options, args) = optparser.parse_args(sys.argv[1:])
511 if len(args) != 2:
512 optparser.error("incorrect number of arguments")
513
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000514 if options.diff is None:
José Fonseca8f9116e2012-11-16 22:19:10 +0000515 if platform.system() == 'Windows':
516 options.diff = 'python'
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000517 else:
José Fonseca8f9116e2012-11-16 22:19:10 +0000518 if which('wdiff'):
519 options.diff = 'wdiff'
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000520 else:
José Fonseca8f9116e2012-11-16 22:19:10 +0000521 sys.stderr.write('warning: wdiff not found\n')
522 if which('sdiff'):
523 options.diff = 'sdiff'
524 else:
525 sys.stderr.write('warning: sdiff not found\n')
526 options.diff = 'diff'
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000527
528 if options.ref_calls is None:
529 options.ref_calls = options.calls
530 if options.src_calls is None:
531 options.src_calls = options.calls
532
José Fonseca8f9116e2012-11-16 22:19:10 +0000533 ref_trace, src_trace = args
534
535 if options.diff == 'python':
536 differ = PythonDiffer(options.apitrace, options.call_nos)
537 else:
538 differ = ExternalDiffer(options.apitrace, options.diff, options.width)
539 differ.setRefTrace(ref_trace, options.ref_calls)
540 differ.setSrcTrace(src_trace, options.src_calls)
541 differ.diff()
José Fonsecaa65795f2012-02-18 18:15:18 +0000542
543
544if __name__ == '__main__':
545 main()