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