]> git.cworth.org Git - apitrace-tests/blob - driver.py
871ef097ec63cc7a1f115d40f23800aa69f72760
[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         if self.doubleBuffer:
347             attachments = ['GL_BACK', 'GL_BACK_LEFT', 'GL_BACK_RIGHT', 'GL_COLOR_ATTACHMENT0']
348         else:
349             attachments = ['GL_FRONT', 'GL_FRONT_LEFT', 'GL_FRONT_RIGHT', 'GL_COLOR_ATTACHMENT0']
350         imageObj = self.getFramebufferAttachment(state, attachments)
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 getFramebufferAttachment(self, state, attachments):
358         framebufferObj = state['framebuffer']
359         for attachment in attachments:
360             try:
361                 attachmentObj = framebufferObj[attachment]
362             except KeyError:
363                 pass
364             else:
365                 return attachmentObj
366         raise Exception("no attachment found")
367
368     def getState(self, callNo):
369         try:
370             state = self.stateCache[callNo]
371         except KeyError:
372             pass
373         else:
374             return state
375
376         p = self._retrace(['-D', str(callNo)])
377         state = json.load(p.stdout, strict=False)
378         p.wait()
379         if p.returncode != 0:
380             fail('retrace returned code %i' % (p.returncode))
381
382         self.stateCache[callNo] = state
383
384         return state
385
386     def _retrace(self, args = None, stdout=subprocess.PIPE):
387         retrace = self.api_map[self.api] + 'retrace'
388         cmd = [_get_build_path(retrace)]
389         if self.doubleBuffer:
390             cmd += ['-db']
391         else:
392             cmd += ['-sb']
393         if args:
394             cmd += args
395         cmd += [self.trace_file]
396         return popen(cmd, stdout=stdout)
397
398     def run(self):
399         self.runApp()
400         self.traceApp()
401         self.checkTrace()
402         self.retrace()
403
404         pass_()
405
406
407 def main():
408     global options
409
410     # Parse command line options
411     optparser = optparse.OptionParser(
412         usage='\n\t%prog [options] -- [TRACE|PROGRAM] ...',
413         version='%%prog')
414     optparser.add_option(
415         '-v', '--verbose',
416         action="store_true",
417         dest="verbose", default=False,
418         help="verbose output")
419     optparser.add_option(
420         '-a', '--api', metavar='API',
421         type='string', dest='api', default='gl',
422         help='api to trace')
423     optparser.add_option(
424         '-B', '--build', metavar='PATH',
425         type='string', dest='build', default='..',
426         help='path to apitrace build')
427     optparser.add_option(
428         '-C', '--directory', metavar='PATH',
429         type='string', dest='cwd', default=None,
430         help='change to directory')
431     optparser.add_option(
432         '-R', '--results', metavar='PATH',
433         type='string', dest='results', default='.',
434         help='results directory [default=%default]')
435     optparser.add_option(
436         '--ref-dump', metavar='PATH',
437         type='string', dest='ref_dump', default=None,
438         help='reference dump')
439
440     (options, args) = optparser.parse_args(sys.argv[1:])
441     if not args:
442         optparser.error('an argument must be specified')
443
444     if not os.path.exists(options.results):
445         os.makedirs(options.results)
446
447     sys.path.insert(0, _get_source_path('scripts'))
448
449     test = TestCase()
450     test.verbose = options.verbose
451
452     if args[0].endswith('.trace'):
453         test.trace_file = args[0]
454     else:
455         test.cmd = args
456     test.cwd = options.cwd
457     test.api = options.api
458     test.ref_dump = options.ref_dump
459     test.results = options.results
460
461     test.run()
462
463
464 if __name__ == '__main__':
465     main()