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