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