2 ##########################################################################
4 # Copyright 2011 Jose Fonseca
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:
14 # The above copyright notice and this permission notice shall be included in
15 # all copies or substantial portions of the Software.
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
25 ##########################################################################/
39 ##########################################################################/
47 def __init__(self, apitrace):
48 self.apitrace = apitrace
49 self.isatty = sys.stdout.isatty()
51 def setRefTrace(self, ref_trace, ref_calls):
52 raise NotImplementedError
54 def setSrcTrace(self, src_trace, src_calls):
55 raise NotImplementedError
58 raise NotImplementedError
61 ##########################################################################/
69 def __init__(self, apitrace, trace, calls):
70 self.output = tempfile.NamedTemporaryFile()
82 self.dump = subprocess.Popen(
85 universal_newlines = True,
89 class ExternalDiffer(Differ):
91 if platform.system() == 'Windows':
97 start_delete = '\33[9m\33[31m'
99 start_insert = '\33[32m'
100 end_insert = '\33[0m'
102 def __init__(self, apitrace, tool, width=None):
103 Differ.__init__(self, apitrace)
104 self.diff_args = [tool]
107 '--speed-large-files',
111 '--old-line-format=' + self.start_delete + '%l' + self.end_delete + '\n',
112 '--new-line-format=' + self.start_insert + '%l' + self.end_insert + '\n',
114 elif tool == 'sdiff':
118 width = curses.tigetnum('cols')
120 '--width=%u' % width,
121 '--speed-large-files',
123 elif tool == 'wdiff':
130 '--start-delete=' + self.start_delete,
131 '--end-delete=' + self.end_delete,
132 '--start-insert=' + self.start_insert,
133 '--end-insert=' + self.end_insert,
138 def setRefTrace(self, ref_trace, ref_calls):
139 self.ref_dumper = AsciiDumper(self.apitrace, ref_trace, ref_calls)
141 def setSrcTrace(self, src_trace, src_calls):
142 self.src_dumper = AsciiDumper(self.apitrace, src_trace, src_calls)
145 diff_args = self.diff_args + [
146 self.ref_dumper.output.name,
147 self.src_dumper.output.name,
150 self.ref_dumper.dump.wait()
151 self.src_dumper.dump.wait()
155 less = subprocess.Popen(
156 args = ['less', '-FRXn'],
157 stdin = subprocess.PIPE
160 diff_stdout = less.stdin
164 diff = subprocess.Popen(
166 stdout = diff_stdout,
167 universal_newlines = True,
177 ##########################################################################/
182 from unpickle import Unpickler, Dumper, Rebuilder
183 from highlight import PlainHighlighter, LessHighlighter
186 ignoredFunctionNames = set([
188 'glXGetClientString',
189 'glXGetCurrentDisplay',
190 'glXGetCurrentContext',
192 'glXGetProcAddressARB',
198 '''Data-less proxy for bytearrays, to save memory.'''
200 def __init__(self, size, hash):
205 return 'blob(%u)' % self.size
207 def __eq__(self, other):
208 return isinstance(other, Blob) and self.size == other.size and self.hash == other.hash
214 class BlobReplacer(Rebuilder):
215 '''Replace blobs with proxys.'''
217 def visitByteArray(self, obj):
218 return Blob(len(obj), hash(str(obj)))
220 def visitCall(self, call):
221 call.args = map(self.visit, call.args)
222 call.ret = self.visit(call.ret)
225 class Loader(Unpickler):
227 def __init__(self, stream):
228 Unpickler.__init__(self, stream)
230 self.rebuilder = BlobReplacer()
232 def handleCall(self, call):
233 if call.functionName not in ignoredFunctionNames:
234 self.rebuilder.visitCall(call)
235 self.calls.append(call)
238 class PythonDiffer(Differ):
240 def __init__(self, apitrace, callNos = False):
241 Differ.__init__(self, apitrace)
245 self.highlighter = LessHighlighter()
247 self.highlighter = PlainHighlighter()
248 self.delete_color = self.highlighter.red
249 self.insert_color = self.highlighter.green
250 self.callNos = callNos
253 self.dumper = Dumper()
255 def setRefTrace(self, ref_trace, ref_calls):
256 self.a = self.readTrace(ref_trace, ref_calls)
258 def setSrcTrace(self, src_trace, src_calls):
259 self.b = self.readTrace(src_trace, src_calls)
261 def readTrace(self, trace, calls):
262 p = subprocess.Popen(
270 stdout = subprocess.PIPE,
273 parser = Loader(p.stdout)
284 matcher = difflib.SequenceMatcher(self.isjunk, self.a, self.b)
285 for tag, alo, ahi, blo, bhi in matcher.get_opcodes():
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)
293 self.equal(alo, ahi, blo, bhi)
295 raise ValueError, 'unknown tag %s' % (tag,)
297 def isjunk(self, call):
298 return call.functionName == 'glGetError' and call.ret in ('GL_NO_ERROR', 0)
300 def replace(self, alo, ahi, blo, bhi):
301 assert alo < ahi and blo < bhi
303 a_names = [call.functionName for call in self.a[alo:ahi]]
304 b_names = [call.functionName for call in self.b[blo:bhi]]
306 matcher = difflib.SequenceMatcher(None, a_names, b_names)
307 for tag, _alo, _ahi, _blo, _bhi in matcher.get_opcodes():
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)
319 self.replace_similar(_alo, _ahi, _blo, _bhi)
321 raise ValueError, 'unknown tag %s' % (tag,)
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('(')
337 numArgs = max(len(a_call.args), len(b_call.args))
338 for j in xrange(numArgs):
339 self.highlighter.write(sep)
341 a_arg = a_call.args[j]
345 b_arg = b_call.args[j]
348 self.replace_value(a_arg, b_arg)
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')
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)
362 self.delete(alo, ahi, blo, blo)
363 self.insert(ahi, ahi, blo, bhi)
365 def replace_value(self, a, b):
367 self.highlighter.write(self.dumper.visit(b))
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()
380 def delete(self, alo, ahi, blo, bhi):
383 for i in xrange(alo, ahi):
385 self.highlighter.write('- ')
386 self.dumpCallNos(call.no, None)
387 self.highlighter.strike()
388 self.highlighter.color(self.delete_color)
391 def insert(self, alo, ahi, blo, bhi):
394 for i in xrange(blo, bhi):
396 self.highlighter.write('+ ')
397 self.dumpCallNos(None, call.no)
398 self.highlighter.color(self.insert_color)
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)
413 def dumpCallNos(self, aNo, bNo):
418 self.highlighter.write(' '*self.aSpace)
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(' ')
428 self.highlighter.write(' '*self.bSpace)
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(' ')
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')
449 ##########################################################################/
455 def which(executable):
456 '''Search for the executable on the PATH.'''
458 if platform.system() == 'Windows':
462 dirs = os.environ['PATH'].split(os.path.pathsep)
464 path = os.path.join(dir, executable)
466 if os.path.exists(path + ext):
475 # Parse command line options
476 optparser = optparse.OptionParser(
477 usage='\n\t%prog [options] TRACE TRACE',
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(
485 type="choice", choices=('diff', 'sdiff', 'wdiff', 'python'),
486 dest="diff", default=None,
487 help="diff program: wdiff, sdiff, or diff [default: auto]")
488 optparser.add_option(
489 '-c', '--calls', metavar='CALLSET',
490 type="string", dest="calls", default='0-10000',
491 help="calls to compare [default: %default]")
492 optparser.add_option(
493 '--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(
503 dest="call_nos", default=False,
504 help="dump call numbers")
505 optparser.add_option(
506 '-w', '--width', metavar='NUM',
507 type="int", dest="width",
508 help="columns [default: auto]")
510 (options, args) = optparser.parse_args(sys.argv[1:])
512 optparser.error("incorrect number of arguments")
514 if options.diff is None:
515 if platform.system() == 'Windows':
516 options.diff = 'python'
519 options.diff = 'wdiff'
521 sys.stderr.write('warning: wdiff not found\n')
523 options.diff = 'sdiff'
525 sys.stderr.write('warning: sdiff not found\n')
526 options.diff = 'diff'
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
533 ref_trace, src_trace = args
535 if options.diff == 'python':
536 differ = PythonDiffer(options.apitrace, options.call_nos)
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)
544 if __name__ == '__main__':