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