2 ##########################################################################
4 # Copyright 2008-2012 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 ##########################################################################/
43 def match(self, value, mo):
44 raise NotImplementedError
46 def _matchSequence(self, refValues, srcValues, mo):
47 if not isinstance(srcValues, (list, tuple)):
50 if len(refValues) != len(srcValues):
53 for refValue, srcValue in zip(refValues, srcValues):
54 if not refValue.match(srcValue, mo):
59 raise NotImplementerError
65 class WildcardMatcher(Matcher):
67 def __init__(self, name = ''):
70 def match(self, value, mo):
73 refValue = mo.params[self.name]
75 mo.params[self.name] = value
77 return refValue == value
81 return '<' + self.name + '>'
84 class LiteralMatcher(Matcher):
86 def __init__(self, refValue):
87 self.refValue = refValue
89 def match(self, value, mo):
90 return self.refValue == value
93 return repr(self.refValue)
96 class ApproxMatcher(Matcher):
98 def __init__(self, refValue, tolerance = 2**-23):
99 self.refValue = refValue
100 self.tolerance = tolerance
102 def match(self, value, mo):
103 if not isinstance(value, float):
106 error = abs(self.refValue - value)
108 error = error / self.refValue
109 return error <= self.tolerance
112 return repr(self.refValue)
115 class StringMatcher(Matcher):
117 def __init__(self, refValue):
118 self.refValue = refValue
120 def isShaderDisassembly(self, value):
121 return value.find('// Generated by Microsoft (R) D3D Shader Disassembler\n') != -1
123 def normalizeShaderDisassembly(self, value):
124 # Unfortunately slightly different disassemblers produce different output
125 return '\n'.join([line.strip() for line in value.split('\n') if line.strip() and not line.startswith('//')])
127 def match(self, value, mo):
128 if self.isShaderDisassembly(self.refValue) and self.isShaderDisassembly(value):
129 return self.normalizeShaderDisassembly(self.refValue) == self.normalizeShaderDisassembly(value)
130 return self.refValue == value
133 return repr(self.refValue)
136 class BitmaskMatcher(Matcher):
138 def __init__(self, refElements):
139 self.refElements = refElements
141 def match(self, value, mo):
142 return self._matchSequence(self.refElements, value, mo)
145 return ' | '.join(map(str, self.refElements))
148 class OffsetMatcher(Matcher):
150 def __init__(self, refValue, offset):
151 self.refValue = refValue
154 def match(self, value, mo):
155 return self.refValue.match(value - self.offset, mo)
158 return '%s + %i' % (self.refValue, self.offset)
161 class ArrayMatcher(Matcher):
163 def __init__(self, refElements):
164 self.refElements = refElements
166 def match(self, value, mo):
167 return self._matchSequence(self.refElements, value, mo)
170 return '{' + ', '.join(map(str, self.refElements)) + '}'
173 class StructMatcher(Matcher):
175 def __init__(self, refMembers):
176 self.refMembers = refMembers
178 def match(self, value, mo):
179 if not isinstance(value, dict):
182 if len(value) != len(self.refMembers):
185 for name, refMember in self.refMembers.iteritems():
191 if not refMember.match(member, mo):
197 return '{' + ', '.join(['%s = %s' % refMember for refMember in self.refMembers.iteritems()]) + '}'
200 class CallMatcher(Matcher):
202 def __init__(self, callNo, functionName, args, ret):
204 self.functionName = functionName
208 def match(self, call, mo):
209 callNo, srcFunctionName, srcArgs, srcRet = call
211 if self.functionName != srcFunctionName:
214 refArgs = [value for name, value in self.args]
215 srcArgs = [value for name, value in srcArgs]
217 if not self._matchSequence(refArgs, srcArgs, mo):
221 if srcRet is not None:
224 if not self.ret.match(srcRet, mo):
227 if self.callNo is not None:
228 if not self.callNo.match(callNo, mo):
234 s = self.functionName
235 s += '(' + ', '.join(['%s = %s' % refArg for refArg in self.args]) + ')'
236 if self.ret is not None:
237 s += ' = ' + str(self.ret)
241 class TraceMismatch(Exception):
248 def __init__(self, calls):
251 def match(self, calls, verbose = False):
253 srcCalls = iter(calls)
254 for refCall in self.calls:
260 srcCall = srcCalls.next()
261 except StopIteration:
263 raise TraceMismatch('missing call `%s` (found `%s`)' % (refCall, skippedSrcCalls[0]))
265 raise TraceMismatch('missing call %s' % refCall)
267 print '\t%s %s%r = %r' % srcCall
268 if refCall.match(srcCall, mo):
271 skippedSrcCalls.append(srcCall)
275 return ''.join(['%s\n' % call for call in self.calls])
278 #######################################################################
284 class ParseError(Exception):
286 def __init__(self, msg=None, filename=None, line=None, col=None):
288 self.filename = filename
293 return ':'.join([str(part) for part in (self.filename, self.line, self.col, self.msg) if part != None])
297 """Stateless scanner."""
299 # should be overriden by derived classes
308 flags |= re.IGNORECASE
309 self.tokens_re = re.compile(
310 '|'.join(['(' + regexp + ')' for type, regexp, test_lit in self.tokens]),
314 def next(self, buf, pos):
317 mo = self.tokens_re.match(buf, pos)
320 type, regexp, test_lit = self.tokens[mo.lastindex - 1]
323 type = self.literals.get(text, type)
324 return type, text, pos
327 return self.symbols.get(c, None), c, pos + 1
332 def __init__(self, type, text, line, col):
341 # should be overriden by derived classes
345 newline_re = re.compile(r'\r\n?|\n')
347 def __init__(self, buf = None, pos = 0, filename = None, fp = None):
351 length = os.path.getsize(fp.name)
354 # read whole file into memory
358 # map the whole file into memory
360 # length must not be zero
361 buf = mmap.mmap(fileno, length, access = mmap.ACCESS_READ)
362 pos = os.lseek(fileno, 0, 1)
370 except AttributeError:
377 self.filename = filename
386 type, text, endpos = self.scanner.next(self.buf, pos)
387 assert pos + len(text) == endpos
389 type, text = self.filter(type, text)
395 msg = 'unexpected char '
396 if text >= ' ' and text <= '~':
399 msg += "0x%X" % ord(text)
400 raise ParseError(msg, self.filename, line, col)
403 return Token(type = type, text = text, line = line, col = col)
405 def consume(self, text):
408 for mo in self.newline_re.finditer(text, pos):
413 # update column number
415 tabpos = text.find('\t', pos)
418 self.col += tabpos - pos
419 self.col = ((self.col - 1)//self.tabsize + 1)*self.tabsize + 1
421 self.col += len(text) - pos
423 def filter(self, type, text):
429 def __init__(self, lexer):
431 self.lookahead = self.lexer.next()
433 def match(self, type):
434 return self.lookahead.type == type
436 def skip(self, type):
437 while not self.match(type):
442 msg = 'unexpected token %r' % self.lookahead.text,
443 filename = self.lexer.filename,
444 line = self.lookahead.line,
445 col = self.lookahead.col)
447 def consume(self, type = None):
448 if type is not None and not self.match(type):
450 token = self.lookahead
451 self.lookahead = self.lexer.next()
455 #######################################################################
457 ID, NUMBER, HEXNUM, STRING, WILDCARD, LPAREN, RPAREN, LCURLY, RCURLY, COMMA, AMP, EQUAL, PLUS, VERT, BLOB = xrange(15)
460 class CallScanner(Scanner):
462 # token regular expression table
465 (SKIP, r'[ \t\f\r\n\v]+', False),
468 (SKIP, r'//[^\r\n]*', False),
471 (ID, r'[a-zA-Z_][a-zA-Z0-9_]*(?:::[a-zA-Z_][a-zA-Z0-9_]*)?', True),
474 (HEXNUM, r'-?0x[0-9a-fA-F]+', False),
477 (NUMBER, r'-?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)(?:[eE][-+][0-9]+)?', False),
480 (STRING, r'"[^"\\]*(?:\\.[^"\\]*)*"', False),
483 (WILDCARD, r'<[^>]*>', False),
505 class CallLexer(Lexer):
507 scanner = CallScanner()
509 def filter(self, type, text):
514 text = text.replace('\\"', '"')
519 class TraceParser(Parser):
521 def __init__(self, stream):
522 lexer = CallLexer(fp = stream)
523 Parser.__init__(self, lexer)
526 return self.match(EOF)
529 while not self.eof():
531 return TraceMatcher(self.calls)
533 def parse_call(self):
534 if self.lookahead.type == NUMBER:
535 token = self.consume()
536 callNo = self.handleInt(int(token.text))
537 elif self.lookahead.type == WILDCARD:
538 token = self.consume()
539 callNo = self.handleWildcard((token.text[1:-1]))
543 functionName = self.consume(ID).text
545 args = self.parse_sequence(LPAREN, RPAREN, self.parse_pair)
547 if self.match(EQUAL):
549 ret = self.parse_value()
553 self.handleCall(callNo, functionName, args, ret)
555 def parse_pair(self):
556 '''Parse a `name = value` pair.'''
557 name = self.consume(ID).text
559 value = self.parse_value()
562 def parse_opt_pair(self):
563 '''Parse an optional `name = value` pair.'''
565 token = self.consume(ID)
566 if self.match(EQUAL):
569 value = self.parse_value()
572 value = self.handleID(token.text)
575 value = self.parse_value()
581 def parse_value(self):
582 value = self._parse_value()
585 while self.match(VERT):
587 value = self._parse_value()
589 return self.handleBitmask(flags)
590 elif self.match(PLUS):
592 if self.match(NUMBER):
593 token = self.consume()
594 offset = int(token.text)
595 elif self.match(HEXNUM):
596 token = self.consume()
597 offset = int(token.text, 16)
600 return self.handleOffset(value, offset)
604 def _parse_value(self):
607 value = [self.parse_value()]
608 return self.handleArray(value)
610 token = self.consume()
612 return self.handleID(value)
613 elif self.match(STRING):
614 token = self.consume()
616 return self.handleString(value)
617 elif self.match(NUMBER):
618 token = self.consume()
620 value = int(token.text)
622 value = float(token.text)
623 return self.handleFloat(value)
625 return self.handleInt(value)
626 elif self.match(HEXNUM):
627 token = self.consume()
628 value = int(token.text, 16)
629 return self.handleInt(value)
630 elif self.match(LCURLY):
631 value = self.parse_sequence(LCURLY, RCURLY, self.parse_opt_pair)
632 if len(value) and isinstance(value[0], tuple):
634 return self.handleStruct(value)
636 return self.handleArray(value)
637 elif self.match(BLOB):
638 token = self.consume()
640 token = self.consume()
641 length = int(token.text)
643 return self.handleBlob(length)
644 elif self.match(WILDCARD):
645 token = self.consume()
646 return self.handleWildcard(token.text[1:-1])
650 def parse_sequence(self, ltype, rtype, elementParser):
651 '''Parse a comma separated list'''
657 while not self.match(rtype):
662 element = elementParser()
663 elements.append(element)
668 def handleID(self, value):
669 raise NotImplementedError
671 def handleInt(self, value):
672 raise NotImplementedError
674 def handleFloat(self, value):
675 raise NotImplementedError
677 def handleString(self, value):
678 raise NotImplementedError
680 def handleBitmask(self, value):
681 raise NotImplementedError
683 def handleOffset(self, value, offset):
684 raise NotImplementedError
686 def handleArray(self, value):
687 raise NotImplementedError
689 def handleStruct(self, value):
690 raise NotImplementedError
692 def handleBlob(self, length):
693 return self.handleID('blob(%u)' % length)
695 def handleWildcard(self, name):
696 raise NotImplementedError
698 def handleCall(self, callNo, functionName, args, ret):
699 raise NotImplementedError
702 class RefTraceParser(TraceParser):
704 def __init__(self, fileName):
705 TraceParser.__init__(self, open(fileName, 'rt'))
709 TraceParser.parse(self)
710 return TraceMatcher(self.calls)
712 def handleID(self, value):
713 return LiteralMatcher(value)
715 def handleInt(self, value):
716 return LiteralMatcher(value)
718 def handleFloat(self, value):
719 return ApproxMatcher(value)
721 def handleString(self, value):
722 return StringMatcher(value)
724 def handleBitmask(self, value):
725 return BitmaskMatcher(value)
727 def handleOffset(self, value, offset):
728 return OffsetMatcher(value, offset)
730 def handleArray(self, value):
731 return ArrayMatcher(value)
733 def handleStruct(self, value):
734 return StructMatcher(value)
736 def handleWildcard(self, name):
737 return WildcardMatcher(name)
739 def handleCall(self, callNo, functionName, args, ret):
740 call = CallMatcher(callNo, functionName, args, ret)
741 self.calls.append(call)
744 class SrcTraceParser(TraceParser):
746 def __init__(self, stream):
747 TraceParser.__init__(self, stream)
751 TraceParser.parse(self)
754 def handleID(self, value):
757 def handleInt(self, value):
760 def handleFloat(self, value):
763 def handleString(self, value):
766 def handleBitmask(self, value):
769 def handleArray(self, elements):
770 return list(elements)
772 def handleStruct(self, members):
775 def handleCall(self, callNo, functionName, args, ret):
776 call = (callNo, functionName, args, ret)
777 self.calls.append(call)
781 # Parse command line options
782 optparser = optparse.OptionParser(
783 usage='\n\t%prog [OPTIONS] REF_TXT SRC_TRACE',
785 optparser.add_option(
786 '--apitrace', metavar='PROGRAM',
787 type='string', dest='apitrace', default=os.environ.get('APITRACE', 'apitrace'),
788 help='path to apitrace executable')
789 optparser.add_option(
792 dest="verbose", default=True,
793 help="verbose output")
794 (options, args) = optparser.parse_args(sys.argv[1:])
797 optparser.error('wrong number of arguments')
799 refFileName, srcFileName = args
801 refParser = RefTraceParser(refFileName)
802 refTrace = refParser.parse()
804 sys.stdout.write('// Reference\n')
805 sys.stdout.write(str(refTrace))
806 sys.stdout.write('\n')
808 if srcFileName.endswith('.trace'):
809 cmd = [options.apitrace, 'dump', '--verbose', '--color=never', srcFileName]
810 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True)
813 srcStream = open(srcFileName, 'rt')
814 srcParser = SrcTraceParser(srcStream)
815 srcTrace = srcParser.parse()
817 sys.stdout.write('// Source\n')
818 sys.stdout.write(''.join(['%s %s%r = %r\n' % call for call in srcTrace]))
819 sys.stdout.write('\n')
822 sys.stdout.write('// Matching\n')
823 mo = refTrace.match(srcTrace, options.verbose)
825 sys.stdout.write('\n')
828 sys.stdout.write('// Parameters\n')
829 paramNames = mo.params.keys()
831 for paramName in paramNames:
832 print '%s = %r' % (paramName, mo.params[paramName])
835 if __name__ == '__main__':