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