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