]> git.cworth.org Git - apitrace-tests/blob - driver.py
Add tweaks/comments to default.ref.json
[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 = self.getRefState(refStateFileName)
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     # Allo non-standard JS comments in JSON
325     json_comment_re = re.compile(r'//.*$', re.MULTILINE)
326
327     def getRefState(self, refStateFileName):
328         data = open(refStateFileName, 'rt').read()
329         data = self.json_comment_re.sub('', data)
330         return json.loads(data, strict=False)
331
332     def getNamePrefix(self):
333         name = os.path.basename(self.ref_dump)
334         try:
335             index = name.index('.')
336         except ValueError:
337             pass
338         else:
339             name = name[:index]
340         return name
341
342     def saveState(self, state, filename):
343         s = json.dumps(state, sort_keys=True, indent=2)
344         open(filename, 'wt').write(s)
345
346     def retrace(self):
347         p = self._retrace()
348         p.wait()
349         if p.returncode != 0:
350             fail('retrace failed with code %i' % (p.returncode))
351
352     def getImage(self, callNo):
353         state = self.getState(callNo)
354         if self.doubleBuffer:
355             attachments = ['GL_BACK', 'GL_BACK_LEFT', 'GL_BACK_RIGHT', 'GL_COLOR_ATTACHMENT0']
356         else:
357             attachments = ['GL_FRONT', 'GL_FRONT_LEFT', 'GL_FRONT_RIGHT', 'GL_COLOR_ATTACHMENT0']
358         imageObj = self.getFramebufferAttachment(state, attachments)
359         data = imageObj['__data__']
360         stream = StringIO(base64.b64decode(data))
361         im = Image.open(stream)
362         im.save('test.png')
363         return im
364
365     def getFramebufferAttachment(self, state, attachments):
366         framebufferObj = state['framebuffer']
367         for attachment in attachments:
368             try:
369                 attachmentObj = framebufferObj[attachment]
370             except KeyError:
371                 pass
372             else:
373                 return attachmentObj
374         raise Exception("no attachment found")
375
376     def getState(self, callNo):
377         try:
378             state = self.stateCache[callNo]
379         except KeyError:
380             pass
381         else:
382             return state
383
384         p = self._retrace(['-D', str(callNo)])
385         state = json.load(p.stdout, strict=False)
386         p.wait()
387         if p.returncode != 0:
388             fail('retrace returned code %i' % (p.returncode))
389
390         self.stateCache[callNo] = state
391
392         return state
393
394     def _retrace(self, args = None, stdout=subprocess.PIPE):
395         retrace = self.api_map[self.api] + 'retrace'
396         cmd = [_get_build_path(retrace)]
397         if self.doubleBuffer:
398             cmd += ['-db']
399         else:
400             cmd += ['-sb']
401         if args:
402             cmd += args
403         cmd += [self.trace_file]
404         return popen(cmd, stdout=stdout)
405
406     def run(self):
407         self.runApp()
408         self.traceApp()
409         self.checkTrace()
410         self.retrace()
411
412         pass_()
413
414
415 def main():
416     global options
417
418     # Parse command line options
419     optparser = optparse.OptionParser(
420         usage='\n\t%prog [options] -- [TRACE|PROGRAM] ...',
421         version='%%prog')
422     optparser.add_option(
423         '-v', '--verbose',
424         action="store_true",
425         dest="verbose", default=False,
426         help="verbose output")
427     optparser.add_option(
428         '-a', '--api', metavar='API',
429         type='string', dest='api', default='gl',
430         help='api to trace')
431     optparser.add_option(
432         '-B', '--build', metavar='PATH',
433         type='string', dest='build', default='..',
434         help='path to apitrace build')
435     optparser.add_option(
436         '-C', '--directory', metavar='PATH',
437         type='string', dest='cwd', default=None,
438         help='change to directory')
439     optparser.add_option(
440         '-R', '--results', metavar='PATH',
441         type='string', dest='results', default='.',
442         help='results directory [default=%default]')
443     optparser.add_option(
444         '--ref-dump', metavar='PATH',
445         type='string', dest='ref_dump', default=None,
446         help='reference dump')
447
448     (options, args) = optparser.parse_args(sys.argv[1:])
449     if not args:
450         optparser.error('an argument must be specified')
451
452     if not os.path.exists(options.results):
453         os.makedirs(options.results)
454
455     sys.path.insert(0, _get_source_path('scripts'))
456
457     test = TestCase()
458     test.verbose = options.verbose
459
460     if args[0].endswith('.trace'):
461         test.trace_file = args[0]
462     else:
463         test.cmd = args
464     test.cwd = options.cwd
465     test.api = options.api
466     test.ref_dump = options.ref_dump
467     test.results = options.results
468
469     test.run()
470
471
472 if __name__ == '__main__':
473     main()