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