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