]> git.cworth.org Git - apitrace-tests/blob - app_driver.py
d3d11 test.
[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         'd3d11': 'd3d11',
183     }
184
185     api_retrace_map = {
186         'gl': 'glretrace',
187         'egl_gl': 'eglretrace',
188         'egl_gles1': 'eglretrace',
189         'egl_gles2': 'eglretrace',
190         'd3d9': 'd3dretrace',
191     }
192
193     def traceApp(self):
194         if not self.cmd:
195             return
196
197         if self.trace_file is None:
198             if self.ref_dump is not None:
199                 name = self.ref_dump
200             else:
201                 name = self.cmd[0]
202             name, ext = os.path.splitext(os.path.basename(name))
203             while ext:
204                 name, ext = os.path.splitext(os.path.basename(name))
205             self.trace_file = os.path.abspath(os.path.join(self.results, name + '.trace'))
206         if os.path.exists(self.trace_file):
207             os.remove(self.trace_file)
208         else:
209             trace_dir = os.path.dirname(self.trace_file)
210             if not os.path.exists(trace_dir):
211                 os.makedirs(trace_dir)
212
213         cmd = self.cmd
214         env = os.environ.copy()
215         
216         cmd = [
217             options.apitrace, 'trace', 
218             '--api', self.api_trace_map[self.api],
219             '--output', self.trace_file,
220             '--'
221         ] + cmd
222         if self.max_frames is not None:
223             env['TRACE_FRAMES'] = str(self.max_frames)
224
225         p = popen(cmd, env=env, cwd=self.cwd)
226         p.wait()
227
228         if not os.path.exists(self.trace_file):
229             fail('no trace file generated\n')
230     
231     def checkTrace(self):
232         cmd = [options.apitrace, 'dump', '--color=never', self.trace_file]
233         p = popen(cmd, stdout=subprocess.PIPE)
234
235         checker = TraceChecker(p.stdout, self.ref_dump, self.verbose)
236         checker.check()
237         p.wait()
238         if p.returncode != 0:
239             fail('`apitrace dump` returned code %i' % p.returncode)
240
241         self.doubleBuffer = checker.doubleBuffer
242
243         if self.api not in self.api_retrace_map:
244             return
245
246         for callNo, refImageFileName in checker.images:
247             self.checkImage(callNo, refImageFileName)
248         for callNo, refStateFileName in checker.states:
249             self.checkState(callNo, refStateFileName)
250
251     def checkImage(self, callNo, refImageFileName):
252         try:
253             from PIL import Image
254         except ImportError:
255             return
256
257         srcImage = self.getImage(callNo)
258         refImage = Image.open(refImageFileName)
259
260         from snapdiff import Comparer
261         comparer = Comparer(refImage, srcImage)
262         precision = comparer.precision(filter=True)
263         sys.stdout.write('precision of %f bits against %s\n' % (precision, refImageFileName))
264         if precision < self.threshold_precision:
265             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
266             srcImageFileName = prefix + '.src.png'
267             srcImage.save(srcImageFileName)
268             diffImageFileName = prefix + '.diff.png'
269             comparer.write_diff(diffImageFileName)
270             fail('snapshot from call %u does not match %s' % (callNo, refImageFileName))
271
272     def checkState(self, callNo, refStateFileName):
273         srcState = self.getState(callNo)
274         refState = self.getRefState(refStateFileName)
275
276         from jsondiff import Comparer, Differ
277         comparer = Comparer(ignore_added = True)
278         match = comparer.visit(refState, srcState)
279         if not match:
280             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
281             srcStateFileName = prefix + '.src.json'
282             diffStateFileName = prefix + '.diff.json'
283             self.saveState(srcState, srcStateFileName)
284             #diffStateFile = open(diffStateFileName, 'wt')
285             diffStateFile = sys.stdout
286             differ = Differ(diffStateFile, ignore_added = True)
287             differ.visit(refState, srcState)
288             fail('state from call %u does not match %s' % (callNo, refStateFileName))
289
290     def getRefState(self, refStateFileName):
291         stream = open(refStateFileName, 'rt')
292         from jsondiff import load
293         state = load(stream)
294         self.adjustRefState(state)
295         return state
296
297     def getNamePrefix(self):
298         name = os.path.basename(self.ref_dump)
299         try:
300             index = name.index('.')
301         except ValueError:
302             pass
303         else:
304             name = name[:index]
305         return name
306
307     def saveState(self, state, filename):
308         s = json.dumps(state, sort_keys=True, indent=2)
309         open(filename, 'wt').write(s)
310
311     def retrace(self):
312         if self.api not in self.api_retrace_map:
313             return
314
315         p = self._retrace()
316         p.wait()
317         if p.returncode != 0:
318             fail('retrace failed with code %i' % (p.returncode))
319
320     def getImage(self, callNo):
321         from PIL import Image
322         state = self.getState(callNo)
323         if self.doubleBuffer:
324             attachments = ['GL_BACK', 'GL_BACK_LEFT', 'GL_BACK_RIGHT', 'GL_COLOR_ATTACHMENT0']
325         else:
326             attachments = ['GL_FRONT', 'GL_FRONT_LEFT', 'GL_FRONT_RIGHT', 'GL_COLOR_ATTACHMENT0']
327         imageObj = self.getFramebufferAttachment(state, attachments)
328         data = imageObj['__data__']
329         stream = StringIO(base64.b64decode(data))
330         im = Image.open(stream)
331         im.save('test.png')
332         return im
333
334     def getFramebufferAttachment(self, state, attachments):
335         framebufferObj = state['framebuffer']
336         for attachment in attachments:
337             try:
338                 attachmentObj = framebufferObj[attachment]
339             except KeyError:
340                 pass
341             else:
342                 return attachmentObj
343         raise Exception("no attachment found")
344
345     def getState(self, callNo):
346         try:
347             state = self.stateCache[callNo]
348         except KeyError:
349             pass
350         else:
351             return state
352
353         p = self._retrace(['-D', str(callNo)])
354         state = json.load(p.stdout, strict=False)
355         p.wait()
356         if p.returncode != 0:
357             fail('retrace returned code %i' % (p.returncode))
358
359         self.adjustSrcState(state)
360
361         self.stateCache[callNo] = state
362
363         return state
364
365     def adjustSrcState(self, state):
366         # Do some adjustments on the obtained state to eliminate failures from
367         # bugs/issues outside of apitrace
368
369         try:
370             parameters = state['parameters']
371         except KeyError:
372             return
373
374         # On NVIDIA drivers glGetIntegerv(GL_INDEX_WRITEMASK) returns -1
375         self.replaceState(parameters, 'GL_INDEX_WRITEMASK', 255, -1)
376
377         # On Gallium 
378         if 'Gallium' in parameters['GL_RENDERER'].split():
379             # Gallium drivers have wrong defaults for draw/read buffer state
380             self.replaceState(parameters, 'GL_DRAW_BUFFER', 'GL_BACK_LEFT', 'GL_BACK')
381             self.replaceState(parameters, 'GL_DRAW_BUFFER0', 'GL_BACK_LEFT', 'GL_BACK')
382             self.replaceState(parameters, 'GL_READ_BUFFER', 'GL_BACK_LEFT', 'GL_BACK')
383             self.replaceState(parameters, 'GL_DRAW_BUFFER', 'GL_FRONT_LEFT', 'GL_FRONT')
384             self.replaceState(parameters, 'GL_DRAW_BUFFER0', 'GL_FRONT_LEFT', 'GL_FRONT')
385             self.replaceState(parameters, 'GL_READ_BUFFER', 'GL_FRONT_LEFT', 'GL_FRONT')
386
387     def adjustRefState(self, state):
388         # Do some adjustments on reference state to eliminate failures from
389         # bugs/issues outside of apitrace
390
391         try:
392             parameters = state['parameters']
393         except KeyError:
394             return
395
396         if platform.system() == 'Darwin':
397             # Mac OS X drivers fail on GL_COLOR_SUM
398             # XXX: investigate this
399             self.removeState(parameters, 'GL_COLOR_SUM')
400
401     def replaceState(self, obj, key, srcValue, dstValue):
402         try:
403             value = obj[key]
404         except KeyError:
405             pass
406         else:
407             if value == srcValue:
408                 obj[key] = dstValue
409
410     def removeState(self, obj, key):
411         try:
412             del obj[key]
413         except KeyError:
414             pass
415
416     def _retrace(self, args = None, stdout=subprocess.PIPE):
417         retrace = self.api_retrace_map[self.api]
418         cmd = [get_build_program(retrace)]
419         if self.doubleBuffer:
420             cmd += ['-db']
421         else:
422             cmd += ['-sb']
423         if args:
424             cmd += args
425         cmd += [self.trace_file]
426         return popen(cmd, stdout=stdout)
427
428     def createOptParser(self):
429         optparser = Driver.createOptParser(self)
430
431         optparser.add_option(
432             '-a', '--api', metavar='API',
433             type='string', dest='api', default='gl',
434             help='api to trace')
435         optparser.add_option(
436             '-R', '--results', metavar='PATH',
437             type='string', dest='results', default='.',
438             help='results directory [default=%default]')
439         optparser.add_option(
440             '--ref-dump', metavar='PATH',
441             type='string', dest='ref_dump', default=None,
442             help='reference dump')
443
444         return optparser
445
446     def run(self):
447         global options
448
449         (options, args) = self.parseOptions()
450
451         if not os.path.exists(options.results):
452             os.makedirs(options.results)
453
454         self.verbose = options.verbose
455
456         self.cmd = args
457         self.cwd = options.cwd
458         self.api = options.api
459         self.ref_dump = options.ref_dump
460         self.results = options.results
461
462         self.runApp()
463         self.traceApp()
464         self.checkTrace()
465         self.retrace()
466
467         pass_()
468
469 if __name__ == '__main__':
470     AppDriver().run()