]> git.cworth.org Git - apitrace-tests/blob - driver.py
Check against reference images/state.
[apitrace-tests] / 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 '''Main test driver.'''
28
29
30 import optparse
31 import os.path
32 import platform
33 import re
34 import shutil
35 import subprocess
36 import sys
37 import time
38 import json
39 import base64
40
41 from PIL import Image
42
43 try:
44     from cStringIO import StringIO
45 except ImportError:
46     from StringIO import StringIO
47
48
49 def _exit(status, code, reason=None):
50     if reason is None:
51         reason = ''
52     else:
53         reason = ' (%s)' % reason
54     sys.stdout.write('%s%s\n' % (status, reason))
55     sys.exit(code)
56
57 def fail(reason=None):
58     _exit('FAIL', 1, reason)
59
60 def skip(reason=None):
61     _exit('SKIP', 0, reason)
62
63 def pass_(reason=None):
64     _exit('PASS', 0, reason)
65
66
67 def popen(command, *args, **kwargs):
68     if kwargs.get('cwd', None) is not None:
69         sys.stdout.write('cd %s && ' % kwargs['cwd'])
70     if 'env' in kwargs:
71         for name, value in kwargs['env'].iteritems():
72             if value != os.environ.get(name, None):
73                 sys.stdout.write('%s=%s ' % (name, value))
74     sys.stdout.write(' '.join(command) + '\n')
75     sys.stdout.flush()
76     return subprocess.Popen(command, *args, **kwargs)
77
78
79 def _get_build_path(path):
80     if options.build is not None:
81         path = os.path.abspath(os.path.join(options.build, path))
82     if not os.path.exists(path):
83         sys.stderr.write('error: %s does not exist\n' % path)
84         sys.exit(1)
85     return path
86
87 def _get_build_program(program):
88     if platform.system() == 'Windows':
89         program += '.exe'
90     return _get_build_path(program)
91
92 def _get_source_path(path):
93     cache = _get_build_path('CMakeCache.txt')
94     for line in open(cache, 'rt'):
95         if line.startswith('CMAKE_HOME_DIRECTORY:INTERNAL='):
96             _, source_root = line.strip().split('=', 1)
97             return os.path.join(source_root, path)
98     return None
99
100
101 class TraceChecker:
102
103     def __init__(self, srcStream, refFileName, verbose=False):
104         self.srcStream = srcStream
105         self.refFileName = refFileName
106         if refFileName:
107             self.refStream = open(refFileName, 'rt')
108         else:
109             self.refStream = None
110         self.verbose = verbose
111         self.doubleBuffer = False
112         self.callNo = 0
113         self.refLine = ''
114         self.images = []
115         self.states = []
116
117     call_re = re.compile(r'^([0-9]+) (\w+)\(')
118
119     def check(self):
120
121         swapbuffers = 0
122         flushes = 0
123
124         srcLines = []
125         self.consumeRefLine()
126         for line in self.srcStream:
127             line = line.rstrip()
128             if self.verbose:
129                 sys.stdout.write(line + '\n')
130             mo = self.call_re.match(line)
131             if mo:
132                 self.call_no = int(mo.group(1))
133                 function_name = mo.group(2)
134                 if function_name.find('SwapBuffers') != -1:
135                     swapbuffers += 1
136                 if function_name in ('glFlush', 'glFinish'):
137                     flushes += 1
138                 srcLine = line[mo.start(2):]
139             else:
140                 srcLine = line
141             if self.refLine:
142                 if srcLine == self.refLine:
143                     self.consumeRefLine()
144                     srcLines = []
145                 else:
146                     srcLines.append(srcLine)
147
148         if self.refLine:
149             if srcLines:
150                 fail('missing call `%s` (found `%s`)' % (self.refLine, srcLines[0]))
151             else:
152                 fail('missing call %s' % self.refLine)
153
154         if swapbuffers:
155             self.doubleBuffer = True
156         else:
157             self.doubleBuffer = False
158
159     def consumeRefLine(self):
160         if not self.refStream:
161             self.refLine = ''
162             return
163
164         while True:
165             line = self.refStream.readline()
166             if not line:
167                 break
168             line = line.rstrip()
169             if line.startswith('#'):
170                 self.handlePragma(line)
171             else:
172                 break
173         self.refLine = line
174
175     def handlePragma(self, line):
176         pragma, rest = line.split(None, 1)
177         if pragma == '#image':
178             imageFileName = self.getAbsPath(rest)
179             self.images.append((self.callNo, imageFileName))
180         elif pragma == '#state':
181             stateFileName = self.getAbsPath(rest)
182             self.states.append((self.callNo, stateFileName))
183         else:
184             assert False
185
186     def getAbsPath(self, path):
187         '''Get the absolute from a path relative to the reference filename'''
188         return os.path.abspath(os.path.join(os.path.dirname(self.refFileName), path))
189
190
191
192 class TestCase:
193
194     cmd = None
195     cwd = None
196
197     api = 'gl'
198     max_frames = None
199     trace_file = None
200
201     ref_dump = None
202
203     doubleBuffer = True
204
205     verbose = False
206
207     def __init__(self):
208         self.stateCache = {}
209     
210     def runApp(self):
211         '''Run the application standalone, skipping this test if it fails by
212         some reason.'''
213
214         if not self.cmd:
215             return
216
217         p = popen(self.cmd, cwd=self.cwd)
218         p.wait()
219         if p.returncode:
220             skip('application returned code %i' % p.returncode)
221
222     api_map = {
223         'gl': 'gl',
224         'egl_gl': 'egl',
225         'egl_gles1': 'egl',
226         'egl_gles2': 'egl',
227     }
228
229     def traceApp(self):
230         if not self.cmd:
231             return
232
233         if self.trace_file is None:
234             name = os.path.basename(self.cmd[0])
235             self.trace_file = os.path.abspath(os.path.join(self.results, name + '.trace'))
236         if os.path.exists(self.trace_file):
237             os.remove(self.trace_file)
238         else:
239             trace_dir = os.path.dirname(self.trace_file)
240             if not os.path.exists(trace_dir):
241                 os.makedirs(trace_dir)
242
243         cmd = self.cmd
244         env = os.environ.copy()
245         
246         system = platform.system()
247         local_wrapper = None
248         if system == 'Windows':
249             wrapper = _get_build_path('wrappers/opengl32.dll')
250             local_wrapper = os.path.join(os.path.dirname(self.cmd[0]), os.path.basename(wrapper))
251             shutil.copy(wrapper, local_wrapper)
252             env['TRACE_FILE'] = self.trace_file
253         else:
254             apitrace = _get_build_program('apitrace')
255             cmd = [
256                 apitrace, 'trace', 
257                 '--api', self.api_map[self.api],
258                 '--output', self.trace_file,
259                 '--'
260             ] + cmd
261         if self.max_frames is not None:
262             env['TRACE_FRAMES'] = str(self.max_frames)
263
264         try:
265             p = popen(cmd, env=env, cwd=self.cwd)
266             p.wait()
267         finally:
268             if local_wrapper is not None:
269                 os.remove(local_wrapper)
270
271         if not os.path.exists(self.trace_file):
272             fail('no trace file generated\n')
273     
274     def checkTrace(self):
275         cmd = [_get_build_program('apitrace'), 'dump', '--color=never', self.trace_file]
276         p = popen(cmd, stdout=subprocess.PIPE)
277
278         checker = TraceChecker(p.stdout, self.ref_dump, self.verbose)
279         checker.check()
280         p.wait()
281         if p.returncode != 0:
282             fail('`apitrace dump` returned code %i' % p.returncode)
283
284         self.doubleBuffer = checker.doubleBuffer
285
286         for callNo, refImageFileName in checker.images:
287             self.checkImage(callNo, refImageFileName)
288         for callNo, refStateFileName in checker.states:
289             self.checkState(callNo, refStateFileName)
290
291     def checkImage(self, callNo, refImageFileName):
292         srcImage = self.getImage(callNo)
293         refImage = Image.open(refImageFileName)
294
295         from snapdiff import Comparer
296         comparer = Comparer(refImage, srcImage)
297         match = comparer.ae()
298         if not match:
299             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
300             srcImageFileName = prefix + '.src.png'
301             diffImageFileName = prefix + '.diff.png'
302             comparer.write_diff(diffImageFileName)
303             fail('snapshot from call %u does not match %s' % (callNo, refImageFileName))
304
305     def checkState(self, callNo, refStateFileName):
306         srcState = self.getState(callNo)
307         refState = json.load(open(refStateFileName, 'rt'), strict=False)
308
309         from jsondiff import Comparer, Differ
310         comparer = Comparer(ignore_added = True)
311         match = comparer.visit(refState, srcState)
312         if not match:
313             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
314             srcStateFileName = prefix + '.src.json'
315             diffStateFileName = prefix + '.diff.json'
316             self.saveState(srcState, srcStateFileName)
317             #diffStateFile = open(diffStateFileName, 'wt')
318             diffStateFile = sys.stdout
319             differ = Differ(diffStateFile, ignore_added = True)
320             differ.visit(refState, srcState)
321             fail('state from call %u does not match %s' % (callNo, refStateFileName))
322
323     def getNamePrefix(self):
324         name = os.path.basename(self.ref_dump)
325         try:
326             index = name.index('.')
327         except ValueError:
328             pass
329         else:
330             name = name[:index]
331         return name
332
333     def saveState(self, state, filename):
334         s = json.dumps(state, sort_keys=True, indent=2)
335         open(filename, 'wt').write(s)
336
337     def retrace(self):
338         p = self._retrace()
339         p.wait()
340         if p.returncode != 0:
341             fail('retrace failed with code %i' % (p.returncode))
342
343     def getImage(self, callNo):
344         state = self.getState(callNo)
345         framebuffer = state['framebuffer']
346         if self.doubleBuffer:
347             imageObj = framebuffer['GL_BACK']
348         else:
349             imageObj = framebuffer['GL_FRONT']
350         data = imageObj['__data__']
351         stream = StringIO(base64.b64decode(data))
352         im = Image.open(stream)
353         im.save('test.png')
354         return im
355
356     def getState(self, callNo):
357         try:
358             state = self.stateCache[callNo]
359         except KeyError:
360             pass
361         else:
362             return state
363
364         p = self._retrace(['-D', str(callNo)])
365         state = json.load(p.stdout, strict=False)
366         p.wait()
367         if p.returncode != 0:
368             fail('retrace returned code %i' % (p.returncode))
369
370         self.stateCache[callNo] = state
371
372         return state
373
374     def _retrace(self, args = None, stdout=subprocess.PIPE):
375         retrace = self.api_map[self.api] + 'retrace'
376         cmd = [_get_build_path(retrace)]
377         if self.doubleBuffer:
378             cmd += ['-db']
379         else:
380             cmd += ['-sb']
381         if args:
382             cmd += args
383         cmd += [self.trace_file]
384         return popen(cmd, stdout=stdout)
385
386     def run(self):
387         self.runApp()
388         self.traceApp()
389         self.checkTrace()
390         self.retrace()
391
392         pass_()
393
394
395 def main():
396     global options
397
398     # Parse command line options
399     optparser = optparse.OptionParser(
400         usage='\n\t%prog [options] -- [TRACE|PROGRAM] ...',
401         version='%%prog')
402     optparser.add_option(
403         '-v', '--verbose',
404         action="store_true",
405         dest="verbose", default=False,
406         help="verbose output")
407     optparser.add_option(
408         '-a', '--api', metavar='API',
409         type='string', dest='api', default='gl',
410         help='api to trace')
411     optparser.add_option(
412         '-B', '--build', metavar='PATH',
413         type='string', dest='build', default='..',
414         help='path to apitrace build')
415     optparser.add_option(
416         '-C', '--directory', metavar='PATH',
417         type='string', dest='cwd', default=None,
418         help='change to directory')
419     optparser.add_option(
420         '-R', '--results', metavar='PATH',
421         type='string', dest='results', default='.',
422         help='results directory [default=%default]')
423     optparser.add_option(
424         '--ref-dump', metavar='PATH',
425         type='string', dest='ref_dump', default=None,
426         help='reference dump')
427
428     (options, args) = optparser.parse_args(sys.argv[1:])
429     if not args:
430         optparser.error('an argument must be specified')
431
432     if not os.path.exists(options.results):
433         os.makedirs(options.results)
434
435     sys.path.insert(0, _get_source_path('scripts'))
436
437     test = TestCase()
438     test.verbose = options.verbose
439
440     if args[0].endswith('.trace'):
441         test.trace_file = args[0]
442     else:
443         test.cmd = args
444     test.cwd = options.cwd
445     test.api = options.api
446     test.ref_dump = options.ref_dump
447     test.results = options.results
448
449     test.run()
450
451
452 if __name__ == '__main__':
453     main()