0e6cc37c65c5f68ab82b82b4e16cc70f358ebe6e
[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             '-v',
156             '-a', self.api_trace_map[self.api],
157             '-o', self.trace_file,
158             '--'
159         ] + cmd
160         if self.max_frames is not None:
161             env['TRACE_FRAMES'] = str(self.max_frames)
162
163         p = popen(cmd, env=env, cwd=self.cwd)
164         p.wait()
165         if p.returncode != 0:
166             fail('`apitrace trace` returned code %i' % p.returncode)
167
168         if not os.path.exists(self.trace_file):
169             fail('no trace file generated\n')
170
171         sys.stdout.flush()
172         sys.stderr.write('\n')
173     
174     def checkTrace(self):
175         sys.stderr.write('Comparing trace %s against %s...\n' % (self.trace_file, self.ref_dump))
176
177         cmd = [options.apitrace, 'dump', '--verbose', '--color=never', self.trace_file]
178         p = popen(cmd, stdout=subprocess.PIPE, universal_newlines=True)
179
180         srcParser = SrcTraceParser(p.stdout)
181         srcTrace = srcParser.parse()
182         self.doubleBuffer = srcParser.swapbuffers > 0
183
184         images = []
185         states = []
186
187         if self.ref_dump:
188             refParser = tracematch.RefTraceParser(self.ref_dump)
189             refTrace = refParser.parse()
190
191             try:
192                 mo = refTrace.match(srcTrace)
193             except tracematch.TraceMismatch, ex:
194                 fail(str(ex))
195
196             dirName, baseName = os.path.split(os.path.abspath(self.ref_dump))
197             prefix, _ = os.path.splitext(baseName)
198             prefix += '.'
199             fileNames = os.listdir(dirName)
200             for fileName in fileNames:
201                 if fileName.startswith(prefix) and fileName != self.ref_dump:
202                     rest = fileName[len(prefix):]
203                     paramName, ext = os.path.splitext(rest)
204                     if ext in ('.json', '.png'):
205                         if paramName.isdigit():
206                             callNo = int(paramName)
207                         else:
208                             try:
209                                 callNo = mo.params[paramName]
210                             except KeyError:
211                                 fail('could not find parameter %s for %s' % (paramName, fileName))
212                         filePath = os.path.join(dirName, fileName)
213                         if ext == '.png':
214                             images.append((callNo, filePath))
215                         if ext == '.json':
216                             states.append((callNo, filePath))
217         p.wait()
218         if p.returncode != 0:
219             fail('`apitrace dump` returned code %i' % p.returncode)
220
221         sys.stdout.flush()
222         sys.stderr.write('\n')
223
224         if self.api not in self.api_retrace_map:
225             return
226
227         for callNo, refImageFileName in images:
228             self.checkImage(callNo, refImageFileName)
229         for callNo, refStateFileName in states:
230             self.checkState(callNo, refStateFileName)
231
232     def checkImage(self, callNo, refImageFileName):
233         sys.stderr.write('Comparing snapshot from call %u against %s...\n' % (callNo, refImageFileName))
234         try:
235             from PIL import Image
236         except ImportError:
237             sys.stderr.write('warning: PIL not found, skipping image comparison\n');
238             return
239
240         srcImage = self.getImage(callNo)
241         refImage = Image.open(refImageFileName)
242
243         from snapdiff import Comparer
244         comparer = Comparer(refImage, srcImage)
245         precision = comparer.precision(filter=True)
246         sys.stdout.write('precision of %f bits against %s\n' % (precision, refImageFileName))
247         if precision < self.threshold_precision:
248             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
249             srcImageFileName = prefix + '.src.png'
250             srcImage.save(srcImageFileName)
251             diffImageFileName = prefix + '.diff.png'
252             comparer.write_diff(diffImageFileName)
253             fail('snapshot from call %u does not match %s' % (callNo, refImageFileName))
254
255         sys.stdout.flush()
256         sys.stderr.write('\n')
257
258     def checkState(self, callNo, refStateFileName):
259         sys.stderr.write('Comparing state dump from call %u against %s...\n' % (callNo, refStateFileName))
260
261         srcState = self.getState(callNo)
262         refState = self.getRefState(refStateFileName)
263
264         from jsondiff import Comparer, Differ
265         comparer = Comparer(ignore_added = True)
266         match = comparer.visit(refState, srcState)
267         if not match:
268             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
269             srcStateFileName = prefix + '.src.json'
270             diffStateFileName = prefix + '.diff.json'
271             self.saveState(srcState, srcStateFileName)
272             #diffStateFile = open(diffStateFileName, 'wt')
273             diffStateFile = sys.stdout
274             differ = Differ(diffStateFile, ignore_added = True)
275             differ.visit(refState, srcState)
276             fail('state from call %u does not match %s' % (callNo, refStateFileName))
277
278         sys.stdout.flush()
279         sys.stderr.write('\n')
280
281     def getRefState(self, refStateFileName):
282         stream = open(refStateFileName, 'rt')
283         from jsondiff import load
284         state = load(stream)
285         self.adjustRefState(state)
286         return state
287
288     def getNamePrefix(self):
289         name = os.path.basename(self.ref_dump)
290         try:
291             index = name.index('.')
292         except ValueError:
293             pass
294         else:
295             name = name[:index]
296         return name
297
298     def saveState(self, state, filename):
299         s = json.dumps(state, sort_keys=True, indent=2)
300         open(filename, 'wt').write(s)
301
302     def retrace(self):
303         if self.api not in self.api_retrace_map:
304             return
305
306         sys.stderr.write('Retracing %s...\n' % (self.trace_file,))
307
308         p = self._retrace()
309         p.wait()
310         if p.returncode != 0:
311             fail('retrace failed with code %i' % (p.returncode))
312
313         sys.stdout.flush()
314         sys.stderr.write('\n')
315
316     def getImage(self, callNo):
317         from PIL import Image
318         state = self.getState(callNo)
319         if self.doubleBuffer:
320             attachments = ['GL_BACK', 'GL_BACK_LEFT', 'GL_BACK_RIGHT', 'GL_COLOR_ATTACHMENT0', 'RENDER_TARGET_0']
321         else:
322             attachments = ['GL_FRONT', 'GL_FRONT_LEFT', 'GL_FRONT_RIGHT', 'GL_COLOR_ATTACHMENT0', 'RENDER_TARGET_0']
323         imageObj = self.getFramebufferAttachment(state, attachments)
324         data = imageObj['__data__']
325         stream = StringIO(base64.b64decode(data))
326         im = Image.open(stream)
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()