blob: 22a26265136d28fc7a832fd1d36a7679ce6a63c5 [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
José Fonseca095fb952014-02-04 14:27:05 +000069 def __init__(self, apitrace, trace, calls, callNos):
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é Fonseca095fb952014-02-04 14:27:05 +000076 '--call-nos=' + ('yes' if callNos else '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é Fonseca095fb952014-02-04 14:27:05 +0000102 def __init__(self, apitrace, tool, width=None, callNos = False):
José Fonseca8f9116e2012-11-16 22:19:10 +0000103 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é Fonseca095fb952014-02-04 14:27:05 +0000137 self.callNos = callNos
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000138
José Fonseca8f9116e2012-11-16 22:19:10 +0000139 def setRefTrace(self, ref_trace, ref_calls):
José Fonseca095fb952014-02-04 14:27:05 +0000140 self.ref_dumper = AsciiDumper(self.apitrace, ref_trace, ref_calls, self.callNos)
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000141
José Fonseca8f9116e2012-11-16 22:19:10 +0000142 def setSrcTrace(self, src_trace, src_calls):
José Fonseca095fb952014-02-04 14:27:05 +0000143 self.src_dumper = AsciiDumper(self.apitrace, src_trace, src_calls, self.callNos)
José Fonseca8f9116e2012-11-16 22:19:10 +0000144
145 def diff(self):
146 diff_args = self.diff_args + [
147 self.ref_dumper.output.name,
148 self.src_dumper.output.name,
149 ]
150
151 self.ref_dumper.dump.wait()
152 self.src_dumper.dump.wait()
153
154 less = None
José Fonsecac6ae8732014-05-02 22:34:05 +0100155 diff_stdout = None
José Fonseca8f9116e2012-11-16 22:19:10 +0000156 if self.isatty:
José Fonsecac6ae8732014-05-02 22:34:05 +0100157 try:
158 less = subprocess.Popen(
159 args = ['less', '-FRXn'],
160 stdin = subprocess.PIPE
161 )
162 except OSError:
163 pass
164 else:
165 diff_stdout = less.stdin
José Fonseca8f9116e2012-11-16 22:19:10 +0000166
167 diff = subprocess.Popen(
168 args = diff_args,
169 stdout = diff_stdout,
170 universal_newlines = True,
José Fonsecaa65795f2012-02-18 18:15:18 +0000171 )
172
José Fonseca8f9116e2012-11-16 22:19:10 +0000173 diff.wait()
José Fonsecaa65795f2012-02-18 18:15:18 +0000174
José Fonseca8f9116e2012-11-16 22:19:10 +0000175 if less is not None:
176 less.stdin.close()
177 less.wait()
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000178
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000179
José Fonseca8f9116e2012-11-16 22:19:10 +0000180##########################################################################/
181#
182# Python diff
183#
184
185from unpickle import Unpickler, Dumper, Rebuilder
José Fonseca975f1242012-12-06 07:11:29 +0000186from highlight import PlainHighlighter, LessHighlighter
José Fonseca8f9116e2012-11-16 22:19:10 +0000187
188
189ignoredFunctionNames = set([
190 'glGetString',
191 'glXGetClientString',
192 'glXGetCurrentDisplay',
193 'glXGetCurrentContext',
194 'glXGetProcAddress',
195 'glXGetProcAddressARB',
196 'wglGetProcAddress',
197])
198
199
200class Blob:
201 '''Data-less proxy for bytearrays, to save memory.'''
202
203 def __init__(self, size, hash):
204 self.size = size
205 self.hash = hash
206
207 def __repr__(self):
208 return 'blob(%u)' % self.size
209
210 def __eq__(self, other):
211 return isinstance(other, Blob) and self.size == other.size and self.hash == other.hash
212
213 def __hash__(self):
214 return self.hash
215
216
217class BlobReplacer(Rebuilder):
218 '''Replace blobs with proxys.'''
219
220 def visitByteArray(self, obj):
221 return Blob(len(obj), hash(str(obj)))
222
223 def visitCall(self, call):
224 call.args = map(self.visit, call.args)
225 call.ret = self.visit(call.ret)
226
227
228class Loader(Unpickler):
229
230 def __init__(self, stream):
231 Unpickler.__init__(self, stream)
232 self.calls = []
233 self.rebuilder = BlobReplacer()
234
235 def handleCall(self, call):
236 if call.functionName not in ignoredFunctionNames:
237 self.rebuilder.visitCall(call)
238 self.calls.append(call)
239
240
241class PythonDiffer(Differ):
242
243 def __init__(self, apitrace, callNos = False):
244 Differ.__init__(self, apitrace)
245 self.a = None
246 self.b = None
247 if self.isatty:
248 self.highlighter = LessHighlighter()
249 else:
José Fonseca975f1242012-12-06 07:11:29 +0000250 self.highlighter = PlainHighlighter()
José Fonseca8f9116e2012-11-16 22:19:10 +0000251 self.delete_color = self.highlighter.red
252 self.insert_color = self.highlighter.green
253 self.callNos = callNos
254 self.aSpace = 0
255 self.bSpace = 0
256 self.dumper = Dumper()
257
258 def setRefTrace(self, ref_trace, ref_calls):
259 self.a = self.readTrace(ref_trace, ref_calls)
260
261 def setSrcTrace(self, src_trace, src_calls):
262 self.b = self.readTrace(src_trace, src_calls)
263
264 def readTrace(self, trace, calls):
265 p = subprocess.Popen(
266 args = [
267 self.apitrace,
268 'pickle',
269 '--symbolic',
270 '--calls=' + calls,
271 trace
272 ],
273 stdout = subprocess.PIPE,
274 )
275
276 parser = Loader(p.stdout)
277 parser.parse()
278 return parser.calls
279
280 def diff(self):
281 try:
282 self._diff()
283 except IOError:
284 pass
285
286 def _diff(self):
287 matcher = difflib.SequenceMatcher(self.isjunk, self.a, self.b)
288 for tag, alo, ahi, blo, bhi in matcher.get_opcodes():
289 if tag == 'replace':
290 self.replace(alo, ahi, blo, bhi)
291 elif tag == 'delete':
292 self.delete(alo, ahi, blo, bhi)
293 elif tag == 'insert':
294 self.insert(alo, ahi, blo, bhi)
295 elif tag == 'equal':
296 self.equal(alo, ahi, blo, bhi)
297 else:
298 raise ValueError, 'unknown tag %s' % (tag,)
299
300 def isjunk(self, call):
301 return call.functionName == 'glGetError' and call.ret in ('GL_NO_ERROR', 0)
302
303 def replace(self, alo, ahi, blo, bhi):
304 assert alo < ahi and blo < bhi
305
306 a_names = [call.functionName for call in self.a[alo:ahi]]
307 b_names = [call.functionName for call in self.b[blo:bhi]]
308
309 matcher = difflib.SequenceMatcher(None, a_names, b_names)
310 for tag, _alo, _ahi, _blo, _bhi in matcher.get_opcodes():
311 _alo += alo
312 _ahi += alo
313 _blo += blo
314 _bhi += blo
315 if tag == 'replace':
316 self.replace_dissimilar(_alo, _ahi, _blo, _bhi)
317 elif tag == 'delete':
318 self.delete(_alo, _ahi, _blo, _bhi)
319 elif tag == 'insert':
320 self.insert(_alo, _ahi, _blo, _bhi)
321 elif tag == 'equal':
322 self.replace_similar(_alo, _ahi, _blo, _bhi)
323 else:
324 raise ValueError, 'unknown tag %s' % (tag,)
325
326 def replace_similar(self, alo, ahi, blo, bhi):
327 assert alo < ahi and blo < bhi
328 assert ahi - alo == bhi - blo
329 for i in xrange(0, bhi - blo):
330 self.highlighter.write('| ')
331 a_call = self.a[alo + i]
332 b_call = self.b[blo + i]
333 assert a_call.functionName == b_call.functionName
334 self.dumpCallNos(a_call.no, b_call.no)
335 self.highlighter.bold(True)
336 self.highlighter.write(b_call.functionName)
337 self.highlighter.bold(False)
338 self.highlighter.write('(')
339 sep = ''
340 numArgs = max(len(a_call.args), len(b_call.args))
341 for j in xrange(numArgs):
342 self.highlighter.write(sep)
343 try:
344 a_arg = a_call.args[j]
345 except IndexError:
346 pass
347 try:
348 b_arg = b_call.args[j]
349 except IndexError:
350 pass
351 self.replace_value(a_arg, b_arg)
352 sep = ', '
353 self.highlighter.write(')')
354 if a_call.ret is not None or b_call.ret is not None:
355 self.highlighter.write(' = ')
356 self.replace_value(a_call.ret, b_call.ret)
357 self.highlighter.write('\n')
358
359 def replace_dissimilar(self, alo, ahi, blo, bhi):
360 assert alo < ahi and blo < bhi
361 if bhi - blo < ahi - alo:
362 self.insert(alo, alo, blo, bhi)
363 self.delete(alo, ahi, bhi, bhi)
364 else:
365 self.delete(alo, ahi, blo, blo)
366 self.insert(ahi, ahi, blo, bhi)
367
368 def replace_value(self, a, b):
369 if b == a:
370 self.highlighter.write(self.dumper.visit(b))
371 else:
372 self.highlighter.strike()
373 self.highlighter.color(self.delete_color)
374 self.highlighter.write(self.dumper.visit(a))
375 self.highlighter.normal()
376 self.highlighter.write(" ")
377 self.highlighter.color(self.insert_color)
378 self.highlighter.write(self.dumper.visit(b))
379 self.highlighter.normal()
380
381 escape = "\33["
382
383 def delete(self, alo, ahi, blo, bhi):
384 assert alo < ahi
385 assert blo == bhi
386 for i in xrange(alo, ahi):
387 call = self.a[i]
388 self.highlighter.write('- ')
389 self.dumpCallNos(call.no, None)
390 self.highlighter.strike()
391 self.highlighter.color(self.delete_color)
392 self.dumpCall(call)
393
394 def insert(self, alo, ahi, blo, bhi):
395 assert alo == ahi
396 assert blo < bhi
397 for i in xrange(blo, bhi):
398 call = self.b[i]
399 self.highlighter.write('+ ')
400 self.dumpCallNos(None, call.no)
401 self.highlighter.color(self.insert_color)
402 self.dumpCall(call)
403
404 def equal(self, alo, ahi, blo, bhi):
405 assert alo < ahi and blo < bhi
406 assert ahi - alo == bhi - blo
407 for i in xrange(0, bhi - blo):
408 self.highlighter.write(' ')
409 a_call = self.a[alo + i]
410 b_call = self.b[blo + i]
411 assert a_call.functionName == b_call.functionName
412 assert len(a_call.args) == len(b_call.args)
413 self.dumpCallNos(a_call.no, b_call.no)
414 self.dumpCall(b_call)
415
416 def dumpCallNos(self, aNo, bNo):
417 if not self.callNos:
418 return
419
José Fonsecafcf044b2014-03-27 17:39:04 +0000420 if aNo is not None and bNo is not None and aNo == bNo:
421 aNoStr = str(aNo)
422 self.highlighter.write(aNoStr)
423 self.aSpace = len(aNoStr)
424 self.bSpace = self.aSpace
425 self.highlighter.write(' ')
426 return
427
José Fonseca8f9116e2012-11-16 22:19:10 +0000428 if aNo is None:
429 self.highlighter.write(' '*self.aSpace)
430 else:
431 aNoStr = str(aNo)
432 self.highlighter.strike()
433 self.highlighter.color(self.delete_color)
434 self.highlighter.write(aNoStr)
435 self.highlighter.normal()
436 self.aSpace = len(aNoStr)
437 self.highlighter.write(' ')
438 if bNo is None:
439 self.highlighter.write(' '*self.bSpace)
440 else:
441 bNoStr = str(bNo)
442 self.highlighter.color(self.insert_color)
443 self.highlighter.write(bNoStr)
444 self.highlighter.normal()
445 self.bSpace = len(bNoStr)
446 self.highlighter.write(' ')
447
448 def dumpCall(self, call):
449 self.highlighter.bold(True)
450 self.highlighter.write(call.functionName)
451 self.highlighter.bold(False)
452 self.highlighter.write('(' + ', '.join(itertools.imap(self.dumper.visit, call.args)) + ')')
453 if call.ret is not None:
454 self.highlighter.write(' = ' + self.dumper.visit(call.ret))
455 self.highlighter.normal()
456 self.highlighter.write('\n')
457
458
459
460##########################################################################/
461#
462# Main program
463#
José Fonsecaa65795f2012-02-18 18:15:18 +0000464
465
José Fonsecac6998962012-03-16 09:56:25 +0000466def which(executable):
467 '''Search for the executable on the PATH.'''
468
469 if platform.system() == 'Windows':
470 exts = ['.exe']
471 else:
472 exts = ['']
473 dirs = os.environ['PATH'].split(os.path.pathsep)
474 for dir in dirs:
475 path = os.path.join(dir, executable)
476 for ext in exts:
477 if os.path.exists(path + ext):
478 return True
479 return False
480
481
José Fonsecaa65795f2012-02-18 18:15:18 +0000482def main():
483 '''Main program.
484 '''
485
José Fonsecaa65795f2012-02-18 18:15:18 +0000486 # Parse command line options
487 optparser = optparse.OptionParser(
José Fonseca8f9116e2012-11-16 22:19:10 +0000488 usage='\n\t%prog [options] TRACE TRACE',
José Fonsecaa65795f2012-02-18 18:15:18 +0000489 version='%%prog')
490 optparser.add_option(
491 '-a', '--apitrace', metavar='PROGRAM',
492 type='string', dest='apitrace', default='apitrace',
493 help='apitrace command [default: %default]')
494 optparser.add_option(
José Fonseca155380f2013-10-18 17:15:17 -0700495 '-t', '--tool', metavar='TOOL',
José Fonseca8f9116e2012-11-16 22:19:10 +0000496 type="choice", choices=('diff', 'sdiff', 'wdiff', 'python'),
José Fonseca155380f2013-10-18 17:15:17 -0700497 dest="tool", default=None,
498 help="diff tool: diff, sdiff, wdiff, or python [default: auto]")
José Fonsecac6998962012-03-16 09:56:25 +0000499 optparser.add_option(
José Fonsecaa65795f2012-02-18 18:15:18 +0000500 '-c', '--calls', metavar='CALLSET',
José Fonseca8f9116e2012-11-16 22:19:10 +0000501 type="string", dest="calls", default='0-10000',
José Fonsecaa65795f2012-02-18 18:15:18 +0000502 help="calls to compare [default: %default]")
503 optparser.add_option(
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000504 '--ref-calls', metavar='CALLSET',
505 type="string", dest="ref_calls", default=None,
506 help="calls to compare from reference trace")
507 optparser.add_option(
508 '--src-calls', metavar='CALLSET',
509 type="string", dest="src_calls", default=None,
510 help="calls to compare from source trace")
511 optparser.add_option(
José Fonseca8f9116e2012-11-16 22:19:10 +0000512 '--call-nos',
513 action="store_true",
514 dest="call_nos", default=False,
515 help="dump call numbers")
516 optparser.add_option(
José Fonsecaa65795f2012-02-18 18:15:18 +0000517 '-w', '--width', metavar='NUM',
José Fonseca8f9116e2012-11-16 22:19:10 +0000518 type="int", dest="width",
519 help="columns [default: auto]")
José Fonsecaa65795f2012-02-18 18:15:18 +0000520
José Fonsecaa65795f2012-02-18 18:15:18 +0000521 (options, args) = optparser.parse_args(sys.argv[1:])
522 if len(args) != 2:
523 optparser.error("incorrect number of arguments")
524
José Fonseca155380f2013-10-18 17:15:17 -0700525 if options.tool is None:
José Fonseca8f9116e2012-11-16 22:19:10 +0000526 if platform.system() == 'Windows':
José Fonseca155380f2013-10-18 17:15:17 -0700527 options.tool = 'python'
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000528 else:
José Fonseca8f9116e2012-11-16 22:19:10 +0000529 if which('wdiff'):
José Fonseca155380f2013-10-18 17:15:17 -0700530 options.tool = 'wdiff'
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000531 else:
José Fonseca8f9116e2012-11-16 22:19:10 +0000532 sys.stderr.write('warning: wdiff not found\n')
533 if which('sdiff'):
José Fonseca155380f2013-10-18 17:15:17 -0700534 options.tool = 'sdiff'
José Fonseca8f9116e2012-11-16 22:19:10 +0000535 else:
536 sys.stderr.write('warning: sdiff not found\n')
José Fonseca155380f2013-10-18 17:15:17 -0700537 options.tool = 'diff'
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000538
539 if options.ref_calls is None:
540 options.ref_calls = options.calls
541 if options.src_calls is None:
542 options.src_calls = options.calls
543
José Fonseca8f9116e2012-11-16 22:19:10 +0000544 ref_trace, src_trace = args
545
José Fonseca155380f2013-10-18 17:15:17 -0700546 if options.tool == 'python':
José Fonseca8f9116e2012-11-16 22:19:10 +0000547 differ = PythonDiffer(options.apitrace, options.call_nos)
548 else:
José Fonseca095fb952014-02-04 14:27:05 +0000549 differ = ExternalDiffer(options.apitrace, options.tool, options.width, options.call_nos)
José Fonseca8f9116e2012-11-16 22:19:10 +0000550 differ.setRefTrace(ref_trace, options.ref_calls)
551 differ.setSrcTrace(src_trace, options.src_calls)
552 differ.diff()
José Fonsecaa65795f2012-02-18 18:15:18 +0000553
554
555if __name__ == '__main__':
556 main()