blob: e1958126950eb2c0603a655cf6eead4df7de5dee [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
José Fonsecab0df9712014-01-21 12:46:49 +000031import math
José Fonseca0b956fd2011-06-04 22:51:45 +010032import optparse
33import os.path
José Fonseca0b956fd2011-06-04 22:51:45 +010034import subprocess
35import platform
36import sys
José Fonseca0b956fd2011-06-04 22:51:45 +010037
José Fonsecabcca5f72011-09-06 00:07:41 +010038from PIL import Image
39
José Fonseca0b956fd2011-06-04 22:51:45 +010040from snapdiff import Comparer
José Fonseca01908962012-03-16 09:56:09 +000041from highlight import AutoHighlighter
José Fonseca0b956fd2011-06-04 22:51:45 +010042import jsondiff
43
44
45# Null file, to use when we're not interested in subprocesses output
46if platform.system() == 'Windows':
José Fonsecadfd413a2013-09-11 18:41:00 +010047 NULL = open('NUL:', 'wb')
José Fonseca0b956fd2011-06-04 22:51:45 +010048else:
José Fonsecadfd413a2013-09-11 18:41:00 +010049 NULL = open('/dev/null', 'wb')
José Fonseca0b956fd2011-06-04 22:51:45 +010050
51
José Fonsecacad91cb2013-05-25 12:14:29 +010052class RetraceRun:
José Fonseca0b956fd2011-06-04 22:51:45 +010053
José Fonsecacad91cb2013-05-25 12:14:29 +010054 def __init__(self, process):
55 self.process = process
56
57 def nextSnapshot(self):
58 image, comment = read_pnm(self.process.stdout)
59 if image is None:
60 return None, None
61
62 callNo = int(comment.strip())
63
64 return image, callNo
65
66 def terminate(self):
67 try:
68 self.process.terminate()
69 except OSError:
70 # Avoid http://bugs.python.org/issue14252
71 pass
72
73
74class Retracer:
75
76 def __init__(self, retraceExe, args, env=None):
77 self.retraceExe = retraceExe
José Fonseca0b956fd2011-06-04 22:51:45 +010078 self.args = args
79 self.env = env
80
José Fonsecacad91cb2013-05-25 12:14:29 +010081 def _retrace(self, args, stdout=subprocess.PIPE):
José Fonseca0b956fd2011-06-04 22:51:45 +010082 cmd = [
José Fonsecacad91cb2013-05-25 12:14:29 +010083 self.retraceExe,
José Fonseca20303032011-10-20 16:12:10 +020084 ] + args + self.args
José Fonseca83508372012-11-27 13:11:21 +000085 if self.env:
86 for name, value in self.env.iteritems():
87 sys.stderr.write('%s=%s ' % (name, value))
88 sys.stderr.write(' '.join(cmd) + '\n')
José Fonseca20303032011-10-20 16:12:10 +020089 try:
José Fonsecacad91cb2013-05-25 12:14:29 +010090 return subprocess.Popen(cmd, env=self.env, stdout=stdout, stderr=NULL)
José Fonseca20303032011-10-20 16:12:10 +020091 except OSError, ex:
92 sys.stderr.write('error: failed to execute %s: %s\n' % (cmd[0], ex.strerror))
93 sys.exit(1)
94
José Fonsecacad91cb2013-05-25 12:14:29 +010095 def retrace(self, args):
96 p = self._retrace([])
97 p.wait()
98 return p.returncode
99
100 def snapshot(self, call_nos):
101 process = self._retrace([
José Fonsecabcca5f72011-09-06 00:07:41 +0100102 '-s', '-',
José Fonsecacad91cb2013-05-25 12:14:29 +0100103 '-S', call_nos,
José Fonseca20303032011-10-20 16:12:10 +0200104 ])
José Fonsecacad91cb2013-05-25 12:14:29 +0100105 return RetraceRun(process)
José Fonseca0b956fd2011-06-04 22:51:45 +0100106
107 def dump_state(self, call_no):
108 '''Get the state dump at the specified call no.'''
109
José Fonseca20303032011-10-20 16:12:10 +0200110 p = self._retrace([
José Fonseca0b956fd2011-06-04 22:51:45 +0100111 '-D', str(call_no),
José Fonseca20303032011-10-20 16:12:10 +0200112 ])
José Fonseca0b956fd2011-06-04 22:51:45 +0100113 state = jsondiff.load(p.stdout)
114 p.wait()
José Fonsecab96ab8e2011-09-06 10:22:56 +0100115 return state.get('parameters', {})
José Fonseca0b956fd2011-06-04 22:51:45 +0100116
José Fonsecad8ea58f2012-02-09 14:35:27 +0000117 def diff_state(self, ref_call_no, src_call_no, stream):
José Fonseca36fa87c2011-09-06 00:15:32 +0100118 '''Compare the state between two calls.'''
119
120 ref_state = self.dump_state(ref_call_no)
121 src_state = self.dump_state(src_call_no)
José Fonsecab96ab8e2011-09-06 10:22:56 +0100122
José Fonsecad8ea58f2012-02-09 14:35:27 +0000123 stream.flush()
124 differ = jsondiff.Differ(stream)
José Fonseca36fa87c2011-09-06 00:15:32 +0100125 differ.visit(ref_state, src_state)
José Fonsecad8ea58f2012-02-09 14:35:27 +0000126 stream.write('\n')
José Fonseca36fa87c2011-09-06 00:15:32 +0100127
José Fonseca0b956fd2011-06-04 22:51:45 +0100128
José Fonsecabcca5f72011-09-06 00:07:41 +0100129def read_pnm(stream):
130 '''Read a PNM from the stream, and return the image object, and the comment.'''
131
132 magic = stream.readline()
133 if not magic:
134 return None, None
José Fonseca0ee87892012-10-30 15:54:04 +0000135 magic = magic.rstrip()
136 if magic == 'P5':
137 channels = 1
José Fonsecadfd413a2013-09-11 18:41:00 +0100138 bytesPerChannel = 1
José Fonseca0ee87892012-10-30 15:54:04 +0000139 mode = 'L'
140 elif magic == 'P6':
141 channels = 3
José Fonsecadfd413a2013-09-11 18:41:00 +0100142 bytesPerChannel = 1
143 mode = 'RGB'
144 elif magic == 'Pf':
145 channels = 1
146 bytesPerChannel = 4
147 mode = 'R'
148 elif magic == 'PF':
149 channels = 3
150 bytesPerChannel = 4
José Fonseca0ee87892012-10-30 15:54:04 +0000151 mode = 'RGB'
José Fonsecad79c9a22013-09-16 14:57:30 +0100152 elif magic == 'PX':
153 channels = 4
154 bytesPerChannel = 4
155 mode = 'RGB'
José Fonseca0ee87892012-10-30 15:54:04 +0000156 else:
157 raise Exception('Unsupported magic `%s`' % magic)
José Fonsecabcca5f72011-09-06 00:07:41 +0100158 comment = ''
159 line = stream.readline()
160 while line.startswith('#'):
161 comment += line[1:]
162 line = stream.readline()
163 width, height = map(int, line.strip().split())
José Fonseca127a7282013-09-16 14:21:39 +0100164 maximum = int(stream.readline().strip())
José Fonsecadfd413a2013-09-11 18:41:00 +0100165 if bytesPerChannel == 1:
José Fonsecadfd413a2013-09-11 18:41:00 +0100166 assert maximum == 255
José Fonseca127a7282013-09-16 14:21:39 +0100167 else:
168 assert maximum == 1
José Fonsecadfd413a2013-09-11 18:41:00 +0100169 data = stream.read(height * width * channels * bytesPerChannel)
José Fonseca127a7282013-09-16 14:21:39 +0100170 if bytesPerChannel == 4:
171 # Image magic only supports single channel floating point images, so
172 # represent the image as numpy arrays
173
174 import numpy
175 pixels = numpy.fromstring(data, dtype=numpy.float32)
176 pixels.resize((height, width, channels))
177 return pixels, comment
José Fonsecadfd413a2013-09-11 18:41:00 +0100178
José Fonseca0ee87892012-10-30 15:54:04 +0000179 image = Image.frombuffer(mode, (width, height), data, 'raw', mode, 0, 1)
José Fonsecabcca5f72011-09-06 00:07:41 +0100180 return image, comment
181
182
José Fonsecab0df9712014-01-21 12:46:49 +0000183def dumpNumpyImage(output, pixels, filename):
José Fonsecaa0708b02013-09-16 14:59:33 +0100184 height, width, channels = pixels.shape
José Fonsecab0df9712014-01-21 12:46:49 +0000185
186 import numpy
187
188 pixels = (pixels*255).clip(0, 255).astype('uint8')
189
190 if 0:
191 # XXX: Doesn't work somehow
192 im = Image.fromarray(pixels)
193 else:
194 # http://code.activestate.com/recipes/577591-conversion-of-pil-image-and-numpy-array/
195 pixels = pixels.reshape(height*width, channels)
196 if channels == 4:
197 mode = 'RGBA'
198 else:
199 if channels < 3:
200 pixels = numpy.c_[arr, 255*numpy.ones((heigth * width, 3 - channels), numpy.uint8)]
201 assert channels == 3
202 mode = 'RGB'
203 im = Image.frombuffer(mode, (width, height), pixels.tostring(), 'raw', mode, 0, 1)
204 im.save(filename)
205
206 if 0:
207 # Dump to stdout
208 for y in range(height):
José Fonsecaa0708b02013-09-16 14:59:33 +0100209 output.write(' ')
José Fonsecab0df9712014-01-21 12:46:49 +0000210 for x in range(width):
211 for c in range(channels):
212 output.write('%0.9g,' % pixels[y, x, c])
213 output.write(' ')
214 output.write('\n')
José Fonsecaa0708b02013-09-16 14:59:33 +0100215
216
José Fonseca0b956fd2011-06-04 22:51:45 +0100217def parse_env(optparser, entries):
218 '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
219
José Fonseca83508372012-11-27 13:11:21 +0000220 if not entries:
221 return None
222
José Fonseca0b956fd2011-06-04 22:51:45 +0100223 env = os.environ.copy()
224 for entry in entries:
225 try:
226 name, var = entry.split('=', 1)
227 except Exception:
228 optparser.error('invalid environment entry %r' % entry)
229 env[name] = var
230 return env
231
232
233def main():
234 '''Main program.
235 '''
236
237 global options
238
239 # Parse command line options
240 optparser = optparse.OptionParser(
241 usage='\n\t%prog [options] -- [glretrace options] <trace>',
242 version='%%prog')
243 optparser.add_option(
244 '-r', '--retrace', metavar='PROGRAM',
245 type='string', dest='retrace', default='glretrace',
246 help='retrace command [default: %default]')
247 optparser.add_option(
José Fonseca83508372012-11-27 13:11:21 +0000248 '--ref-driver', metavar='DRIVER',
249 type='string', dest='ref_driver', default=None,
250 help='force reference driver')
251 optparser.add_option(
252 '--src-driver', metavar='DRIVER',
253 type='string', dest='src_driver', default=None,
254 help='force source driver')
255 optparser.add_option(
256 '--ref-arg', metavar='OPTION',
257 type='string', action='append', dest='ref_args', default=[],
258 help='pass argument to reference retrace')
259 optparser.add_option(
260 '--src-arg', metavar='OPTION',
261 type='string', action='append', dest='src_args', default=[],
262 help='pass argument to source retrace')
263 optparser.add_option(
José Fonseca0b956fd2011-06-04 22:51:45 +0100264 '--ref-env', metavar='NAME=VALUE',
265 type='string', action='append', dest='ref_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100266 help='add variable to reference environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100267 optparser.add_option(
268 '--src-env', metavar='NAME=VALUE',
269 type='string', action='append', dest='src_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100270 help='add variable to source environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100271 optparser.add_option(
272 '--diff-prefix', metavar='PATH',
273 type='string', dest='diff_prefix', default='.',
José Fonsecabcca5f72011-09-06 00:07:41 +0100274 help='prefix for the difference images')
José Fonseca0b956fd2011-06-04 22:51:45 +0100275 optparser.add_option(
276 '-t', '--threshold', metavar='BITS',
277 type="float", dest="threshold", default=12.0,
278 help="threshold precision [default: %default]")
279 optparser.add_option(
José Fonseca225193d2012-01-26 19:08:32 +0000280 '-S', '--snapshot-frequency', metavar='CALLSET',
José Fonseca0b956fd2011-06-04 22:51:45 +0100281 type="string", dest="snapshot_frequency", default='draw',
José Fonseca225193d2012-01-26 19:08:32 +0000282 help="calls to compare [default: %default]")
José Fonsecad8ea58f2012-02-09 14:35:27 +0000283 optparser.add_option(
José Fonseca0ef175f2013-08-22 17:39:11 +0100284 '--diff-state',
285 action='store_true', dest='diff_state', default=False,
286 help='diff state between failing calls')
287 optparser.add_option(
José Fonsecad8ea58f2012-02-09 14:35:27 +0000288 '-o', '--output', metavar='FILE',
289 type="string", dest="output",
290 help="output file [default: stdout]")
José Fonseca0b956fd2011-06-04 22:51:45 +0100291
292 (options, args) = optparser.parse_args(sys.argv[1:])
293 ref_env = parse_env(optparser, options.ref_env)
294 src_env = parse_env(optparser, options.src_env)
295 if not args:
296 optparser.error("incorrect number of arguments")
José Fonseca83508372012-11-27 13:11:21 +0000297
298 if options.ref_driver:
299 options.ref_args.insert(0, '--driver=' + options.ref_driver)
300 if options.src_driver:
301 options.src_args.insert(0, '--driver=' + options.src_driver)
José Fonseca0b956fd2011-06-04 22:51:45 +0100302
José Fonsecacad91cb2013-05-25 12:14:29 +0100303 refRetracer = Retracer(options.retrace, options.ref_args + args, ref_env)
304 srcRetracer = Retracer(options.retrace, options.src_args + args, src_env)
José Fonseca0b956fd2011-06-04 22:51:45 +0100305
José Fonsecad8ea58f2012-02-09 14:35:27 +0000306 if options.output:
307 output = open(options.output, 'wt')
308 else:
309 output = sys.stdout
310
José Fonseca01908962012-03-16 09:56:09 +0000311 highligher = AutoHighlighter(output)
José Fonsecab96ab8e2011-09-06 10:22:56 +0100312
313 highligher.write('call\tprecision\n')
José Fonseca0b956fd2011-06-04 22:51:45 +0100314
José Fonseca0b956fd2011-06-04 22:51:45 +0100315 last_bad = -1
José Fonseca36fa87c2011-09-06 00:15:32 +0100316 last_good = 0
José Fonsecacad91cb2013-05-25 12:14:29 +0100317 refRun = refRetracer.snapshot(options.snapshot_frequency)
José Fonseca0b956fd2011-06-04 22:51:45 +0100318 try:
José Fonsecacad91cb2013-05-25 12:14:29 +0100319 srcRun = srcRetracer.snapshot(options.snapshot_frequency)
José Fonseca0b956fd2011-06-04 22:51:45 +0100320 try:
José Fonsecabcca5f72011-09-06 00:07:41 +0100321 while True:
322 # Get the reference image
José Fonsecacad91cb2013-05-25 12:14:29 +0100323 refImage, refCallNo = refRun.nextSnapshot()
324 if refImage is None:
José Fonsecabcca5f72011-09-06 00:07:41 +0100325 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100326
José Fonsecabcca5f72011-09-06 00:07:41 +0100327 # Get the source image
José Fonsecacad91cb2013-05-25 12:14:29 +0100328 srcImage, srcCallNo = srcRun.nextSnapshot()
329 if srcImage is None:
José Fonsecabcca5f72011-09-06 00:07:41 +0100330 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100331
José Fonsecacad91cb2013-05-25 12:14:29 +0100332 assert refCallNo == srcCallNo
333 callNo = refCallNo
José Fonsecabcca5f72011-09-06 00:07:41 +0100334
335 # Compare the two images
José Fonseca127a7282013-09-16 14:21:39 +0100336 if isinstance(refImage, Image.Image) and isinstance(srcImage, Image.Image):
337 # Using PIL
338 numpyImages = False
339 comparer = Comparer(refImage, srcImage)
340 precision = comparer.precision()
341 else:
342 # Using numpy (for floating point images)
343 # TODO: drop PIL when numpy path becomes general enough
344 import numpy
345 assert not isinstance(refImage, Image.Image)
346 assert not isinstance(srcImage, Image.Image)
347 numpyImages = True
348 assert refImage.shape == srcImage.shape
349 diffImage = numpy.square(srcImage - refImage)
José Fonsecadd706f42014-01-21 12:46:32 +0000350
351 height, width, channels = diffImage.shape
352 square_error = numpy.sum(diffImage)
353 rel_error = square_error / float(height*width*channels)
354 bits = -math.log(rel_error)/math.log(2.0)
355 precision = bits
José Fonsecabcca5f72011-09-06 00:07:41 +0100356
José Fonsecab96ab8e2011-09-06 10:22:56 +0100357 mismatch = precision < options.threshold
José Fonsecabcca5f72011-09-06 00:07:41 +0100358
José Fonsecab96ab8e2011-09-06 10:22:56 +0100359 if mismatch:
360 highligher.color(highligher.red)
361 highligher.bold()
José Fonsecacad91cb2013-05-25 12:14:29 +0100362 highligher.write('%u\t%f\n' % (callNo, precision))
José Fonsecab96ab8e2011-09-06 10:22:56 +0100363 if mismatch:
364 highligher.normal()
365
366 if mismatch:
José Fonsecab0df9712014-01-21 12:46:49 +0000367 if options.diff_prefix:
José Fonsecacad91cb2013-05-25 12:14:29 +0100368 prefix = os.path.join(options.diff_prefix, '%010u' % callNo)
José Fonsecabcca5f72011-09-06 00:07:41 +0100369 prefix_dir = os.path.dirname(prefix)
370 if not os.path.isdir(prefix_dir):
371 os.makedirs(prefix_dir)
José Fonsecab0df9712014-01-21 12:46:49 +0000372 if numpyImages:
373 dumpNumpyImage(output, refImage, prefix + '.ref.png')
374 dumpNumpyImage(output, srcImage, prefix + '.src.png')
375 else:
376 refImage.save(prefix + '.ref.png')
377 srcImage.save(prefix + '.src.png')
378 comparer.write_diff(prefix + '.diff.png')
José Fonseca0ef175f2013-08-22 17:39:11 +0100379 if last_bad < last_good and options.diff_state:
José Fonsecacad91cb2013-05-25 12:14:29 +0100380 srcRetracer.diff_state(last_good, callNo, output)
381 last_bad = callNo
José Fonsecabcca5f72011-09-06 00:07:41 +0100382 else:
José Fonsecacad91cb2013-05-25 12:14:29 +0100383 last_good = callNo
José Fonsecabcca5f72011-09-06 00:07:41 +0100384
José Fonsecab96ab8e2011-09-06 10:22:56 +0100385 highligher.flush()
José Fonseca0b956fd2011-06-04 22:51:45 +0100386 finally:
José Fonsecacad91cb2013-05-25 12:14:29 +0100387 srcRun.terminate()
José Fonseca0b956fd2011-06-04 22:51:45 +0100388 finally:
José Fonsecacad91cb2013-05-25 12:14:29 +0100389 refRun.terminate()
José Fonseca0b956fd2011-06-04 22:51:45 +0100390
391
392if __name__ == '__main__':
393 main()