blob: ed61e39db796940d2b6c38f4b231ad060650379f [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:
José Fonseca87c34802014-06-20 14:13:13 +0100344 a_argName, a_argVal = a_call.args[j]
José Fonseca8f9116e2012-11-16 22:19:10 +0000345 except IndexError:
346 pass
347 try:
José Fonseca87c34802014-06-20 14:13:13 +0100348 b_argName, b_argVal = b_call.args[j]
José Fonseca8f9116e2012-11-16 22:19:10 +0000349 except IndexError:
350 pass
José Fonseca87c34802014-06-20 14:13:13 +0100351 self.replace_value(a_argName, b_argName)
352 self.highlighter.write(' = ')
353 self.replace_value(a_argVal, b_argVal)
José Fonseca8f9116e2012-11-16 22:19:10 +0000354 sep = ', '
355 self.highlighter.write(')')
356 if a_call.ret is not None or b_call.ret is not None:
357 self.highlighter.write(' = ')
358 self.replace_value(a_call.ret, b_call.ret)
359 self.highlighter.write('\n')
360
361 def replace_dissimilar(self, alo, ahi, blo, bhi):
362 assert alo < ahi and blo < bhi
363 if bhi - blo < ahi - alo:
364 self.insert(alo, alo, blo, bhi)
365 self.delete(alo, ahi, bhi, bhi)
366 else:
367 self.delete(alo, ahi, blo, blo)
368 self.insert(ahi, ahi, blo, bhi)
369
370 def replace_value(self, a, b):
371 if b == a:
372 self.highlighter.write(self.dumper.visit(b))
373 else:
374 self.highlighter.strike()
375 self.highlighter.color(self.delete_color)
376 self.highlighter.write(self.dumper.visit(a))
377 self.highlighter.normal()
378 self.highlighter.write(" ")
379 self.highlighter.color(self.insert_color)
380 self.highlighter.write(self.dumper.visit(b))
381 self.highlighter.normal()
382
383 escape = "\33["
384
385 def delete(self, alo, ahi, blo, bhi):
386 assert alo < ahi
387 assert blo == bhi
388 for i in xrange(alo, ahi):
389 call = self.a[i]
390 self.highlighter.write('- ')
391 self.dumpCallNos(call.no, None)
392 self.highlighter.strike()
393 self.highlighter.color(self.delete_color)
394 self.dumpCall(call)
395
396 def insert(self, alo, ahi, blo, bhi):
397 assert alo == ahi
398 assert blo < bhi
399 for i in xrange(blo, bhi):
400 call = self.b[i]
401 self.highlighter.write('+ ')
402 self.dumpCallNos(None, call.no)
403 self.highlighter.color(self.insert_color)
404 self.dumpCall(call)
405
406 def equal(self, alo, ahi, blo, bhi):
407 assert alo < ahi and blo < bhi
408 assert ahi - alo == bhi - blo
409 for i in xrange(0, bhi - blo):
410 self.highlighter.write(' ')
411 a_call = self.a[alo + i]
412 b_call = self.b[blo + i]
413 assert a_call.functionName == b_call.functionName
414 assert len(a_call.args) == len(b_call.args)
415 self.dumpCallNos(a_call.no, b_call.no)
416 self.dumpCall(b_call)
417
418 def dumpCallNos(self, aNo, bNo):
419 if not self.callNos:
420 return
421
José Fonsecafcf044b2014-03-27 17:39:04 +0000422 if aNo is not None and bNo is not None and aNo == bNo:
423 aNoStr = str(aNo)
424 self.highlighter.write(aNoStr)
425 self.aSpace = len(aNoStr)
426 self.bSpace = self.aSpace
427 self.highlighter.write(' ')
428 return
429
José Fonseca8f9116e2012-11-16 22:19:10 +0000430 if aNo is None:
431 self.highlighter.write(' '*self.aSpace)
432 else:
433 aNoStr = str(aNo)
434 self.highlighter.strike()
435 self.highlighter.color(self.delete_color)
436 self.highlighter.write(aNoStr)
437 self.highlighter.normal()
438 self.aSpace = len(aNoStr)
439 self.highlighter.write(' ')
440 if bNo is None:
441 self.highlighter.write(' '*self.bSpace)
442 else:
443 bNoStr = str(bNo)
444 self.highlighter.color(self.insert_color)
445 self.highlighter.write(bNoStr)
446 self.highlighter.normal()
447 self.bSpace = len(bNoStr)
448 self.highlighter.write(' ')
449
450 def dumpCall(self, call):
451 self.highlighter.bold(True)
452 self.highlighter.write(call.functionName)
453 self.highlighter.bold(False)
José Fonseca87c34802014-06-20 14:13:13 +0100454 self.highlighter.write('(' + self.dumper.visitItems(call.args) + ')')
José Fonseca8f9116e2012-11-16 22:19:10 +0000455 if call.ret is not None:
456 self.highlighter.write(' = ' + self.dumper.visit(call.ret))
457 self.highlighter.normal()
458 self.highlighter.write('\n')
459
460
461
462##########################################################################/
463#
464# Main program
465#
José Fonsecaa65795f2012-02-18 18:15:18 +0000466
467
José Fonsecac6998962012-03-16 09:56:25 +0000468def which(executable):
469 '''Search for the executable on the PATH.'''
470
471 if platform.system() == 'Windows':
472 exts = ['.exe']
473 else:
474 exts = ['']
475 dirs = os.environ['PATH'].split(os.path.pathsep)
476 for dir in dirs:
477 path = os.path.join(dir, executable)
478 for ext in exts:
479 if os.path.exists(path + ext):
480 return True
481 return False
482
483
José Fonsecaa65795f2012-02-18 18:15:18 +0000484def main():
485 '''Main program.
486 '''
487
José Fonsecaa65795f2012-02-18 18:15:18 +0000488 # Parse command line options
489 optparser = optparse.OptionParser(
José Fonseca8f9116e2012-11-16 22:19:10 +0000490 usage='\n\t%prog [options] TRACE TRACE',
José Fonsecaa65795f2012-02-18 18:15:18 +0000491 version='%%prog')
492 optparser.add_option(
493 '-a', '--apitrace', metavar='PROGRAM',
494 type='string', dest='apitrace', default='apitrace',
495 help='apitrace command [default: %default]')
496 optparser.add_option(
José Fonseca155380f2013-10-18 17:15:17 -0700497 '-t', '--tool', metavar='TOOL',
José Fonseca8f9116e2012-11-16 22:19:10 +0000498 type="choice", choices=('diff', 'sdiff', 'wdiff', 'python'),
José Fonseca155380f2013-10-18 17:15:17 -0700499 dest="tool", default=None,
500 help="diff tool: diff, sdiff, wdiff, or python [default: auto]")
José Fonsecac6998962012-03-16 09:56:25 +0000501 optparser.add_option(
José Fonsecaa65795f2012-02-18 18:15:18 +0000502 '-c', '--calls', metavar='CALLSET',
José Fonseca8f9116e2012-11-16 22:19:10 +0000503 type="string", dest="calls", default='0-10000',
José Fonsecaa65795f2012-02-18 18:15:18 +0000504 help="calls to compare [default: %default]")
505 optparser.add_option(
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000506 '--ref-calls', metavar='CALLSET',
507 type="string", dest="ref_calls", default=None,
508 help="calls to compare from reference trace")
509 optparser.add_option(
510 '--src-calls', metavar='CALLSET',
511 type="string", dest="src_calls", default=None,
512 help="calls to compare from source trace")
513 optparser.add_option(
José Fonseca8f9116e2012-11-16 22:19:10 +0000514 '--call-nos',
515 action="store_true",
516 dest="call_nos", default=False,
517 help="dump call numbers")
518 optparser.add_option(
José Fonsecaa65795f2012-02-18 18:15:18 +0000519 '-w', '--width', metavar='NUM',
José Fonseca8f9116e2012-11-16 22:19:10 +0000520 type="int", dest="width",
521 help="columns [default: auto]")
José Fonsecaa65795f2012-02-18 18:15:18 +0000522
José Fonsecaa65795f2012-02-18 18:15:18 +0000523 (options, args) = optparser.parse_args(sys.argv[1:])
524 if len(args) != 2:
525 optparser.error("incorrect number of arguments")
526
José Fonseca155380f2013-10-18 17:15:17 -0700527 if options.tool is None:
José Fonseca8f9116e2012-11-16 22:19:10 +0000528 if platform.system() == 'Windows':
José Fonseca155380f2013-10-18 17:15:17 -0700529 options.tool = 'python'
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000530 else:
José Fonseca8f9116e2012-11-16 22:19:10 +0000531 if which('wdiff'):
José Fonseca155380f2013-10-18 17:15:17 -0700532 options.tool = 'wdiff'
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000533 else:
José Fonseca8f9116e2012-11-16 22:19:10 +0000534 sys.stderr.write('warning: wdiff not found\n')
535 if which('sdiff'):
José Fonseca155380f2013-10-18 17:15:17 -0700536 options.tool = 'sdiff'
José Fonseca8f9116e2012-11-16 22:19:10 +0000537 else:
538 sys.stderr.write('warning: sdiff not found\n')
José Fonseca155380f2013-10-18 17:15:17 -0700539 options.tool = 'diff'
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000540
541 if options.ref_calls is None:
542 options.ref_calls = options.calls
543 if options.src_calls is None:
544 options.src_calls = options.calls
545
José Fonseca8f9116e2012-11-16 22:19:10 +0000546 ref_trace, src_trace = args
547
José Fonseca155380f2013-10-18 17:15:17 -0700548 if options.tool == 'python':
José Fonseca8f9116e2012-11-16 22:19:10 +0000549 differ = PythonDiffer(options.apitrace, options.call_nos)
550 else:
José Fonseca095fb952014-02-04 14:27:05 +0000551 differ = ExternalDiffer(options.apitrace, options.tool, options.width, options.call_nos)
José Fonseca8f9116e2012-11-16 22:19:10 +0000552 differ.setRefTrace(ref_trace, options.ref_calls)
553 differ.setSrcTrace(src_trace, options.src_calls)
554 differ.diff()
José Fonsecaa65795f2012-02-18 18:15:18 +0000555
556
557if __name__ == '__main__':
558 main()