]> git.cworth.org Git - apitrace-tests/blob - app_driver.py
a7f83e89845714a09f7ea49dcc886095154587ab
[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', '--color=never', self.trace_file]
177         p = popen(cmd, stdout=subprocess.PIPE)
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         im.save('test.png')
327         return im
328
329     def getFramebufferAttachment(self, state, attachments):
330         framebufferObj = state['framebuffer']
331         for attachment in attachments:
332             try:
333                 attachmentObj = framebufferObj[attachment]
334             except KeyError:
335                 pass
336             else:
337                 return attachmentObj
338         raise Exception("no attachment found")
339
340     def getState(self, callNo):
341         try:
342             state = self.stateCache[callNo]
343         except KeyError:
344             pass
345         else:
346             return state
347
348         p = self._retrace(['-D', str(callNo)])
349         state = json.load(p.stdout, strict=False)
350         p.wait()
351         if p.returncode != 0:
352             fail('retrace returned code %i' % (p.returncode))
353
354         self.adjustSrcState(state)
355
356         self.stateCache[callNo] = state
357
358         return state
359
360     def adjustSrcState(self, state):
361         # Do some adjustments on the obtained state to eliminate failures from
362         # bugs/issues outside of apitrace
363
364         try:
365             parameters = state['parameters']
366         except KeyError:
367             return
368
369         # On NVIDIA drivers glGetIntegerv(GL_INDEX_WRITEMASK) returns 255
370         self.replaceState(parameters, 'GL_INDEX_WRITEMASK', 255, -1)
371
372     def adjustRefState(self, state):
373         # Do some adjustments on reference state to eliminate failures from
374         # bugs/issues outside of apitrace
375
376         try:
377             parameters = state['parameters']
378         except KeyError:
379             return
380
381         if platform.system() == 'Darwin':
382             # Mac OS X drivers fail on GL_COLOR_SUM
383             # XXX: investigate this
384             self.removeState(parameters, 'GL_COLOR_SUM')
385
386     def replaceState(self, obj, key, srcValue, dstValue):
387         try:
388             value = obj[key]
389         except KeyError:
390             pass
391         else:
392             if value == srcValue:
393                 obj[key] = dstValue
394
395     def removeState(self, obj, key):
396         try:
397             del obj[key]
398         except KeyError:
399             pass
400
401     def _retrace(self, args = None, stdout=subprocess.PIPE):
402         retrace = self.api_retrace_map[self.api]
403         #cmd = [get_build_program(retrace)]
404         cmd = [options.apitrace, 'retrace']
405         if self.doubleBuffer:
406             cmd += ['-db']
407         else:
408             cmd += ['-sb']
409         if args:
410             cmd += args
411         cmd += [self.trace_file]
412         return popen(cmd, stdout=stdout)
413
414     def createOptParser(self):
415         optparser = Driver.createOptParser(self)
416
417         optparser.add_option(
418             '-a', '--api', metavar='API',
419             type='string', dest='api', default='gl',
420             help='api to trace')
421         optparser.add_option(
422             '-R', '--results', metavar='PATH',
423             type='string', dest='results', default='.',
424             help='results directory [default=%default]')
425         optparser.add_option(
426             '--ref-dump', metavar='PATH',
427             type='string', dest='ref_dump', default=None,
428             help='reference dump')
429
430         return optparser
431
432     def run(self):
433         global options
434
435         (options, args) = self.parseOptions()
436
437         if not os.path.exists(options.results):
438             os.makedirs(options.results)
439
440         self.verbose = options.verbose
441
442         self.cmd = args
443         self.cwd = options.cwd
444         self.api = options.api
445         self.ref_dump = options.ref_dump
446         self.results = options.results
447
448         self.runApp()
449         self.traceApp()
450         self.checkTrace()
451         self.retrace()
452
453         pass_()
454
455 if __name__ == '__main__':
456     AppDriver().run()