]> git.cworth.org Git - apitrace-tests/blob - driver.py
More flexible image comparison.
[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.callNo = 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     threshold_precision = 12.0
220
221     def __init__(self):
222         self.stateCache = {}
223     
224     def runApp(self):
225         '''Run the application standalone, skipping this test if it fails by
226         some reason.'''
227
228         if not self.cmd:
229             return
230
231         p = popen(self.cmd, cwd=self.cwd)
232         p.wait()
233         if p.returncode:
234             skip('application returned code %i' % p.returncode)
235
236     api_map = {
237         'gl': 'gl',
238         'egl_gl': 'egl',
239         'egl_gles1': 'egl',
240         'egl_gles2': 'egl',
241     }
242
243     def traceApp(self):
244         if not self.cmd:
245             return
246
247         if self.trace_file is None:
248             if self.ref_dump is not None:
249                 name = self.ref_dump
250             else:
251                 name = self.cmd[0]
252             name, ext = os.path.splitext(os.path.basename(name))
253             while ext:
254                 name, ext = os.path.splitext(os.path.basename(name))
255             self.trace_file = os.path.abspath(os.path.join(self.results, name + '.trace'))
256         if os.path.exists(self.trace_file):
257             os.remove(self.trace_file)
258         else:
259             trace_dir = os.path.dirname(self.trace_file)
260             if not os.path.exists(trace_dir):
261                 os.makedirs(trace_dir)
262
263         cmd = self.cmd
264         env = os.environ.copy()
265         
266         system = platform.system()
267         local_wrapper = None
268         if system == 'Windows':
269             wrapper = _get_build_path('wrappers/opengl32.dll')
270             local_wrapper = os.path.join(os.path.dirname(self.cmd[0]), os.path.basename(wrapper))
271             shutil.copy(wrapper, local_wrapper)
272             env['TRACE_FILE'] = str(self.trace_file)
273         else:
274             apitrace = _get_build_program('apitrace')
275             cmd = [
276                 apitrace, 'trace', 
277                 '--api', self.api_map[self.api],
278                 '--output', self.trace_file,
279                 '--'
280             ] + cmd
281         if self.max_frames is not None:
282             env['TRACE_FRAMES'] = str(self.max_frames)
283
284         try:
285             p = popen(cmd, env=env, cwd=self.cwd)
286             p.wait()
287         finally:
288             if local_wrapper is not None:
289                 os.remove(local_wrapper)
290
291         if not os.path.exists(self.trace_file):
292             fail('no trace file generated\n')
293     
294     def checkTrace(self):
295         cmd = [_get_build_program('apitrace'), 'dump', '--color=never', self.trace_file]
296         p = popen(cmd, stdout=subprocess.PIPE)
297
298         checker = TraceChecker(p.stdout, self.ref_dump, self.verbose)
299         checker.check()
300         p.wait()
301         if p.returncode != 0:
302             fail('`apitrace dump` returned code %i' % p.returncode)
303
304         self.doubleBuffer = checker.doubleBuffer
305
306         for callNo, refImageFileName in checker.images:
307             self.checkImage(callNo, refImageFileName)
308         for callNo, refStateFileName in checker.states:
309             self.checkState(callNo, refStateFileName)
310
311     def checkImage(self, callNo, refImageFileName):
312         srcImage = self.getImage(callNo)
313         refImage = Image.open(refImageFileName)
314
315         from snapdiff import Comparer
316         comparer = Comparer(refImage, srcImage)
317         precision = comparer.precision()
318         sys.stdout.write('precision of %f bits against %s\n' % (precision, refImageFileName))
319         if precision < self.threshold_precision:
320             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
321             srcImageFileName = prefix + '.src.png'
322             srcImage.save(srcImageFileName)
323             diffImageFileName = prefix + '.diff.png'
324             comparer.write_diff(diffImageFileName)
325             fail('snapshot from call %u does not match %s' % (callNo, refImageFileName))
326
327     def checkState(self, callNo, refStateFileName):
328         srcState = self.getState(callNo)
329         refState = self.getRefState(refStateFileName)
330
331         from jsondiff import Comparer, Differ
332         comparer = Comparer(ignore_added = True)
333         match = comparer.visit(refState, srcState)
334         if not match:
335             prefix = '%s.%u' % (self.getNamePrefix(), callNo)
336             srcStateFileName = prefix + '.src.json'
337             diffStateFileName = prefix + '.diff.json'
338             self.saveState(srcState, srcStateFileName)
339             #diffStateFile = open(diffStateFileName, 'wt')
340             diffStateFile = sys.stdout
341             differ = Differ(diffStateFile, ignore_added = True)
342             differ.visit(refState, srcState)
343             fail('state from call %u does not match %s' % (callNo, refStateFileName))
344
345     def getRefState(self, refStateFileName):
346         stream = open(refStateFileName, 'rt')
347         from jsondiff import load
348         state = load(stream)
349         self.adjustRefState(state)
350         return state
351
352     def getNamePrefix(self):
353         name = os.path.basename(self.ref_dump)
354         try:
355             index = name.index('.')
356         except ValueError:
357             pass
358         else:
359             name = name[:index]
360         return name
361
362     def saveState(self, state, filename):
363         s = json.dumps(state, sort_keys=True, indent=2)
364         open(filename, 'wt').write(s)
365
366     def retrace(self):
367         p = self._retrace()
368         p.wait()
369         if p.returncode != 0:
370             fail('retrace failed with code %i' % (p.returncode))
371
372     def getImage(self, callNo):
373         state = self.getState(callNo)
374         if self.doubleBuffer:
375             attachments = ['GL_BACK', 'GL_BACK_LEFT', 'GL_BACK_RIGHT', 'GL_COLOR_ATTACHMENT0']
376         else:
377             attachments = ['GL_FRONT', 'GL_FRONT_LEFT', 'GL_FRONT_RIGHT', 'GL_COLOR_ATTACHMENT0']
378         imageObj = self.getFramebufferAttachment(state, attachments)
379         data = imageObj['__data__']
380         stream = StringIO(base64.b64decode(data))
381         im = Image.open(stream)
382         im.save('test.png')
383         return im
384
385     def getFramebufferAttachment(self, state, attachments):
386         framebufferObj = state['framebuffer']
387         for attachment in attachments:
388             try:
389                 attachmentObj = framebufferObj[attachment]
390             except KeyError:
391                 pass
392             else:
393                 return attachmentObj
394         raise Exception("no attachment found")
395
396     def getState(self, callNo):
397         try:
398             state = self.stateCache[callNo]
399         except KeyError:
400             pass
401         else:
402             return state
403
404         p = self._retrace(['-D', str(callNo)])
405         state = json.load(p.stdout, strict=False)
406         p.wait()
407         if p.returncode != 0:
408             fail('retrace returned code %i' % (p.returncode))
409
410         self.adjustSrcState(state)
411
412         self.stateCache[callNo] = state
413
414         return state
415
416     def adjustSrcState(self, state):
417         # Do some adjustments on the obtained state to eliminate failures from
418         # bugs/issues outside of apitrace
419
420         try:
421             parameters = state['parameters']
422         except KeyError:
423             return
424
425         # On NVIDIA drivers glGetIntegerv(GL_INDEX_WRITEMASK) returns -1
426         self.replaceState(parameters, 'GL_INDEX_WRITEMASK', 255, -1)
427
428         # On Gallium 
429         if 'Gallium' in parameters['GL_RENDERER'].split():
430             # Gallium drivers have wrong defaults for draw/read buffer state
431             self.replaceState(parameters, 'GL_DRAW_BUFFER', 'GL_BACK_LEFT', 'GL_BACK')
432             self.replaceState(parameters, 'GL_DRAW_BUFFER0', 'GL_BACK_LEFT', 'GL_BACK')
433             self.replaceState(parameters, 'GL_READ_BUFFER', 'GL_BACK_LEFT', 'GL_BACK')
434             self.replaceState(parameters, 'GL_DRAW_BUFFER', 'GL_FRONT_LEFT', 'GL_FRONT')
435             self.replaceState(parameters, 'GL_DRAW_BUFFER0', 'GL_FRONT_LEFT', 'GL_FRONT')
436             self.replaceState(parameters, 'GL_READ_BUFFER', 'GL_FRONT_LEFT', 'GL_FRONT')
437
438     def adjustRefState(self, state):
439         # Do some adjustments on reference state to eliminate failures from
440         # bugs/issues outside of apitrace
441
442         try:
443             parameters = state['parameters']
444         except KeyError:
445             return
446
447         if platform.system() == 'Darwin':
448             # Mac OS X drivers fail on GL_COLOR_SUM
449             # XXX: investigate this
450             self.removeState(parameters, 'GL_COLOR_SUM')
451
452     def replaceState(self, obj, key, srcValue, dstValue):
453         try:
454             value = obj[key]
455         except KeyError:
456             pass
457         else:
458             if value == srcValue:
459                 obj[key] = dstValue
460
461     def removeState(self, obj, key):
462         try:
463             del obj[key]
464         except KeyError:
465             pass
466
467     def _retrace(self, args = None, stdout=subprocess.PIPE):
468         retrace = self.api_map[self.api] + 'retrace'
469         cmd = [_get_build_program(retrace)]
470         if self.doubleBuffer:
471             cmd += ['-db']
472         else:
473             cmd += ['-sb']
474         if args:
475             cmd += args
476         cmd += [self.trace_file]
477         return popen(cmd, stdout=stdout)
478
479     def run(self):
480         self.runApp()
481         self.traceApp()
482         self.checkTrace()
483         self.retrace()
484
485         pass_()
486
487
488 def main():
489     global options
490
491     # Parse command line options
492     optparser = optparse.OptionParser(
493         usage='\n\t%prog [options] -- [TRACE|PROGRAM] ...',
494         version='%%prog')
495     optparser.add_option(
496         '-v', '--verbose',
497         action="store_true",
498         dest="verbose", default=False,
499         help="verbose output")
500     optparser.add_option(
501         '-a', '--api', metavar='API',
502         type='string', dest='api', default='gl',
503         help='api to trace')
504     optparser.add_option(
505         '-B', '--build', metavar='PATH',
506         type='string', dest='build', default='..',
507         help='path to apitrace build')
508     optparser.add_option(
509         '-C', '--directory', metavar='PATH',
510         type='string', dest='cwd', default=None,
511         help='change to directory')
512     optparser.add_option(
513         '-R', '--results', metavar='PATH',
514         type='string', dest='results', default='.',
515         help='results directory [default=%default]')
516     optparser.add_option(
517         '--ref-dump', metavar='PATH',
518         type='string', dest='ref_dump', default=None,
519         help='reference dump')
520
521     (options, args) = optparser.parse_args(sys.argv[1:])
522     if not args:
523         optparser.error('an argument must be specified')
524
525     if not os.path.exists(options.results):
526         os.makedirs(options.results)
527
528     sys.path.insert(0, _get_source_path('scripts'))
529
530     test = TestCase()
531     test.verbose = options.verbose
532
533     if args[0].endswith('.trace'):
534         test.trace_file = args[0]
535     else:
536         test.cmd = args
537     test.cwd = options.cwd
538     test.api = options.api
539     test.ref_dump = options.ref_dump
540     test.results = options.results
541
542     test.run()
543
544
545 if __name__ == '__main__':
546     main()