1#!/usr/local/bin/python3.8 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 28import difflib 29import itertools 30import optparse 31import os.path 32import platform 33import shutil 34import subprocess 35import sys 36import tempfile 37 38 39##########################################################################/ 40# 41# Abstract interface 42# 43 44 45class Differ: 46 47 def __init__(self, apitrace): 48 self.apitrace = apitrace 49 self.isatty = sys.stdout.isatty() 50 51 def setRefTrace(self, refTrace, ref_calls): 52 raise NotImplementedError 53 54 def setSrcTrace(self, srcTrace, 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, callNos): 70 self.output = tempfile.NamedTemporaryFile() 71 72 dump_args = [ 73 apitrace, 74 'dump', 75 '--color=never', 76 '--call-nos=' + ('yes' if callNos else 'no'), 77 '--arg-names=no', 78 '--calls=' + calls, 79 trace 80 ] 81 82 self.dump = subprocess.Popen( 83 args = dump_args, 84 stdout = self.output, 85 universal_newlines = True, 86 ) 87 88 89class ExternalDiffer(Differ): 90 91 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' 101 102 def __init__(self, apitrace, options): 103 Differ.__init__(self, apitrace) 104 tool = options.tool 105 callNos = options.callNos 106 107 self.diff_args = [tool] 108 if tool == 'diff': 109 self.diff_args += [ 110 '--speed-large-files', 111 ] 112 if self.isatty: 113 if options.suppressCommonLines: 114 self.diff_args += ['--unchanged-line-format='] 115 else: 116 self.diff_args += ['--unchanged-line-format=%l\n'] 117 self.diff_args += [ 118 '--old-line-format=' + self.start_delete + '%l' + self.end_delete + '\n', 119 '--new-line-format=' + self.start_insert + '%l' + self.end_insert + '\n', 120 ] 121 else: 122 if options.suppressCommonLines: 123 self.diff_args += ['--unchanged-line-format='] 124 else: 125 self.diff_args += ['--unchanged-line-format= %l\n'] 126 self.diff_args += [ 127 '--old-line-format=- %l\n', 128 '--new-line-format=+ %l\n', 129 ] 130 elif tool == 'sdiff': 131 if options.width is None: 132 import curses 133 curses.setupterm() 134 options.width = curses.tigetnum('cols') 135 self.diff_args += [ 136 '--width=%u' % options.width, 137 '--speed-large-files', 138 ] 139 elif tool == 'wdiff': 140 self.diff_args += [ 141 #'--terminal', 142 '--avoid-wraps', 143 ] 144 if self.isatty: 145 self.diff_args += [ 146 '--start-delete=' + self.start_delete, 147 '--end-delete=' + self.end_delete, 148 '--start-insert=' + self.start_insert, 149 '--end-insert=' + self.end_insert, 150 ] 151 else: 152 assert False 153 self.callNos = callNos 154 155 def setRefTrace(self, refTrace, ref_calls): 156 self.ref_dumper = AsciiDumper(self.apitrace, refTrace, ref_calls, self.callNos) 157 158 def setSrcTrace(self, srcTrace, src_calls): 159 self.src_dumper = AsciiDumper(self.apitrace, srcTrace, src_calls, self.callNos) 160 161 def diff(self): 162 diff_args = self.diff_args + [ 163 self.ref_dumper.output.name, 164 self.src_dumper.output.name, 165 ] 166 167 self.ref_dumper.dump.wait() 168 self.src_dumper.dump.wait() 169 170 less = None 171 diff_stdout = None 172 if self.isatty: 173 try: 174 less = subprocess.Popen( 175 args = ['less', '-FRXn'], 176 stdin = subprocess.PIPE, 177 ) 178 except OSError: 179 pass 180 else: 181 diff_stdout = less.stdin 182 183 diff = subprocess.Popen( 184 args = diff_args, 185 stdout = diff_stdout, 186 universal_newlines = True, 187 ) 188 189 diff.wait() 190 191 if less is not None: 192 less.stdin.close() 193 less.wait() 194 195 196##########################################################################/ 197# 198# Python diff 199# 200 201from unpickle import Unpickler, Dumper, Rebuilder 202from highlight import PlainHighlighter, LessHighlighter 203 204 205ignoredFunctionNames = set([ 206 'glGetString', 207 'glXGetClientString', 208 'glXGetCurrentDisplay', 209 'glXGetCurrentContext', 210 'glXGetProcAddress', 211 'glXGetProcAddressARB', 212 'wglGetProcAddress', 213]) 214 215 216class Blob: 217 '''Data-less proxy for bytes, to save memory.''' 218 219 def __init__(self, size, hash): 220 self.size = size 221 self.hash = hash 222 223 def __repr__(self): 224 return 'blob(%u)' % self.size 225 226 def __eq__(self, other): 227 return isinstance(other, Blob) and self.size == other.size and self.hash == other.hash 228 229 def __hash__(self): 230 return self.hash 231 232 233class BlobReplacer(Rebuilder): 234 '''Replace blobs with proxys.''' 235 236 def visitBytes(self, obj): 237 return Blob(len(obj), hash(str(obj))) 238 239 def visitCall(self, call): 240 call.args = list(map(self.visit, call.args)) 241 call.ret = self.visit(call.ret) 242 243 244class Loader(Unpickler): 245 246 def __init__(self, stream): 247 Unpickler.__init__(self, stream) 248 self.calls = [] 249 self.rebuilder = BlobReplacer() 250 251 def handleCall(self, call): 252 if call.functionName not in ignoredFunctionNames: 253 self.rebuilder.visitCall(call) 254 self.calls.append(call) 255 256 257class PythonDiffer(Differ): 258 259 def __init__(self, apitrace, options): 260 Differ.__init__(self, apitrace) 261 self.a = None 262 self.b = None 263 if self.isatty: 264 self.highlighter = LessHighlighter() 265 else: 266 self.highlighter = PlainHighlighter() 267 self.delete_color = self.highlighter.red 268 self.insert_color = self.highlighter.green 269 self.callNos = options.callNos 270 self.suppressCommonLines = options.suppressCommonLines 271 self.aSpace = 0 272 self.bSpace = 0 273 self.dumper = Dumper() 274 275 def setRefTrace(self, refTrace, ref_calls): 276 self.a = self.readTrace(refTrace, ref_calls) 277 278 def setSrcTrace(self, srcTrace, src_calls): 279 self.b = self.readTrace(srcTrace, src_calls) 280 281 def readTrace(self, trace, calls): 282 p = subprocess.Popen( 283 args = [ 284 self.apitrace, 285 'pickle', 286 '--symbolic', 287 '--calls=' + calls, 288 trace 289 ], 290 stdout=subprocess.PIPE, 291 ) 292 293 parser = Loader(p.stdout) 294 parser.parse() 295 return parser.calls 296 297 def diff(self): 298 try: 299 self._diff() 300 except IOError: 301 pass 302 303 def _diff(self): 304 matcher = difflib.SequenceMatcher(self.isjunk, self.a, self.b) 305 for tag, alo, ahi, blo, bhi in matcher.get_opcodes(): 306 if tag == 'replace': 307 self.replace(alo, ahi, blo, bhi) 308 elif tag == 'delete': 309 self.delete(alo, ahi, blo, bhi) 310 elif tag == 'insert': 311 self.insert(alo, ahi, blo, bhi) 312 elif tag == 'equal': 313 self.equal(alo, ahi, blo, bhi) 314 else: 315 raise ValueError('unknown tag %s' % (tag,)) 316 317 def isjunk(self, call): 318 return call.functionName == 'glGetError' and call.ret in ('GL_NO_ERROR', 0) 319 320 def replace(self, alo, ahi, blo, bhi): 321 assert alo < ahi and blo < bhi 322 323 a_names = [call.functionName for call in self.a[alo:ahi]] 324 b_names = [call.functionName for call in self.b[blo:bhi]] 325 326 matcher = difflib.SequenceMatcher(None, a_names, b_names) 327 for tag, _alo, _ahi, _blo, _bhi in matcher.get_opcodes(): 328 _alo += alo 329 _ahi += alo 330 _blo += blo 331 _bhi += blo 332 if tag == 'replace': 333 self.replace_dissimilar(_alo, _ahi, _blo, _bhi) 334 elif tag == 'delete': 335 self.delete(_alo, _ahi, _blo, _bhi) 336 elif tag == 'insert': 337 self.insert(_alo, _ahi, _blo, _bhi) 338 elif tag == 'equal': 339 self.replace_similar(_alo, _ahi, _blo, _bhi) 340 else: 341 raise ValueError('unknown tag %s' % (tag,)) 342 343 def replace_similar(self, alo, ahi, blo, bhi): 344 assert alo < ahi and blo < bhi 345 assert ahi - alo == bhi - blo 346 for i in range(0, bhi - blo): 347 self.highlighter.write('| ') 348 a_call = self.a[alo + i] 349 b_call = self.b[blo + i] 350 assert a_call.functionName == b_call.functionName 351 self.dumpCallNos(a_call.no, b_call.no) 352 self.highlighter.bold(True) 353 self.highlighter.write(b_call.functionName) 354 self.highlighter.bold(False) 355 self.highlighter.write('(') 356 sep = '' 357 numArgs = max(len(a_call.args), len(b_call.args)) 358 for j in range(numArgs): 359 self.highlighter.write(sep) 360 try: 361 a_argName, a_argVal = a_call.args[j] 362 except IndexError: 363 pass 364 try: 365 b_argName, b_argVal = b_call.args[j] 366 except IndexError: 367 pass 368 self.replace_value(a_argName, b_argName) 369 self.highlighter.write(' = ') 370 self.replace_value(a_argVal, b_argVal) 371 sep = ', ' 372 self.highlighter.write(')') 373 if a_call.ret is not None or b_call.ret is not None: 374 self.highlighter.write(' = ') 375 self.replace_value(a_call.ret, b_call.ret) 376 self.highlighter.write('\n') 377 378 def replace_dissimilar(self, alo, ahi, blo, bhi): 379 assert alo < ahi and blo < bhi 380 if bhi - blo < ahi - alo: 381 self.insert(alo, alo, blo, bhi) 382 self.delete(alo, ahi, bhi, bhi) 383 else: 384 self.delete(alo, ahi, blo, blo) 385 self.insert(ahi, ahi, blo, bhi) 386 387 def replace_value(self, a, b): 388 if b == a: 389 self.highlighter.write(self.dumper.visit(b)) 390 else: 391 self.highlighter.strike() 392 self.highlighter.color(self.delete_color) 393 self.highlighter.write(self.dumper.visit(a)) 394 self.highlighter.normal() 395 self.highlighter.write(" -> ") 396 self.highlighter.color(self.insert_color) 397 self.highlighter.write(self.dumper.visit(b)) 398 self.highlighter.normal() 399 400 escape = "\33[" 401 402 def delete(self, alo, ahi, blo, bhi): 403 assert alo < ahi 404 assert blo == bhi 405 for i in range(alo, ahi): 406 call = self.a[i] 407 self.highlighter.write('- ') 408 self.dumpCallNos(call.no, None) 409 self.highlighter.strike() 410 self.highlighter.color(self.delete_color) 411 self.dumpCall(call) 412 413 def insert(self, alo, ahi, blo, bhi): 414 assert alo == ahi 415 assert blo < bhi 416 for i in range(blo, bhi): 417 call = self.b[i] 418 self.highlighter.write('+ ') 419 self.dumpCallNos(None, call.no) 420 self.highlighter.color(self.insert_color) 421 self.dumpCall(call) 422 423 def equal(self, alo, ahi, blo, bhi): 424 if self.suppressCommonLines: 425 return 426 assert alo < ahi and blo < bhi 427 assert ahi - alo == bhi - blo 428 for i in range(0, bhi - blo): 429 self.highlighter.write(' ') 430 a_call = self.a[alo + i] 431 b_call = self.b[blo + i] 432 assert a_call.functionName == b_call.functionName 433 assert len(a_call.args) == len(b_call.args) 434 self.dumpCallNos(a_call.no, b_call.no) 435 self.dumpCall(b_call) 436 437 def dumpCallNos(self, aNo, bNo): 438 if not self.callNos: 439 return 440 441 if aNo is not None and bNo is not None and aNo == bNo: 442 aNoStr = str(aNo) 443 self.highlighter.write(aNoStr) 444 self.aSpace = len(aNoStr) 445 self.bSpace = self.aSpace 446 self.highlighter.write(' ') 447 return 448 449 if aNo is None: 450 self.highlighter.write(' '*self.aSpace) 451 else: 452 aNoStr = str(aNo) 453 self.highlighter.strike() 454 self.highlighter.color(self.delete_color) 455 self.highlighter.write(aNoStr) 456 self.highlighter.normal() 457 self.aSpace = len(aNoStr) 458 self.highlighter.write(' ') 459 if bNo is None: 460 self.highlighter.write(' '*self.bSpace) 461 else: 462 bNoStr = str(bNo) 463 self.highlighter.color(self.insert_color) 464 self.highlighter.write(bNoStr) 465 self.highlighter.normal() 466 self.bSpace = len(bNoStr) 467 self.highlighter.write(' ') 468 469 def dumpCall(self, call): 470 self.highlighter.bold(True) 471 self.highlighter.write(call.functionName) 472 self.highlighter.bold(False) 473 self.highlighter.write('(' + self.dumper.visitItems(call.args) + ')') 474 if call.ret is not None: 475 self.highlighter.write(' = ' + self.dumper.visit(call.ret)) 476 self.highlighter.normal() 477 self.highlighter.write('\n') 478 479 480 481##########################################################################/ 482# 483# Main program 484# 485 486 487def which(executable): 488 '''Search for the executable on the PATH.''' 489 490 if platform.system() == 'Windows': 491 exts = ['.exe'] 492 else: 493 exts = [''] 494 dirs = os.environ['PATH'].split(os.path.pathsep) 495 for dir in dirs: 496 path = os.path.join(dir, executable) 497 for ext in exts: 498 if os.path.exists(path + ext): 499 return True 500 return False 501 502 503def main(): 504 '''Main program. 505 ''' 506 507 # Parse command line options 508 optparser = optparse.OptionParser( 509 usage='\n\t%prog [options] TRACE TRACE', 510 version='%%prog') 511 optparser.add_option( 512 '-a', '--apitrace', metavar='PROGRAM', 513 type='string', dest='apitrace', default='apitrace', 514 help='apitrace command [default: %default]') 515 optparser.add_option( 516 '-t', '--tool', metavar='TOOL', 517 type="choice", choices=('diff', 'sdiff', 'wdiff', 'python'), 518 dest="tool", default=None, 519 help="diff tool: diff, sdiff, wdiff, or python [default: auto]") 520 optparser.add_option( 521 '-c', '--calls', metavar='CALLSET', 522 type="string", dest="calls", default='0-10000', 523 help="calls to compare [default: %default]") 524 optparser.add_option( 525 '--ref-calls', metavar='CALLSET', 526 type="string", dest="refCalls", default=None, 527 help="calls to compare from reference trace") 528 optparser.add_option( 529 '--src-calls', metavar='CALLSET', 530 type="string", dest="srcCalls", default=None, 531 help="calls to compare from source trace") 532 optparser.add_option( 533 '--call-nos', 534 action="store_true", 535 dest="callNos", default=False, 536 help="dump call numbers") 537 optparser.add_option( 538 '--suppress-common-lines', 539 action="store_true", 540 dest="suppressCommonLines", default=False, 541 help="do not output common lines") 542 optparser.add_option( 543 '-w', '--width', metavar='NUM', 544 type="int", dest="width", 545 help="columns [default: auto]") 546 547 (options, args) = optparser.parse_args(sys.argv[1:]) 548 if len(args) != 2: 549 optparser.error("incorrect number of arguments") 550 551 if options.tool is None: 552 if platform.system() == 'Windows': 553 options.tool = 'python' 554 else: 555 if which('wdiff'): 556 options.tool = 'wdiff' 557 else: 558 sys.stderr.write('warning: wdiff not found\n') 559 if which('sdiff'): 560 options.tool = 'sdiff' 561 else: 562 sys.stderr.write('warning: sdiff not found\n') 563 options.tool = 'diff' 564 565 if options.refCalls is None: 566 options.refCalls = options.calls 567 if options.srcCalls is None: 568 options.srcCalls = options.calls 569 570 refTrace, srcTrace = args 571 572 if options.tool == 'python': 573 factory = PythonDiffer 574 else: 575 factory = ExternalDiffer 576 differ = factory(options.apitrace, options) 577 differ.setRefTrace(refTrace, options.refCalls) 578 differ.setSrcTrace(srcTrace, options.srcCalls) 579 differ.diff() 580 581 582if __name__ == '__main__': 583 main() 584