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