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