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