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