]> git.cworth.org Git - apitrace-tests/blob - driver.py
1c4904cc24b335f3672e846386ac4fd707ce89ce
[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 or \
135                    line.find('kCGLPFADoubleBuffer') != -1:
136                     swapbuffers += 1
137                 if function_name in ('glFlush', 'glFinish'):
138                     flushes += 1
139                 srcLine = line[mo.start(2):]
140             else:
141                 srcLine = line
142             if self.refLine:
143                 if srcLine == self.refLine:
144                     self.consumeRefLine()
145                     srcLines = []
146                 else:
147                     srcLines.append(srcLine)
148
149         if self.refLine:
150             if srcLines:
151                 fail('missing call `%s` (found `%s`)' % (self.refLine, srcLines[0]))
152             else:
153                 fail('missing call %s' % self.refLine)
154
155         if swapbuffers:
156             self.doubleBuffer = True
157         else:
158             self.doubleBuffer = False
159
160     def consumeRefLine(self):
161         if not self.refStream:
162             self.refLine = ''
163             return
164
165         while True:
166             line = self.refStream.readline()
167             if not line:
168                 break
169             line = line.rstrip()
170             if line.startswith('#'):
171                 self.handlePragma(line)
172             else:
173                 break
174         self.refLine = line
175
176     def handlePragma(self, line):
177         pragma, rest = line.split(None, 1)
178         if pragma == '#image':
179             imageFileName = self.getAbsPath(rest)
180             self.images.append((self.callNo, imageFileName))
181         elif pragma == '#state':
182             stateFileName = self.getAbsPath(rest)
183             self.states.append((self.callNo, stateFileName))
184         else:
185             assert False
186
187     def getAbsPath(self, path):
188         '''Get the absolute from a path relative to the reference filename'''
189         return os.path.abspath(os.path.join(os.path.dirname(self.refFileName), path))
190
191
192
193 class TestCase:
194
195     cmd = None
196     cwd = None
197
198     api = 'gl'
199     max_frames = None
200     trace_file = None
201
202     ref_dump = None
203
204     doubleBuffer = True
205
206     verbose = False
207
208     def __init__(self):
209         self.stateCache = {}
210     
211     def runApp(self):
212         '''Run the application standalone, skipping this test if it fails by
213         some reason.'''
214
215         if not self.cmd:
216             return
217
218         p = popen(self.cmd, cwd=self.cwd)
219         p.wait()
220         if p.returncode:
221             skip('application returned code %i' % p.returncode)
222
223     api_map = {
224         'gl': 'gl',
225         'egl_gl': 'egl',
226         'egl_gles1': 'egl',
227         'egl_gles2': 'egl',
228     }
229
230     def traceApp(self):
231         if not self.cmd:
232             return
233
234         if self.trace_file is None:
235             name = os.path.basename(self.cmd[0])
236             self.trace_file = os.path.abspath(os.path.join(self.results, name + '.trace'))
237         if os.path.exists(self.trace_file):
238             os.remove(self.trace_file)
239         else:
240             trace_dir = os.path.dirname(self.trace_file)
241             if not os.path.exists(trace_dir):
242                 os.makedirs(trace_dir)
243
244         cmd = self.cmd
245         env = os.environ.copy()
246         
247         system = platform.system()
248         local_wrapper = None
249         if system == 'Windows':
250             wrapper = _get_build_path('wrappers/opengl32.dll')
251             local_wrapper = os.path.join(os.path.dirname(self.cmd[0]), os.path.basename(wrapper))
252             shutil.copy(wrapper, local_wrapper)
253             env['TRACE_FILE'] = self.trace_file
254         else:
255             apitrace = _get_build_program('apitrace')
256             cmd = [
257                 apitrace, 'trace', 
258                 '--api', self.api_map[self.api],
259                 '--output', self.trace_file,
260                 '--'
261             ] + cmd
262         if self.max_frames is not None:
263             env['TRACE_FRAMES'] = str(self.max_frames)
264
265         try:
266             p = popen(cmd, env=env, cwd=self.cwd)
267             p.wait()
268         finally:
269             if local_wrapper is not None:
270                 os.remove(local_wrapper)
271
272         if not os.path.exists(self.trace_file):
273             fail('no trace file generated\n')
274     
275     def checkTrace(self):
276         cmd = [_get_build_program('apitrace'), 'dump', '--color=never', self.trace_file]
277         p = popen(cmd, stdout=subprocess.PIPE)
278
279         checker = TraceChecker(p.stdout, self.ref_dump, self.verbose)
280         checker.check()
281         p.wait()
282         if p.returncode != 0:
283             fail('`apitrace dump` returned code %i' % p.returncode)
284
285         self.doubleBuffer = checker.doubleBuffer
286
287         for callNo, refImageFileName in checker.images:
288             self.checkImage(callNo, refImageFileName)
289         for callNo, refStateFileName in checker.states:
290             self.checkState(callNo, refStateFileName)
291
292     def checkImage(self, callNo, refImageFileName):
293         srcImage = self.getImage(callNo)
294         refImage = Image.open(refImageFileName)
295
296         from snapdiff import Comparer
297         comparer = Comparer(refImage, srcImage)
298         match = comparer.ae()
299         if not match:
300             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
301             srcImageFileName = prefix + '.src.png'
302             diffImageFileName = prefix + '.diff.png'
303             comparer.write_diff(diffImageFileName)
304             fail('snapshot from call %u does not match %s' % (callNo, refImageFileName))
305
306     def checkState(self, callNo, refStateFileName):
307         srcState = self.getState(callNo)
308         refState = json.load(open(refStateFileName, 'rt'), strict=False)
309
310         from jsondiff import Comparer, Differ
311         comparer = Comparer(ignore_added = True)
312         match = comparer.visit(refState, srcState)
313         if not match:
314             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
315             srcStateFileName = prefix + '.src.json'
316             diffStateFileName = prefix + '.diff.json'
317             self.saveState(srcState, srcStateFileName)
318             #diffStateFile = open(diffStateFileName, 'wt')
319             diffStateFile = sys.stdout
320             differ = Differ(diffStateFile, ignore_added = True)
321             differ.visit(refState, srcState)
322             fail('state from call %u does not match %s' % (callNo, refStateFileName))
323
324     def getNamePrefix(self):
325         name = os.path.basename(self.ref_dump)
326         try:
327             index = name.index('.')
328         except ValueError:
329             pass
330         else:
331             name = name[:index]
332         return name
333
334     def saveState(self, state, filename):
335         s = json.dumps(state, sort_keys=True, indent=2)
336         open(filename, 'wt').write(s)
337
338     def retrace(self):
339         p = self._retrace()
340         p.wait()
341         if p.returncode != 0:
342             fail('retrace failed with code %i' % (p.returncode))
343
344     def getImage(self, callNo):
345         state = self.getState(callNo)
346         framebuffer = state['framebuffer']
347         if self.doubleBuffer:
348             imageObj = framebuffer['GL_BACK']
349         else:
350             imageObj = framebuffer['GL_FRONT']
351         data = imageObj['__data__']
352         stream = StringIO(base64.b64decode(data))
353         im = Image.open(stream)
354         im.save('test.png')
355         return im
356
357     def getState(self, callNo):
358         try:
359             state = self.stateCache[callNo]
360         except KeyError:
361             pass
362         else:
363             return state
364
365         p = self._retrace(['-D', str(callNo)])
366         state = json.load(p.stdout, strict=False)
367         p.wait()
368         if p.returncode != 0:
369             fail('retrace returned code %i' % (p.returncode))
370
371         self.stateCache[callNo] = state
372
373         return state
374
375     def _retrace(self, args = None, stdout=subprocess.PIPE):
376         retrace = self.api_map[self.api] + 'retrace'
377         cmd = [_get_build_path(retrace)]
378         if self.doubleBuffer:
379             cmd += ['-db']
380         else:
381             cmd += ['-sb']
382         if args:
383             cmd += args
384         cmd += [self.trace_file]
385         return popen(cmd, stdout=stdout)
386
387     def run(self):
388         self.runApp()
389         self.traceApp()
390         self.checkTrace()
391         self.retrace()
392
393         pass_()
394
395
396 def main():
397     global options
398
399     # Parse command line options
400     optparser = optparse.OptionParser(
401         usage='\n\t%prog [options] -- [TRACE|PROGRAM] ...',
402         version='%%prog')
403     optparser.add_option(
404         '-v', '--verbose',
405         action="store_true",
406         dest="verbose", default=False,
407         help="verbose output")
408     optparser.add_option(
409         '-a', '--api', metavar='API',
410         type='string', dest='api', default='gl',
411         help='api to trace')
412     optparser.add_option(
413         '-B', '--build', metavar='PATH',
414         type='string', dest='build', default='..',
415         help='path to apitrace build')
416     optparser.add_option(
417         '-C', '--directory', metavar='PATH',
418         type='string', dest='cwd', default=None,
419         help='change to directory')
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     (options, args) = optparser.parse_args(sys.argv[1:])
430     if not args:
431         optparser.error('an argument must be specified')
432
433     if not os.path.exists(options.results):
434         os.makedirs(options.results)
435
436     sys.path.insert(0, _get_source_path('scripts'))
437
438     test = TestCase()
439     test.verbose = options.verbose
440
441     if args[0].endswith('.trace'):
442         test.trace_file = args[0]
443     else:
444         test.cmd = args
445     test.cwd = options.cwd
446     test.api = options.api
447     test.ref_dump = options.ref_dump
448     test.results = options.results
449
450     test.run()
451
452
453 if __name__ == '__main__':
454     main()