]> git.cworth.org Git - apitrace-tests/blob - app_driver.py
Rename checker.py -> tracematch.py
[apitrace-tests] / app_driver.py
1 #!/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 '''Application test driver.'''
28
29
30 import os.path
31 import platform
32 import re
33 import subprocess
34 import sys
35 import time
36 import json
37 import base64
38
39 try:
40     from cStringIO import StringIO
41 except ImportError:
42     from StringIO import StringIO
43
44
45 import tracematch
46 from base_driver import *
47
48
49 class RefTraceParser(tracematch.RefTraceParser):
50
51     def __init__(self, fileName):
52         tracematch.RefTraceParser.__init__(self, open(fileName, 'rt'))
53         self.fileName = fileName
54         self.images = []
55         self.states = []
56         self.pragmaNo = 0
57
58     def handlePragma(self, line):
59         if self.calls:
60             lastCall = self.calls[-1]
61             if lastCall.callNo is None:
62                 paramName = 'pragma%u' % self.pragmaNo
63                 lastCall.callNo = tracematch.WildcardMatcher(paramName)
64             else:
65                 paramName = lastCall.callNo.name
66         else:
67             paramName = 0
68             self.pragmaNo += 1
69
70         pragma, rest = line.split(None, 1)
71         if pragma == '#image':
72             imageFileName = self.getAbsPath(rest)
73             self.images.append((paramName, imageFileName))
74         elif pragma == '#state':
75             stateFileName = self.getAbsPath(rest)
76             self.states.append((paramName, stateFileName))
77         else:
78             assert False
79
80     def getAbsPath(self, path):
81         '''Get the absolute from a path relative to the reference filename'''
82         return os.path.abspath(os.path.join(os.path.dirname(self.fileName), path))
83
84
85 class SrcTraceParser(tracematch.SrcTraceParser):
86
87     def __init__(self, stream):
88         tracematch.SrcTraceParser.__init__(self, stream)
89         self.swapbuffers = 0
90
91     def handleCall(self, callNo, functionName, args, ret):
92         tracematch.SrcTraceParser.handleCall(self, callNo, functionName, args, ret)
93
94         if functionName.find('SwapBuffers') != -1 or \
95            repr(args).find('kCGLPFADoubleBuffer') != -1:
96             self.swapbuffers += 1
97
98
99 class TraceChecker:
100
101     def __init__(self, srcStream, refFileName):
102         self.srcStream = srcStream
103         self.refFileName = refFileName
104         self.doubleBuffer = False
105         self.callNo = 0
106         self.images = []
107         self.states = []
108
109     def check(self):
110         srcParser = SrcTraceParser(self.srcStream)
111         srcTrace = srcParser.parse()
112         self.doubleBuffer = srcParser.swapbuffers > 0
113
114         if self.refFileName:
115             refParser = RefTraceParser(self.refFileName)
116             refTrace = refParser.parse()
117
118             try:
119                 mo = refTrace.match(srcTrace)
120             except tracematch.TraceMismatch, ex:
121                 self.fail(str(ex))
122
123             for paramName, imageFileName in refParser.images:
124                 if isinstance(paramName, int):
125                     callNo = paramName
126                 else:
127                     callNo = mo.params[paramName]
128                 self.images.append((callNo, imageFileName))
129             for paramName, stateFileName in refParser.states:
130                 if isinstance(paramName, int):
131                     callNo = paramName
132                 else:
133                     callNo = mo.params[paramName]
134                 self.states.append((callNo, stateFileName))
135
136
137 class AppDriver(Driver):
138
139     cmd = None
140     cwd = None
141
142     api = 'gl'
143     max_frames = None
144     trace_file = None
145
146     ref_dump = None
147
148     doubleBuffer = True
149
150     verbose = False
151
152     threshold_precision = 12.0
153
154     def __init__(self):
155         Driver.__init__(self)
156         self.stateCache = {}
157     
158     def runApp(self):
159         '''Run the application standalone, skipping this test if it fails by
160         some reason.'''
161
162         if not self.cmd:
163             return
164
165         p = popen(self.cmd, cwd=self.cwd)
166         p.wait()
167         if p.returncode != 0:
168             skip('application returned code %i' % p.returncode)
169
170     api_trace_map = {
171         'gl': 'gl',
172         'egl_gl': 'egl',
173         'egl_gles1': 'egl',
174         'egl_gles2': 'egl',
175         'd3d7': 'd3d7',
176         'd3d8': 'd3d8',
177         'd3d9': 'd3d9',
178         'd3d10': 'd3d10',
179         'd3d10_1': 'd3d10_1',
180         'd3d11': 'd3d11',
181         'd3d11_1': 'd3d11',
182     }
183
184     api_retrace_map = {
185         'gl': 'glretrace',
186         'egl_gl': 'eglretrace',
187         'egl_gles1': 'eglretrace',
188         'egl_gles2': 'eglretrace',
189         #'d3d8': 'd3dretrace',
190         'd3d9': 'd3dretrace',
191         'd3d10': 'd3dretrace',
192         'd3d10_1': 'd3dretrace',
193         'd3d11': 'd3dretrace',
194         'd3d11_1': 'd3dretrace',
195     }
196
197     def traceApp(self):
198         if not self.cmd:
199             return
200
201         if self.trace_file is None:
202             if self.ref_dump is not None:
203                 name = self.ref_dump
204             else:
205                 name = self.cmd[0]
206             name, ext = os.path.splitext(os.path.basename(name))
207             while ext:
208                 name, ext = os.path.splitext(os.path.basename(name))
209             self.trace_file = os.path.abspath(os.path.join(self.results, name + '.trace'))
210         if os.path.exists(self.trace_file):
211             os.remove(self.trace_file)
212         else:
213             trace_dir = os.path.dirname(self.trace_file)
214             if not os.path.exists(trace_dir):
215                 os.makedirs(trace_dir)
216
217         cmd = self.cmd
218         env = os.environ.copy()
219         
220         cmd = [
221             options.apitrace, 'trace', 
222             '--api', self.api_trace_map[self.api],
223             '--output', self.trace_file,
224             '--'
225         ] + cmd
226         if self.max_frames is not None:
227             env['TRACE_FRAMES'] = str(self.max_frames)
228
229         p = popen(cmd, env=env, cwd=self.cwd)
230         p.wait()
231         if p.returncode != 0:
232             fail('`apitrace trace` returned code %i' % p.returncode)
233
234         if not os.path.exists(self.trace_file):
235             fail('no trace file generated\n')
236     
237     def checkTrace(self):
238         cmd = [options.apitrace, 'dump', '--color=never', self.trace_file]
239         p = popen(cmd, stdout=subprocess.PIPE)
240
241         checker = TraceChecker(p.stdout, self.ref_dump)
242         checker.check()
243         p.wait()
244         if p.returncode != 0:
245             fail('`apitrace dump` returned code %i' % p.returncode)
246
247         self.doubleBuffer = checker.doubleBuffer
248
249         if self.api not in self.api_retrace_map:
250             return
251
252         for callNo, refImageFileName in checker.images:
253             self.checkImage(callNo, refImageFileName)
254         for callNo, refStateFileName in checker.states:
255             self.checkState(callNo, refStateFileName)
256
257     def checkImage(self, callNo, refImageFileName):
258         try:
259             from PIL import Image
260         except ImportError:
261             return
262
263         srcImage = self.getImage(callNo)
264         refImage = Image.open(refImageFileName)
265
266         from snapdiff import Comparer
267         comparer = Comparer(refImage, srcImage)
268         precision = comparer.precision(filter=True)
269         sys.stdout.write('precision of %f bits against %s\n' % (precision, refImageFileName))
270         if precision < self.threshold_precision:
271             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
272             srcImageFileName = prefix + '.src.png'
273             srcImage.save(srcImageFileName)
274             diffImageFileName = prefix + '.diff.png'
275             comparer.write_diff(diffImageFileName)
276             fail('snapshot from call %u does not match %s' % (callNo, refImageFileName))
277
278     def checkState(self, callNo, refStateFileName):
279         srcState = self.getState(callNo)
280         refState = self.getRefState(refStateFileName)
281
282         from jsondiff import Comparer, Differ
283         comparer = Comparer(ignore_added = True)
284         match = comparer.visit(refState, srcState)
285         if not match:
286             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
287             srcStateFileName = prefix + '.src.json'
288             diffStateFileName = prefix + '.diff.json'
289             self.saveState(srcState, srcStateFileName)
290             #diffStateFile = open(diffStateFileName, 'wt')
291             diffStateFile = sys.stdout
292             differ = Differ(diffStateFile, ignore_added = True)
293             differ.visit(refState, srcState)
294             fail('state from call %u does not match %s' % (callNo, refStateFileName))
295
296     def getRefState(self, refStateFileName):
297         stream = open(refStateFileName, 'rt')
298         from jsondiff import load
299         state = load(stream)
300         self.adjustRefState(state)
301         return state
302
303     def getNamePrefix(self):
304         name = os.path.basename(self.ref_dump)
305         try:
306             index = name.index('.')
307         except ValueError:
308             pass
309         else:
310             name = name[:index]
311         return name
312
313     def saveState(self, state, filename):
314         s = json.dumps(state, sort_keys=True, indent=2)
315         open(filename, 'wt').write(s)
316
317     def retrace(self):
318         if self.api not in self.api_retrace_map:
319             return
320
321         p = self._retrace()
322         p.wait()
323         if p.returncode != 0:
324             fail('retrace failed with code %i' % (p.returncode))
325
326     def getImage(self, callNo):
327         from PIL import Image
328         state = self.getState(callNo)
329         if self.doubleBuffer:
330             attachments = ['GL_BACK', 'GL_BACK_LEFT', 'GL_BACK_RIGHT', 'GL_COLOR_ATTACHMENT0']
331         else:
332             attachments = ['GL_FRONT', 'GL_FRONT_LEFT', 'GL_FRONT_RIGHT', 'GL_COLOR_ATTACHMENT0']
333         imageObj = self.getFramebufferAttachment(state, attachments)
334         data = imageObj['__data__']
335         stream = StringIO(base64.b64decode(data))
336         im = Image.open(stream)
337         im.save('test.png')
338         return im
339
340     def getFramebufferAttachment(self, state, attachments):
341         framebufferObj = state['framebuffer']
342         for attachment in attachments:
343             try:
344                 attachmentObj = framebufferObj[attachment]
345             except KeyError:
346                 pass
347             else:
348                 return attachmentObj
349         raise Exception("no attachment found")
350
351     def getState(self, callNo):
352         try:
353             state = self.stateCache[callNo]
354         except KeyError:
355             pass
356         else:
357             return state
358
359         p = self._retrace(['-D', str(callNo)])
360         state = json.load(p.stdout, strict=False)
361         p.wait()
362         if p.returncode != 0:
363             fail('retrace returned code %i' % (p.returncode))
364
365         self.adjustSrcState(state)
366
367         self.stateCache[callNo] = state
368
369         return state
370
371     def adjustSrcState(self, state):
372         # Do some adjustments on the obtained state to eliminate failures from
373         # bugs/issues outside of apitrace
374
375         try:
376             parameters = state['parameters']
377         except KeyError:
378             return
379
380         # On NVIDIA drivers glGetIntegerv(GL_INDEX_WRITEMASK) returns 255
381         self.replaceState(parameters, 'GL_INDEX_WRITEMASK', 255, -1)
382
383     def adjustRefState(self, state):
384         # Do some adjustments on reference state to eliminate failures from
385         # bugs/issues outside of apitrace
386
387         try:
388             parameters = state['parameters']
389         except KeyError:
390             return
391
392         if platform.system() == 'Darwin':
393             # Mac OS X drivers fail on GL_COLOR_SUM
394             # XXX: investigate this
395             self.removeState(parameters, 'GL_COLOR_SUM')
396
397     def replaceState(self, obj, key, srcValue, dstValue):
398         try:
399             value = obj[key]
400         except KeyError:
401             pass
402         else:
403             if value == srcValue:
404                 obj[key] = dstValue
405
406     def removeState(self, obj, key):
407         try:
408             del obj[key]
409         except KeyError:
410             pass
411
412     def _retrace(self, args = None, stdout=subprocess.PIPE):
413         retrace = self.api_retrace_map[self.api]
414         #cmd = [get_build_program(retrace)]
415         cmd = [options.apitrace, 'retrace']
416         if self.doubleBuffer:
417             cmd += ['-db']
418         else:
419             cmd += ['-sb']
420         if args:
421             cmd += args
422         cmd += [self.trace_file]
423         return popen(cmd, stdout=stdout)
424
425     def createOptParser(self):
426         optparser = Driver.createOptParser(self)
427
428         optparser.add_option(
429             '-a', '--api', metavar='API',
430             type='string', dest='api', default='gl',
431             help='api to trace')
432         optparser.add_option(
433             '-R', '--results', metavar='PATH',
434             type='string', dest='results', default='.',
435             help='results directory [default=%default]')
436         optparser.add_option(
437             '--ref-dump', metavar='PATH',
438             type='string', dest='ref_dump', default=None,
439             help='reference dump')
440
441         return optparser
442
443     def run(self):
444         global options
445
446         (options, args) = self.parseOptions()
447
448         if not os.path.exists(options.results):
449             os.makedirs(options.results)
450
451         self.verbose = options.verbose
452
453         self.cmd = args
454         self.cwd = options.cwd
455         self.api = options.api
456         self.ref_dump = options.ref_dump
457         self.results = options.results
458
459         self.runApp()
460         self.traceApp()
461         self.checkTrace()
462         self.retrace()
463
464         pass_()
465
466 if __name__ == '__main__':
467     AppDriver().run()