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