]> git.cworth.org Git - apitrace-tests/blob - driver.py
0a0dc5b7bd87f8cc0b98e807a98d29a8e84f7a70
[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
71     try:
72         env = kwargs.pop('env')
73     except KeyError:
74         env = None
75     else:
76         names = env.keys()
77         names.sort()
78         for name in names:
79             value = env[name]
80             if value != os.environ.get(name, None):
81                 sys.stdout.write('%s=%s ' % (name, value))
82             env[name] = str(value)
83
84     sys.stdout.write(' '.join(command) + '\n')
85     sys.stdout.flush()
86
87     return subprocess.Popen(command, *args, env=env, **kwargs)
88
89
90 def _get_build_path(path):
91     if options.build is not None:
92         path = os.path.abspath(os.path.join(options.build, path))
93     if not os.path.exists(path):
94         sys.stderr.write('error: %s does not exist\n' % path)
95         sys.exit(1)
96     return path
97
98 def _get_build_program(program):
99     if platform.system() == 'Windows':
100         program += '.exe'
101     return _get_build_path(program)
102
103 def _get_source_path(path):
104     cache = _get_build_path('CMakeCache.txt')
105     for line in open(cache, 'rt'):
106         if line.startswith('CMAKE_HOME_DIRECTORY:INTERNAL='):
107             _, source_root = line.strip().split('=', 1)
108             return os.path.join(source_root, path)
109     return None
110
111
112 class TraceChecker:
113
114     def __init__(self, srcStream, refFileName, verbose=False):
115         self.srcStream = srcStream
116         self.refFileName = refFileName
117         if refFileName:
118             self.refStream = open(refFileName, 'rt')
119         else:
120             self.refStream = None
121         self.verbose = verbose
122         self.doubleBuffer = False
123         self.callNo = 0
124         self.refLine = ''
125         self.images = []
126         self.states = []
127
128     call_re = re.compile(r'^([0-9]+) (\w+)\(')
129
130     def check(self):
131
132         swapbuffers = 0
133         flushes = 0
134
135         srcLines = []
136         self.consumeRefLine()
137         for line in self.srcStream:
138             line = line.rstrip()
139             if self.verbose:
140                 sys.stdout.write(line + '\n')
141             mo = self.call_re.match(line)
142             if mo:
143                 self.call_no = int(mo.group(1))
144                 function_name = mo.group(2)
145                 if function_name.find('SwapBuffers') != -1 or \
146                    line.find('kCGLPFADoubleBuffer') != -1:
147                     swapbuffers += 1
148                 if function_name in ('glFlush', 'glFinish'):
149                     flushes += 1
150                 srcLine = line[mo.start(2):]
151             else:
152                 srcLine = line
153             if self.refLine:
154                 if srcLine == self.refLine:
155                     self.consumeRefLine()
156                     srcLines = []
157                 else:
158                     srcLines.append(srcLine)
159
160         if self.refLine:
161             if srcLines:
162                 fail('missing call `%s` (found `%s`)' % (self.refLine, srcLines[0]))
163             else:
164                 fail('missing call %s' % self.refLine)
165
166         if swapbuffers:
167             self.doubleBuffer = True
168         else:
169             self.doubleBuffer = False
170
171     def consumeRefLine(self):
172         if not self.refStream:
173             self.refLine = ''
174             return
175
176         while True:
177             line = self.refStream.readline()
178             if not line:
179                 break
180             line = line.rstrip()
181             if line.startswith('#'):
182                 self.handlePragma(line)
183             else:
184                 break
185         self.refLine = line
186
187     def handlePragma(self, line):
188         pragma, rest = line.split(None, 1)
189         if pragma == '#image':
190             imageFileName = self.getAbsPath(rest)
191             self.images.append((self.callNo, imageFileName))
192         elif pragma == '#state':
193             stateFileName = self.getAbsPath(rest)
194             self.states.append((self.callNo, stateFileName))
195         else:
196             assert False
197
198     def getAbsPath(self, path):
199         '''Get the absolute from a path relative to the reference filename'''
200         return os.path.abspath(os.path.join(os.path.dirname(self.refFileName), path))
201
202
203
204 class TestCase:
205
206     cmd = None
207     cwd = None
208
209     api = 'gl'
210     max_frames = None
211     trace_file = None
212
213     ref_dump = None
214
215     doubleBuffer = True
216
217     verbose = False
218
219     def __init__(self):
220         self.stateCache = {}
221     
222     def runApp(self):
223         '''Run the application standalone, skipping this test if it fails by
224         some reason.'''
225
226         if not self.cmd:
227             return
228
229         p = popen(self.cmd, cwd=self.cwd)
230         p.wait()
231         if p.returncode:
232             skip('application returned code %i' % p.returncode)
233
234     api_map = {
235         'gl': 'gl',
236         'egl_gl': 'egl',
237         'egl_gles1': 'egl',
238         'egl_gles2': 'egl',
239     }
240
241     def traceApp(self):
242         if not self.cmd:
243             return
244
245         if self.trace_file is None:
246             name, ext = os.path.splitext(os.path.basename(self.cmd[0]))
247             self.trace_file = os.path.abspath(os.path.join(self.results, name + '.trace'))
248         if os.path.exists(self.trace_file):
249             os.remove(self.trace_file)
250         else:
251             trace_dir = os.path.dirname(self.trace_file)
252             if not os.path.exists(trace_dir):
253                 os.makedirs(trace_dir)
254
255         cmd = self.cmd
256         env = os.environ.copy()
257         
258         system = platform.system()
259         local_wrapper = None
260         if system == 'Windows':
261             wrapper = _get_build_path('wrappers/opengl32.dll')
262             local_wrapper = os.path.join(os.path.dirname(self.cmd[0]), os.path.basename(wrapper))
263             shutil.copy(wrapper, local_wrapper)
264             env['TRACE_FILE'] = str(self.trace_file)
265         else:
266             apitrace = _get_build_program('apitrace')
267             cmd = [
268                 apitrace, 'trace', 
269                 '--api', self.api_map[self.api],
270                 '--output', self.trace_file,
271                 '--'
272             ] + cmd
273         if self.max_frames is not None:
274             env['TRACE_FRAMES'] = str(self.max_frames)
275
276         try:
277             p = popen(cmd, env=env, cwd=self.cwd)
278             p.wait()
279         finally:
280             if local_wrapper is not None:
281                 os.remove(local_wrapper)
282
283         if not os.path.exists(self.trace_file):
284             fail('no trace file generated\n')
285     
286     def checkTrace(self):
287         cmd = [_get_build_program('apitrace'), 'dump', '--color=never', self.trace_file]
288         p = popen(cmd, stdout=subprocess.PIPE)
289
290         checker = TraceChecker(p.stdout, self.ref_dump, self.verbose)
291         checker.check()
292         p.wait()
293         if p.returncode != 0:
294             fail('`apitrace dump` returned code %i' % p.returncode)
295
296         self.doubleBuffer = checker.doubleBuffer
297
298         for callNo, refImageFileName in checker.images:
299             self.checkImage(callNo, refImageFileName)
300         for callNo, refStateFileName in checker.states:
301             self.checkState(callNo, refStateFileName)
302
303     def checkImage(self, callNo, refImageFileName):
304         srcImage = self.getImage(callNo)
305         refImage = Image.open(refImageFileName)
306
307         from snapdiff import Comparer
308         comparer = Comparer(refImage, srcImage)
309         match = comparer.ae()
310         if not match:
311             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
312             srcImageFileName = prefix + '.src.png'
313             diffImageFileName = prefix + '.diff.png'
314             comparer.write_diff(diffImageFileName)
315             fail('snapshot from call %u does not match %s' % (callNo, refImageFileName))
316
317     def checkState(self, callNo, refStateFileName):
318         srcState = self.getState(callNo)
319         refState = self.getRefState(refStateFileName)
320
321         from jsondiff import Comparer, Differ
322         comparer = Comparer(ignore_added = True)
323         match = comparer.visit(refState, srcState)
324         if not match:
325             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
326             srcStateFileName = prefix + '.src.json'
327             diffStateFileName = prefix + '.diff.json'
328             self.saveState(srcState, srcStateFileName)
329             #diffStateFile = open(diffStateFileName, 'wt')
330             diffStateFile = sys.stdout
331             differ = Differ(diffStateFile, ignore_added = True)
332             differ.visit(refState, srcState)
333             fail('state from call %u does not match %s' % (callNo, refStateFileName))
334
335     # Allo non-standard JS comments in JSON
336     json_comment_re = re.compile(r'//.*$', re.MULTILINE)
337
338     def getRefState(self, refStateFileName):
339         data = open(refStateFileName, 'rt').read()
340         data = self.json_comment_re.sub('', data)
341         state = json.loads(data, strict=False)
342         self.adjustRefState(state)
343         return state
344
345     def getNamePrefix(self):
346         name = os.path.basename(self.ref_dump)
347         try:
348             index = name.index('.')
349         except ValueError:
350             pass
351         else:
352             name = name[:index]
353         return name
354
355     def saveState(self, state, filename):
356         s = json.dumps(state, sort_keys=True, indent=2)
357         open(filename, 'wt').write(s)
358
359     def retrace(self):
360         p = self._retrace()
361         p.wait()
362         if p.returncode != 0:
363             fail('retrace failed with code %i' % (p.returncode))
364
365     def getImage(self, callNo):
366         state = self.getState(callNo)
367         if self.doubleBuffer:
368             attachments = ['GL_BACK', 'GL_BACK_LEFT', 'GL_BACK_RIGHT', 'GL_COLOR_ATTACHMENT0']
369         else:
370             attachments = ['GL_FRONT', 'GL_FRONT_LEFT', 'GL_FRONT_RIGHT', 'GL_COLOR_ATTACHMENT0']
371         imageObj = self.getFramebufferAttachment(state, attachments)
372         data = imageObj['__data__']
373         stream = StringIO(base64.b64decode(data))
374         im = Image.open(stream)
375         im.save('test.png')
376         return im
377
378     def getFramebufferAttachment(self, state, attachments):
379         framebufferObj = state['framebuffer']
380         for attachment in attachments:
381             try:
382                 attachmentObj = framebufferObj[attachment]
383             except KeyError:
384                 pass
385             else:
386                 return attachmentObj
387         raise Exception("no attachment found")
388
389     def getState(self, callNo):
390         try:
391             state = self.stateCache[callNo]
392         except KeyError:
393             pass
394         else:
395             return state
396
397         p = self._retrace(['-D', str(callNo)])
398         state = json.load(p.stdout, strict=False)
399         p.wait()
400         if p.returncode != 0:
401             fail('retrace returned code %i' % (p.returncode))
402
403         self.adjustSrcState(state)
404
405         self.stateCache[callNo] = state
406
407         return state
408
409     def adjustSrcState(self, state):
410         # Do some adjustments on the obtained state to eliminate failures from
411         # bugs/issues outside of apitrace
412
413         try:
414             parameters = state['parameters']
415         except KeyError:
416             return
417
418         # On NVIDIA drivers glGetIntegerv(GL_INDEX_WRITEMASK) returns -1
419         self.replaceState(parameters, 'GL_INDEX_WRITEMASK', 255, -1)
420
421         # On Gallium 
422         if 'Gallium' in parameters['GL_RENDERER'].split():
423             # Gallium drivers have wrong defaults for draw/read buffer state
424             self.replaceState(parameters, 'GL_DRAW_BUFFER', 'GL_BACK_LEFT', 'GL_BACK')
425             self.replaceState(parameters, 'GL_DRAW_BUFFER0', 'GL_BACK_LEFT', 'GL_BACK')
426             self.replaceState(parameters, 'GL_READ_BUFFER', 'GL_BACK_LEFT', 'GL_BACK')
427             self.replaceState(parameters, 'GL_DRAW_BUFFER', 'GL_FRONT_LEFT', 'GL_FRONT')
428             self.replaceState(parameters, 'GL_DRAW_BUFFER0', 'GL_FRONT_LEFT', 'GL_FRONT')
429             self.replaceState(parameters, 'GL_READ_BUFFER', 'GL_FRONT_LEFT', 'GL_FRONT')
430
431     def adjustRefState(self, state):
432         # Do some adjustments on reference state to eliminate failures from
433         # bugs/issues outside of apitrace
434
435         try:
436             parameters = state['parameters']
437         except KeyError:
438             return
439
440         if platform.system() == 'Darwin' or True:
441             # Mac OS X drivers fail on GL_COLOR_SUM
442             # XXX: investigate this
443             self.removeState(parameters, 'GL_COLOR_SUM')
444
445     def replaceState(self, obj, key, srcValue, dstValue):
446         try:
447             value = obj[key]
448         except KeyError:
449             pass
450         else:
451             if value == srcValue:
452                 obj[key] = dstValue
453
454     def removeState(self, obj, key):
455         try:
456             del obj[key]
457         except KeyError:
458             pass
459
460     def _retrace(self, args = None, stdout=subprocess.PIPE):
461         retrace = self.api_map[self.api] + 'retrace'
462         cmd = [_get_build_program(retrace)]
463         if self.doubleBuffer:
464             cmd += ['-db']
465         else:
466             cmd += ['-sb']
467         if args:
468             cmd += args
469         cmd += [self.trace_file]
470         return popen(cmd, stdout=stdout)
471
472     def run(self):
473         self.runApp()
474         self.traceApp()
475         self.checkTrace()
476         self.retrace()
477
478         pass_()
479
480
481 def main():
482     global options
483
484     # Parse command line options
485     optparser = optparse.OptionParser(
486         usage='\n\t%prog [options] -- [TRACE|PROGRAM] ...',
487         version='%%prog')
488     optparser.add_option(
489         '-v', '--verbose',
490         action="store_true",
491         dest="verbose", default=False,
492         help="verbose output")
493     optparser.add_option(
494         '-a', '--api', metavar='API',
495         type='string', dest='api', default='gl',
496         help='api to trace')
497     optparser.add_option(
498         '-B', '--build', metavar='PATH',
499         type='string', dest='build', default='..',
500         help='path to apitrace build')
501     optparser.add_option(
502         '-C', '--directory', metavar='PATH',
503         type='string', dest='cwd', default=None,
504         help='change to directory')
505     optparser.add_option(
506         '-R', '--results', metavar='PATH',
507         type='string', dest='results', default='.',
508         help='results directory [default=%default]')
509     optparser.add_option(
510         '--ref-dump', metavar='PATH',
511         type='string', dest='ref_dump', default=None,
512         help='reference dump')
513
514     (options, args) = optparser.parse_args(sys.argv[1:])
515     if not args:
516         optparser.error('an argument must be specified')
517
518     if not os.path.exists(options.results):
519         os.makedirs(options.results)
520
521     sys.path.insert(0, _get_source_path('scripts'))
522
523     test = TestCase()
524     test.verbose = options.verbose
525
526     if args[0].endswith('.trace'):
527         test.trace_file = args[0]
528     else:
529         test.cmd = args
530     test.cwd = options.cwd
531     test.api = options.api
532     test.ref_dump = options.ref_dump
533     test.results = options.results
534
535     test.run()
536
537
538 if __name__ == '__main__':
539     main()