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