blob: 71e9f06473d38f38dd2e9f8259ad1b1104b10bef [file] [log] [blame]
José Fonseca0b956fd2011-06-04 22:51:45 +01001#!/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'''Run two retrace instances in parallel, comparing generated snapshots.
28'''
29
30
31import optparse
32import os.path
José Fonseca0b956fd2011-06-04 22:51:45 +010033import subprocess
34import platform
35import sys
José Fonseca0b956fd2011-06-04 22:51:45 +010036
José Fonsecabcca5f72011-09-06 00:07:41 +010037from PIL import Image
38
José Fonseca0b956fd2011-06-04 22:51:45 +010039from snapdiff import Comparer
José Fonseca01908962012-03-16 09:56:09 +000040from highlight import AutoHighlighter
José Fonseca0b956fd2011-06-04 22:51:45 +010041import jsondiff
42
43
44# Null file, to use when we're not interested in subprocesses output
45if platform.system() == 'Windows':
José Fonsecadfd413a2013-09-11 18:41:00 +010046 NULL = open('NUL:', 'wb')
José Fonseca0b956fd2011-06-04 22:51:45 +010047else:
José Fonsecadfd413a2013-09-11 18:41:00 +010048 NULL = open('/dev/null', 'wb')
José Fonseca0b956fd2011-06-04 22:51:45 +010049
50
José Fonsecacad91cb2013-05-25 12:14:29 +010051class RetraceRun:
José Fonseca0b956fd2011-06-04 22:51:45 +010052
José Fonsecacad91cb2013-05-25 12:14:29 +010053 def __init__(self, process):
54 self.process = process
55
56 def nextSnapshot(self):
57 image, comment = read_pnm(self.process.stdout)
58 if image is None:
59 return None, None
60
61 callNo = int(comment.strip())
62
63 return image, callNo
64
65 def terminate(self):
66 try:
67 self.process.terminate()
68 except OSError:
69 # Avoid http://bugs.python.org/issue14252
70 pass
71
72
73class Retracer:
74
75 def __init__(self, retraceExe, args, env=None):
76 self.retraceExe = retraceExe
José Fonseca0b956fd2011-06-04 22:51:45 +010077 self.args = args
78 self.env = env
79
José Fonsecacad91cb2013-05-25 12:14:29 +010080 def _retrace(self, args, stdout=subprocess.PIPE):
José Fonseca0b956fd2011-06-04 22:51:45 +010081 cmd = [
José Fonsecacad91cb2013-05-25 12:14:29 +010082 self.retraceExe,
José Fonseca20303032011-10-20 16:12:10 +020083 ] + args + self.args
José Fonseca83508372012-11-27 13:11:21 +000084 if self.env:
85 for name, value in self.env.iteritems():
86 sys.stderr.write('%s=%s ' % (name, value))
87 sys.stderr.write(' '.join(cmd) + '\n')
José Fonseca20303032011-10-20 16:12:10 +020088 try:
José Fonsecacad91cb2013-05-25 12:14:29 +010089 return subprocess.Popen(cmd, env=self.env, stdout=stdout, stderr=NULL)
José Fonseca20303032011-10-20 16:12:10 +020090 except OSError, ex:
91 sys.stderr.write('error: failed to execute %s: %s\n' % (cmd[0], ex.strerror))
92 sys.exit(1)
93
José Fonsecacad91cb2013-05-25 12:14:29 +010094 def retrace(self, args):
95 p = self._retrace([])
96 p.wait()
97 return p.returncode
98
99 def snapshot(self, call_nos):
100 process = self._retrace([
José Fonsecabcca5f72011-09-06 00:07:41 +0100101 '-s', '-',
José Fonsecacad91cb2013-05-25 12:14:29 +0100102 '-S', call_nos,
José Fonseca20303032011-10-20 16:12:10 +0200103 ])
José Fonsecacad91cb2013-05-25 12:14:29 +0100104 return RetraceRun(process)
José Fonseca0b956fd2011-06-04 22:51:45 +0100105
106 def dump_state(self, call_no):
107 '''Get the state dump at the specified call no.'''
108
José Fonseca20303032011-10-20 16:12:10 +0200109 p = self._retrace([
José Fonseca0b956fd2011-06-04 22:51:45 +0100110 '-D', str(call_no),
José Fonseca20303032011-10-20 16:12:10 +0200111 ])
José Fonseca0b956fd2011-06-04 22:51:45 +0100112 state = jsondiff.load(p.stdout)
113 p.wait()
José Fonsecab96ab8e2011-09-06 10:22:56 +0100114 return state.get('parameters', {})
José Fonseca0b956fd2011-06-04 22:51:45 +0100115
José Fonsecad8ea58f2012-02-09 14:35:27 +0000116 def diff_state(self, ref_call_no, src_call_no, stream):
José Fonseca36fa87c2011-09-06 00:15:32 +0100117 '''Compare the state between two calls.'''
118
119 ref_state = self.dump_state(ref_call_no)
120 src_state = self.dump_state(src_call_no)
José Fonsecab96ab8e2011-09-06 10:22:56 +0100121
José Fonsecad8ea58f2012-02-09 14:35:27 +0000122 stream.flush()
123 differ = jsondiff.Differ(stream)
José Fonseca36fa87c2011-09-06 00:15:32 +0100124 differ.visit(ref_state, src_state)
José Fonsecad8ea58f2012-02-09 14:35:27 +0000125 stream.write('\n')
José Fonseca36fa87c2011-09-06 00:15:32 +0100126
José Fonseca0b956fd2011-06-04 22:51:45 +0100127
José Fonsecabcca5f72011-09-06 00:07:41 +0100128def read_pnm(stream):
129 '''Read a PNM from the stream, and return the image object, and the comment.'''
130
131 magic = stream.readline()
132 if not magic:
133 return None, None
José Fonseca0ee87892012-10-30 15:54:04 +0000134 magic = magic.rstrip()
135 if magic == 'P5':
136 channels = 1
José Fonsecadfd413a2013-09-11 18:41:00 +0100137 bytesPerChannel = 1
José Fonseca0ee87892012-10-30 15:54:04 +0000138 mode = 'L'
139 elif magic == 'P6':
140 channels = 3
José Fonsecadfd413a2013-09-11 18:41:00 +0100141 bytesPerChannel = 1
142 mode = 'RGB'
143 elif magic == 'Pf':
144 channels = 1
145 bytesPerChannel = 4
146 mode = 'R'
147 elif magic == 'PF':
148 channels = 3
149 bytesPerChannel = 4
José Fonseca0ee87892012-10-30 15:54:04 +0000150 mode = 'RGB'
José Fonsecad79c9a22013-09-16 14:57:30 +0100151 elif magic == 'PX':
152 channels = 4
153 bytesPerChannel = 4
154 mode = 'RGB'
José Fonseca0ee87892012-10-30 15:54:04 +0000155 else:
156 raise Exception('Unsupported magic `%s`' % magic)
José Fonsecabcca5f72011-09-06 00:07:41 +0100157 comment = ''
158 line = stream.readline()
159 while line.startswith('#'):
160 comment += line[1:]
161 line = stream.readline()
162 width, height = map(int, line.strip().split())
José Fonseca127a7282013-09-16 14:21:39 +0100163 maximum = int(stream.readline().strip())
José Fonsecadfd413a2013-09-11 18:41:00 +0100164 if bytesPerChannel == 1:
José Fonsecadfd413a2013-09-11 18:41:00 +0100165 assert maximum == 255
José Fonseca127a7282013-09-16 14:21:39 +0100166 else:
167 assert maximum == 1
José Fonsecadfd413a2013-09-11 18:41:00 +0100168 data = stream.read(height * width * channels * bytesPerChannel)
José Fonseca127a7282013-09-16 14:21:39 +0100169 if bytesPerChannel == 4:
170 # Image magic only supports single channel floating point images, so
171 # represent the image as numpy arrays
172
173 import numpy
174 pixels = numpy.fromstring(data, dtype=numpy.float32)
175 pixels.resize((height, width, channels))
176 return pixels, comment
José Fonsecadfd413a2013-09-11 18:41:00 +0100177
José Fonseca0ee87892012-10-30 15:54:04 +0000178 image = Image.frombuffer(mode, (width, height), data, 'raw', mode, 0, 1)
José Fonsecabcca5f72011-09-06 00:07:41 +0100179 return image, comment
180
181
José Fonseca0b956fd2011-06-04 22:51:45 +0100182def parse_env(optparser, entries):
183 '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
184
José Fonseca83508372012-11-27 13:11:21 +0000185 if not entries:
186 return None
187
José Fonseca0b956fd2011-06-04 22:51:45 +0100188 env = os.environ.copy()
189 for entry in entries:
190 try:
191 name, var = entry.split('=', 1)
192 except Exception:
193 optparser.error('invalid environment entry %r' % entry)
194 env[name] = var
195 return env
196
197
198def main():
199 '''Main program.
200 '''
201
202 global options
203
204 # Parse command line options
205 optparser = optparse.OptionParser(
206 usage='\n\t%prog [options] -- [glretrace options] <trace>',
207 version='%%prog')
208 optparser.add_option(
209 '-r', '--retrace', metavar='PROGRAM',
210 type='string', dest='retrace', default='glretrace',
211 help='retrace command [default: %default]')
212 optparser.add_option(
José Fonseca83508372012-11-27 13:11:21 +0000213 '--ref-driver', metavar='DRIVER',
214 type='string', dest='ref_driver', default=None,
215 help='force reference driver')
216 optparser.add_option(
217 '--src-driver', metavar='DRIVER',
218 type='string', dest='src_driver', default=None,
219 help='force source driver')
220 optparser.add_option(
221 '--ref-arg', metavar='OPTION',
222 type='string', action='append', dest='ref_args', default=[],
223 help='pass argument to reference retrace')
224 optparser.add_option(
225 '--src-arg', metavar='OPTION',
226 type='string', action='append', dest='src_args', default=[],
227 help='pass argument to source retrace')
228 optparser.add_option(
José Fonseca0b956fd2011-06-04 22:51:45 +0100229 '--ref-env', metavar='NAME=VALUE',
230 type='string', action='append', dest='ref_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100231 help='add variable to reference environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100232 optparser.add_option(
233 '--src-env', metavar='NAME=VALUE',
234 type='string', action='append', dest='src_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100235 help='add variable to source environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100236 optparser.add_option(
237 '--diff-prefix', metavar='PATH',
238 type='string', dest='diff_prefix', default='.',
José Fonsecabcca5f72011-09-06 00:07:41 +0100239 help='prefix for the difference images')
José Fonseca0b956fd2011-06-04 22:51:45 +0100240 optparser.add_option(
241 '-t', '--threshold', metavar='BITS',
242 type="float", dest="threshold", default=12.0,
243 help="threshold precision [default: %default]")
244 optparser.add_option(
José Fonseca225193d2012-01-26 19:08:32 +0000245 '-S', '--snapshot-frequency', metavar='CALLSET',
José Fonseca0b956fd2011-06-04 22:51:45 +0100246 type="string", dest="snapshot_frequency", default='draw',
José Fonseca225193d2012-01-26 19:08:32 +0000247 help="calls to compare [default: %default]")
José Fonsecad8ea58f2012-02-09 14:35:27 +0000248 optparser.add_option(
José Fonseca0ef175f2013-08-22 17:39:11 +0100249 '--diff-state',
250 action='store_true', dest='diff_state', default=False,
251 help='diff state between failing calls')
252 optparser.add_option(
José Fonsecad8ea58f2012-02-09 14:35:27 +0000253 '-o', '--output', metavar='FILE',
254 type="string", dest="output",
255 help="output file [default: stdout]")
José Fonseca0b956fd2011-06-04 22:51:45 +0100256
257 (options, args) = optparser.parse_args(sys.argv[1:])
258 ref_env = parse_env(optparser, options.ref_env)
259 src_env = parse_env(optparser, options.src_env)
260 if not args:
261 optparser.error("incorrect number of arguments")
José Fonseca83508372012-11-27 13:11:21 +0000262
263 if options.ref_driver:
264 options.ref_args.insert(0, '--driver=' + options.ref_driver)
265 if options.src_driver:
266 options.src_args.insert(0, '--driver=' + options.src_driver)
José Fonseca0b956fd2011-06-04 22:51:45 +0100267
José Fonsecacad91cb2013-05-25 12:14:29 +0100268 refRetracer = Retracer(options.retrace, options.ref_args + args, ref_env)
269 srcRetracer = Retracer(options.retrace, options.src_args + args, src_env)
José Fonseca0b956fd2011-06-04 22:51:45 +0100270
José Fonsecad8ea58f2012-02-09 14:35:27 +0000271 if options.output:
272 output = open(options.output, 'wt')
273 else:
274 output = sys.stdout
275
José Fonseca01908962012-03-16 09:56:09 +0000276 highligher = AutoHighlighter(output)
José Fonsecab96ab8e2011-09-06 10:22:56 +0100277
278 highligher.write('call\tprecision\n')
José Fonseca0b956fd2011-06-04 22:51:45 +0100279
José Fonseca0b956fd2011-06-04 22:51:45 +0100280 last_bad = -1
José Fonseca36fa87c2011-09-06 00:15:32 +0100281 last_good = 0
José Fonsecacad91cb2013-05-25 12:14:29 +0100282 refRun = refRetracer.snapshot(options.snapshot_frequency)
José Fonseca0b956fd2011-06-04 22:51:45 +0100283 try:
José Fonsecacad91cb2013-05-25 12:14:29 +0100284 srcRun = srcRetracer.snapshot(options.snapshot_frequency)
José Fonseca0b956fd2011-06-04 22:51:45 +0100285 try:
José Fonsecabcca5f72011-09-06 00:07:41 +0100286 while True:
287 # Get the reference image
José Fonsecacad91cb2013-05-25 12:14:29 +0100288 refImage, refCallNo = refRun.nextSnapshot()
289 if refImage is None:
José Fonsecabcca5f72011-09-06 00:07:41 +0100290 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100291
José Fonsecabcca5f72011-09-06 00:07:41 +0100292 # Get the source image
José Fonsecacad91cb2013-05-25 12:14:29 +0100293 srcImage, srcCallNo = srcRun.nextSnapshot()
294 if srcImage is None:
José Fonsecabcca5f72011-09-06 00:07:41 +0100295 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100296
José Fonsecacad91cb2013-05-25 12:14:29 +0100297 assert refCallNo == srcCallNo
298 callNo = refCallNo
José Fonsecabcca5f72011-09-06 00:07:41 +0100299
300 # Compare the two images
José Fonseca127a7282013-09-16 14:21:39 +0100301 if isinstance(refImage, Image.Image) and isinstance(srcImage, Image.Image):
302 # Using PIL
303 numpyImages = False
304 comparer = Comparer(refImage, srcImage)
305 precision = comparer.precision()
306 else:
307 # Using numpy (for floating point images)
308 # TODO: drop PIL when numpy path becomes general enough
309 import numpy
310 assert not isinstance(refImage, Image.Image)
311 assert not isinstance(srcImage, Image.Image)
312 numpyImages = True
313 assert refImage.shape == srcImage.shape
314 diffImage = numpy.square(srcImage - refImage)
315 match = numpy.all(diffImage == 0)
316 if match:
317 precision = 24
318 else:
319 precision = 0
José Fonsecabcca5f72011-09-06 00:07:41 +0100320
José Fonsecab96ab8e2011-09-06 10:22:56 +0100321 mismatch = precision < options.threshold
José Fonsecabcca5f72011-09-06 00:07:41 +0100322
José Fonsecab96ab8e2011-09-06 10:22:56 +0100323 if mismatch:
324 highligher.color(highligher.red)
325 highligher.bold()
José Fonsecacad91cb2013-05-25 12:14:29 +0100326 highligher.write('%u\t%f\n' % (callNo, precision))
José Fonsecab96ab8e2011-09-06 10:22:56 +0100327 if mismatch:
328 highligher.normal()
329
330 if mismatch:
José Fonseca127a7282013-09-16 14:21:39 +0100331 if options.diff_prefix and not numpyImages:
José Fonsecacad91cb2013-05-25 12:14:29 +0100332 prefix = os.path.join(options.diff_prefix, '%010u' % callNo)
José Fonsecabcca5f72011-09-06 00:07:41 +0100333 prefix_dir = os.path.dirname(prefix)
334 if not os.path.isdir(prefix_dir):
335 os.makedirs(prefix_dir)
José Fonsecacad91cb2013-05-25 12:14:29 +0100336 refImage.save(prefix + '.ref.png')
337 srcImage.save(prefix + '.src.png')
José Fonsecabcca5f72011-09-06 00:07:41 +0100338 comparer.write_diff(prefix + '.diff.png')
José Fonseca0ef175f2013-08-22 17:39:11 +0100339 if last_bad < last_good and options.diff_state:
José Fonsecacad91cb2013-05-25 12:14:29 +0100340 srcRetracer.diff_state(last_good, callNo, output)
341 last_bad = callNo
José Fonsecabcca5f72011-09-06 00:07:41 +0100342 else:
José Fonsecacad91cb2013-05-25 12:14:29 +0100343 last_good = callNo
José Fonsecabcca5f72011-09-06 00:07:41 +0100344
José Fonsecab96ab8e2011-09-06 10:22:56 +0100345 highligher.flush()
José Fonseca0b956fd2011-06-04 22:51:45 +0100346 finally:
José Fonsecacad91cb2013-05-25 12:14:29 +0100347 srcRun.terminate()
José Fonseca0b956fd2011-06-04 22:51:45 +0100348 finally:
José Fonsecacad91cb2013-05-25 12:14:29 +0100349 refRun.terminate()
José Fonseca0b956fd2011-06-04 22:51:45 +0100350
351
352if __name__ == '__main__':
353 main()