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