]> git.cworth.org Git - apitrace-tests/blob - app_driver.py
Remove left over debug statement.
[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 import tracematch
46 from base_driver import *
47
48
49 class SrcTraceParser(tracematch.SrcTraceParser):
50
51     def __init__(self, stream):
52         tracematch.SrcTraceParser.__init__(self, stream)
53         self.swapbuffers = 0
54
55     def handleCall(self, callNo, functionName, args, ret):
56         tracematch.SrcTraceParser.handleCall(self, callNo, functionName, args, ret)
57
58         if functionName.find('SwapBuffers') != -1 or \
59            repr(args).find('kCGLPFADoubleBuffer') != -1:
60             self.swapbuffers += 1
61
62
63 class AppDriver(Driver):
64
65     cmd = None
66     cwd = None
67
68     api = 'gl'
69     max_frames = None
70     trace_file = None
71
72     ref_dump = None
73
74     doubleBuffer = True
75
76     verbose = False
77
78     threshold_precision = 12.0
79
80     def __init__(self):
81         Driver.__init__(self)
82         self.stateCache = {}
83     
84     def runApp(self):
85         '''Run the application standalone, skipping this test if it fails by
86         some reason.'''
87
88         if not self.cmd:
89             return
90
91         sys.stderr.write('Run application...\n')
92
93         p = popen(self.cmd, cwd=self.cwd)
94         p.wait()
95         if p.returncode != 0:
96             skip('application returned code %i' % p.returncode)
97
98         sys.stdout.flush()
99         sys.stderr.write('\n')
100
101     api_trace_map = {
102         'gl': 'gl',
103         'egl_gl': 'egl',
104         'egl_gles1': 'egl',
105         'egl_gles2': 'egl',
106         'd3d7': 'd3d7',
107         'd3d8': 'd3d8',
108         'd3d9': 'd3d9',
109         'd3d10': 'd3d10',
110         'd3d10_1': 'd3d10_1',
111         'd3d11': 'd3d11',
112         'd3d11_1': 'd3d11',
113     }
114
115     api_retrace_map = {
116         'gl': 'glretrace',
117         'egl_gl': 'eglretrace',
118         'egl_gles1': 'eglretrace',
119         'egl_gles2': 'eglretrace',
120         'd3d8': 'd3dretrace',
121         'd3d9': 'd3dretrace',
122         'd3d10': 'd3dretrace',
123         'd3d10_1': 'd3dretrace',
124         'd3d11': 'd3dretrace',
125         'd3d11_1': 'd3dretrace',
126     }
127
128     def traceApp(self):
129         if not self.cmd:
130             return
131
132         sys.stderr.write('Capturing trace...\n')
133
134         if self.trace_file is None:
135             if self.ref_dump is not None:
136                 name = self.ref_dump
137             else:
138                 name = self.cmd[0]
139             name, ext = os.path.splitext(os.path.basename(name))
140             while ext:
141                 name, ext = os.path.splitext(os.path.basename(name))
142             self.trace_file = os.path.abspath(os.path.join(self.results, name + '.trace'))
143         if os.path.exists(self.trace_file):
144             os.remove(self.trace_file)
145         else:
146             trace_dir = os.path.dirname(self.trace_file)
147             if not os.path.exists(trace_dir):
148                 os.makedirs(trace_dir)
149
150         cmd = self.cmd
151         env = os.environ.copy()
152         
153         cmd = [
154             options.apitrace, 'trace', 
155             '--api', self.api_trace_map[self.api],
156             '--output', self.trace_file,
157             '--'
158         ] + cmd
159         if self.max_frames is not None:
160             env['TRACE_FRAMES'] = str(self.max_frames)
161
162         p = popen(cmd, env=env, cwd=self.cwd)
163         p.wait()
164         if p.returncode != 0:
165             fail('`apitrace trace` returned code %i' % p.returncode)
166
167         if not os.path.exists(self.trace_file):
168             fail('no trace file generated\n')
169
170         sys.stdout.flush()
171         sys.stderr.write('\n')
172     
173     def checkTrace(self):
174         sys.stderr.write('Comparing trace %s against %s...\n' % (self.trace_file, self.ref_dump))
175
176         cmd = [options.apitrace, 'dump', '--verbose', '--color=never', self.trace_file]
177         p = popen(cmd, stdout=subprocess.PIPE, universal_newlines=True)
178
179         srcParser = SrcTraceParser(p.stdout)
180         srcTrace = srcParser.parse()
181         self.doubleBuffer = srcParser.swapbuffers > 0
182
183         images = []
184         states = []
185
186         if self.ref_dump:
187             refParser = tracematch.RefTraceParser(self.ref_dump)
188             refTrace = refParser.parse()
189
190             try:
191                 mo = refTrace.match(srcTrace)
192             except tracematch.TraceMismatch, ex:
193                 fail(str(ex))
194
195             dirName, baseName = os.path.split(os.path.abspath(self.ref_dump))
196             prefix, _ = os.path.splitext(baseName)
197             prefix += '.'
198             fileNames = os.listdir(dirName)
199             for fileName in fileNames:
200                 if fileName.startswith(prefix) and fileName != self.ref_dump:
201                     rest = fileName[len(prefix):]
202                     paramName, ext = os.path.splitext(rest)
203                     if ext in ('.json', '.png'):
204                         if paramName.isdigit():
205                             callNo = int(paramName)
206                         else:
207                             try:
208                                 callNo = mo.params[paramName]
209                             except KeyError:
210                                 fail('could not find parameter %s for %s' % (paramName, fileName))
211                         filePath = os.path.join(dirName, fileName)
212                         if ext == '.png':
213                             images.append((callNo, filePath))
214                         if ext == '.json':
215                             states.append((callNo, filePath))
216         p.wait()
217         if p.returncode != 0:
218             fail('`apitrace dump` returned code %i' % p.returncode)
219
220         sys.stdout.flush()
221         sys.stderr.write('\n')
222
223         if self.api not in self.api_retrace_map:
224             return
225
226         for callNo, refImageFileName in images:
227             self.checkImage(callNo, refImageFileName)
228         for callNo, refStateFileName in states:
229             self.checkState(callNo, refStateFileName)
230
231     def checkImage(self, callNo, refImageFileName):
232         sys.stderr.write('Comparing snapshot from call %u against %s...\n' % (callNo, refImageFileName))
233         try:
234             from PIL import Image
235         except ImportError:
236             sys.stderr.write('warning: PIL not found, skipping image comparison\n');
237             return
238
239         srcImage = self.getImage(callNo)
240         refImage = Image.open(refImageFileName)
241
242         from snapdiff import Comparer
243         comparer = Comparer(refImage, srcImage)
244         precision = comparer.precision(filter=True)
245         sys.stdout.write('precision of %f bits against %s\n' % (precision, refImageFileName))
246         if precision < self.threshold_precision:
247             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
248             srcImageFileName = prefix + '.src.png'
249             srcImage.save(srcImageFileName)
250             diffImageFileName = prefix + '.diff.png'
251             comparer.write_diff(diffImageFileName)
252             fail('snapshot from call %u does not match %s' % (callNo, refImageFileName))
253
254         sys.stdout.flush()
255         sys.stderr.write('\n')
256
257     def checkState(self, callNo, refStateFileName):
258         sys.stderr.write('Comparing state dump from call %u against %s...\n' % (callNo, refStateFileName))
259
260         srcState = self.getState(callNo)
261         refState = self.getRefState(refStateFileName)
262
263         from jsondiff import Comparer, Differ
264         comparer = Comparer(ignore_added = True)
265         match = comparer.visit(refState, srcState)
266         if not match:
267             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
268             srcStateFileName = prefix + '.src.json'
269             diffStateFileName = prefix + '.diff.json'
270             self.saveState(srcState, srcStateFileName)
271             #diffStateFile = open(diffStateFileName, 'wt')
272             diffStateFile = sys.stdout
273             differ = Differ(diffStateFile, ignore_added = True)
274             differ.visit(refState, srcState)
275             fail('state from call %u does not match %s' % (callNo, refStateFileName))
276
277         sys.stdout.flush()
278         sys.stderr.write('\n')
279
280     def getRefState(self, refStateFileName):
281         stream = open(refStateFileName, 'rt')
282         from jsondiff import load
283         state = load(stream)
284         self.adjustRefState(state)
285         return state
286
287     def getNamePrefix(self):
288         name = os.path.basename(self.ref_dump)
289         try:
290             index = name.index('.')
291         except ValueError:
292             pass
293         else:
294             name = name[:index]
295         return name
296
297     def saveState(self, state, filename):
298         s = json.dumps(state, sort_keys=True, indent=2)
299         open(filename, 'wt').write(s)
300
301     def retrace(self):
302         if self.api not in self.api_retrace_map:
303             return
304
305         sys.stderr.write('Retracing %s...\n' % (self.trace_file,))
306
307         p = self._retrace()
308         p.wait()
309         if p.returncode != 0:
310             fail('retrace failed with code %i' % (p.returncode))
311
312         sys.stdout.flush()
313         sys.stderr.write('\n')
314
315     def getImage(self, callNo):
316         from PIL import Image
317         state = self.getState(callNo)
318         if self.doubleBuffer:
319             attachments = ['GL_BACK', 'GL_BACK_LEFT', 'GL_BACK_RIGHT', 'GL_COLOR_ATTACHMENT0']
320         else:
321             attachments = ['GL_FRONT', 'GL_FRONT_LEFT', 'GL_FRONT_RIGHT', 'GL_COLOR_ATTACHMENT0']
322         imageObj = self.getFramebufferAttachment(state, attachments)
323         data = imageObj['__data__']
324         stream = StringIO(base64.b64decode(data))
325         im = Image.open(stream)
326         return im
327
328     def getFramebufferAttachment(self, state, attachments):
329         framebufferObj = state['framebuffer']
330         for attachment in attachments:
331             try:
332                 attachmentObj = framebufferObj[attachment]
333             except KeyError:
334                 pass
335             else:
336                 return attachmentObj
337         raise Exception("no attachment found")
338
339     def getState(self, callNo):
340         try:
341             state = self.stateCache[callNo]
342         except KeyError:
343             pass
344         else:
345             return state
346
347         p = self._retrace(['-D', str(callNo)])
348         state = json.load(p.stdout, strict=False)
349         p.wait()
350         if p.returncode != 0:
351             fail('retrace returned code %i' % (p.returncode))
352
353         self.adjustSrcState(state)
354
355         self.stateCache[callNo] = state
356
357         return state
358
359     def adjustSrcState(self, state):
360         # Do some adjustments on the obtained state to eliminate failures from
361         # bugs/issues outside of apitrace
362
363         try:
364             parameters = state['parameters']
365         except KeyError:
366             return
367
368         # On NVIDIA drivers glGetIntegerv(GL_INDEX_WRITEMASK) returns 255
369         self.replaceState(parameters, 'GL_INDEX_WRITEMASK', 255, -1)
370
371     def adjustRefState(self, state):
372         # Do some adjustments on reference state to eliminate failures from
373         # bugs/issues outside of apitrace
374
375         try:
376             parameters = state['parameters']
377         except KeyError:
378             return
379
380         if platform.system() == 'Darwin':
381             # Mac OS X drivers fail on GL_COLOR_SUM
382             # XXX: investigate this
383             self.removeState(parameters, 'GL_COLOR_SUM')
384
385     def replaceState(self, obj, key, srcValue, dstValue):
386         try:
387             value = obj[key]
388         except KeyError:
389             pass
390         else:
391             if value == srcValue:
392                 obj[key] = dstValue
393
394     def removeState(self, obj, key):
395         try:
396             del obj[key]
397         except KeyError:
398             pass
399
400     def _retrace(self, args = None, stdout=subprocess.PIPE):
401         retrace = self.api_retrace_map[self.api]
402         #cmd = [get_build_program(retrace)]
403         cmd = [options.apitrace, 'retrace']
404         if self.doubleBuffer:
405             cmd += ['-db']
406         else:
407             cmd += ['-sb']
408         if args:
409             cmd += args
410         cmd += [self.trace_file]
411         return popen(cmd, stdout=stdout)
412
413     def createOptParser(self):
414         optparser = Driver.createOptParser(self)
415
416         optparser.add_option(
417             '-a', '--api', metavar='API',
418             type='string', dest='api', default='gl',
419             help='api to trace')
420         optparser.add_option(
421             '-R', '--results', metavar='PATH',
422             type='string', dest='results', default='.',
423             help='results directory [default=%default]')
424         optparser.add_option(
425             '--ref-dump', metavar='PATH',
426             type='string', dest='ref_dump', default=None,
427             help='reference dump')
428
429         return optparser
430
431     def run(self):
432         global options
433
434         (options, args) = self.parseOptions()
435
436         if not os.path.exists(options.results):
437             os.makedirs(options.results)
438
439         self.verbose = options.verbose
440
441         self.cmd = args
442         self.cwd = options.cwd
443         self.api = options.api
444         self.ref_dump = options.ref_dump
445         self.results = options.results
446
447         self.runApp()
448         self.traceApp()
449         self.checkTrace()
450         self.retrace()
451
452         pass_()
453
454 if __name__ == '__main__':
455     AppDriver().run()