blob: b7f64f3d3bddd77575011061f0f5cdb8a6a7a772 [file] [log] [blame]
Jose Fonseca247e1fa2019-04-28 14:14:44 +01001#!/usr/bin/env python3
José Fonseca0b956fd2011-06-04 22:51:45 +01002##########################################################################
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
Jose Fonseca7247d8d2016-02-17 15:37:28 +000046NULL = open(os.path.devnull, 'wb')
José Fonseca0b956fd2011-06-04 22:51:45 +010047
48
José Fonsecacad91cb2013-05-25 12:14:29 +010049class RetraceRun:
José Fonseca0b956fd2011-06-04 22:51:45 +010050
José Fonsecacad91cb2013-05-25 12:14:29 +010051 def __init__(self, process):
52 self.process = process
53
54 def nextSnapshot(self):
55 image, comment = read_pnm(self.process.stdout)
56 if image is None:
57 return None, None
58
59 callNo = int(comment.strip())
60
61 return image, callNo
62
63 def terminate(self):
64 try:
65 self.process.terminate()
66 except OSError:
67 # Avoid http://bugs.python.org/issue14252
68 pass
69
70
71class Retracer:
72
73 def __init__(self, retraceExe, args, env=None):
74 self.retraceExe = retraceExe
José Fonseca0b956fd2011-06-04 22:51:45 +010075 self.args = args
76 self.env = env
77
José Fonsecacad91cb2013-05-25 12:14:29 +010078 def _retrace(self, args, stdout=subprocess.PIPE):
José Fonseca0b956fd2011-06-04 22:51:45 +010079 cmd = [
José Fonsecacad91cb2013-05-25 12:14:29 +010080 self.retraceExe,
José Fonseca20303032011-10-20 16:12:10 +020081 ] + args + self.args
José Fonseca83508372012-11-27 13:11:21 +000082 if self.env:
Piotr Podsiadły0b8b0192019-01-03 20:39:55 +010083 for name, value in self.env.items():
José Fonseca83508372012-11-27 13:11:21 +000084 sys.stderr.write('%s=%s ' % (name, value))
85 sys.stderr.write(' '.join(cmd) + '\n')
José Fonseca20303032011-10-20 16:12:10 +020086 try:
José Fonsecacad91cb2013-05-25 12:14:29 +010087 return subprocess.Popen(cmd, env=self.env, stdout=stdout, stderr=NULL)
Piotr Podsiadły0b8b0192019-01-03 20:39:55 +010088 except OSError as ex:
José Fonseca20303032011-10-20 16:12:10 +020089 sys.stderr.write('error: failed to execute %s: %s\n' % (cmd[0], ex.strerror))
90 sys.exit(1)
91
José Fonsecacad91cb2013-05-25 12:14:29 +010092 def retrace(self, args):
93 p = self._retrace([])
94 p.wait()
95 return p.returncode
96
97 def snapshot(self, call_nos):
98 process = self._retrace([
José Fonsecabcca5f72011-09-06 00:07:41 +010099 '-s', '-',
José Fonsecacad91cb2013-05-25 12:14:29 +0100100 '-S', call_nos,
José Fonseca20303032011-10-20 16:12:10 +0200101 ])
José Fonsecacad91cb2013-05-25 12:14:29 +0100102 return RetraceRun(process)
José Fonseca0b956fd2011-06-04 22:51:45 +0100103
104 def dump_state(self, call_no):
105 '''Get the state dump at the specified call no.'''
106
José Fonseca20303032011-10-20 16:12:10 +0200107 p = self._retrace([
José Fonseca0b956fd2011-06-04 22:51:45 +0100108 '-D', str(call_no),
José Fonseca20303032011-10-20 16:12:10 +0200109 ])
José Fonseca0b956fd2011-06-04 22:51:45 +0100110 state = jsondiff.load(p.stdout)
111 p.wait()
José Fonsecab96ab8e2011-09-06 10:22:56 +0100112 return state.get('parameters', {})
José Fonseca0b956fd2011-06-04 22:51:45 +0100113
José Fonsecad8ea58f2012-02-09 14:35:27 +0000114 def diff_state(self, ref_call_no, src_call_no, stream):
José Fonseca36fa87c2011-09-06 00:15:32 +0100115 '''Compare the state between two calls.'''
116
117 ref_state = self.dump_state(ref_call_no)
118 src_state = self.dump_state(src_call_no)
José Fonsecab96ab8e2011-09-06 10:22:56 +0100119
José Fonsecad8ea58f2012-02-09 14:35:27 +0000120 stream.flush()
121 differ = jsondiff.Differ(stream)
José Fonseca36fa87c2011-09-06 00:15:32 +0100122 differ.visit(ref_state, src_state)
José Fonsecad8ea58f2012-02-09 14:35:27 +0000123 stream.write('\n')
José Fonseca36fa87c2011-09-06 00:15:32 +0100124
José Fonseca0b956fd2011-06-04 22:51:45 +0100125
José Fonsecabcca5f72011-09-06 00:07:41 +0100126def read_pnm(stream):
127 '''Read a PNM from the stream, and return the image object, and the comment.'''
128
129 magic = stream.readline()
130 if not magic:
131 return None, None
José Fonseca0ee87892012-10-30 15:54:04 +0000132 magic = magic.rstrip()
Jose Fonsecada21fd02019-05-22 06:59:18 +0100133 if magic == b'P5':
José Fonseca0ee87892012-10-30 15:54:04 +0000134 channels = 1
José Fonsecadfd413a2013-09-11 18:41:00 +0100135 bytesPerChannel = 1
José Fonseca0ee87892012-10-30 15:54:04 +0000136 mode = 'L'
Jose Fonsecada21fd02019-05-22 06:59:18 +0100137 elif magic == b'P6':
José Fonseca0ee87892012-10-30 15:54:04 +0000138 channels = 3
José Fonsecadfd413a2013-09-11 18:41:00 +0100139 bytesPerChannel = 1
140 mode = 'RGB'
Jose Fonsecada21fd02019-05-22 06:59:18 +0100141 elif magic == b'Pf':
José Fonsecadfd413a2013-09-11 18:41:00 +0100142 channels = 1
143 bytesPerChannel = 4
144 mode = 'R'
Jose Fonsecada21fd02019-05-22 06:59:18 +0100145 elif magic == b'PF':
José Fonsecadfd413a2013-09-11 18:41:00 +0100146 channels = 3
147 bytesPerChannel = 4
José Fonseca0ee87892012-10-30 15:54:04 +0000148 mode = 'RGB'
Jose Fonsecada21fd02019-05-22 06:59:18 +0100149 elif magic == b'PX':
José Fonsecad79c9a22013-09-16 14:57:30 +0100150 channels = 4
151 bytesPerChannel = 4
152 mode = 'RGB'
José Fonseca0ee87892012-10-30 15:54:04 +0000153 else:
Jose Fonsecada21fd02019-05-22 06:59:18 +0100154 raise Exception('Unsupported magic %r' % magic)
155 comment = b''
José Fonsecabcca5f72011-09-06 00:07:41 +0100156 line = stream.readline()
Jose Fonsecada21fd02019-05-22 06:59:18 +0100157 while line.startswith(b'#'):
José Fonsecabcca5f72011-09-06 00:07:41 +0100158 comment += line[1:]
159 line = stream.readline()
Piotr Podsiadły0b8b0192019-01-03 20:39:55 +0100160 width, height = list(map(int, line.strip().split()))
José Fonseca127a7282013-09-16 14:21:39 +0100161 maximum = int(stream.readline().strip())
José Fonsecadfd413a2013-09-11 18:41:00 +0100162 if bytesPerChannel == 1:
José Fonsecadfd413a2013-09-11 18:41:00 +0100163 assert maximum == 255
José Fonseca127a7282013-09-16 14:21:39 +0100164 else:
165 assert maximum == 1
José Fonsecadfd413a2013-09-11 18:41:00 +0100166 data = stream.read(height * width * channels * bytesPerChannel)
José Fonseca127a7282013-09-16 14:21:39 +0100167 if bytesPerChannel == 4:
168 # Image magic only supports single channel floating point images, so
169 # represent the image as numpy arrays
170
171 import numpy
172 pixels = numpy.fromstring(data, dtype=numpy.float32)
173 pixels.resize((height, width, channels))
174 return pixels, comment
José Fonsecadfd413a2013-09-11 18:41:00 +0100175
José Fonseca0ee87892012-10-30 15:54:04 +0000176 image = Image.frombuffer(mode, (width, height), data, 'raw', mode, 0, 1)
José Fonsecabcca5f72011-09-06 00:07:41 +0100177 return image, comment
178
179
José Fonsecab0df9712014-01-21 12:46:49 +0000180def dumpNumpyImage(output, pixels, filename):
José Fonsecaa0708b02013-09-16 14:59:33 +0100181 height, width, channels = pixels.shape
José Fonsecab0df9712014-01-21 12:46:49 +0000182
183 import numpy
184
185 pixels = (pixels*255).clip(0, 255).astype('uint8')
186
187 if 0:
188 # XXX: Doesn't work somehow
189 im = Image.fromarray(pixels)
190 else:
191 # http://code.activestate.com/recipes/577591-conversion-of-pil-image-and-numpy-array/
192 pixels = pixels.reshape(height*width, channels)
193 if channels == 4:
194 mode = 'RGBA'
195 else:
196 if channels < 3:
197 pixels = numpy.c_[arr, 255*numpy.ones((heigth * width, 3 - channels), numpy.uint8)]
198 assert channels == 3
199 mode = 'RGB'
200 im = Image.frombuffer(mode, (width, height), pixels.tostring(), 'raw', mode, 0, 1)
201 im.save(filename)
202
203 if 0:
204 # Dump to stdout
205 for y in range(height):
José Fonsecaa0708b02013-09-16 14:59:33 +0100206 output.write(' ')
José Fonsecab0df9712014-01-21 12:46:49 +0000207 for x in range(width):
208 for c in range(channels):
209 output.write('%0.9g,' % pixels[y, x, c])
210 output.write(' ')
211 output.write('\n')
José Fonsecaa0708b02013-09-16 14:59:33 +0100212
213
José Fonseca0b956fd2011-06-04 22:51:45 +0100214def parse_env(optparser, entries):
215 '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
216
José Fonseca83508372012-11-27 13:11:21 +0000217 if not entries:
218 return None
219
José Fonseca0b956fd2011-06-04 22:51:45 +0100220 env = os.environ.copy()
221 for entry in entries:
222 try:
223 name, var = entry.split('=', 1)
224 except Exception:
225 optparser.error('invalid environment entry %r' % entry)
226 env[name] = var
227 return env
228
229
230def main():
231 '''Main program.
232 '''
233
234 global options
235
236 # Parse command line options
237 optparser = optparse.OptionParser(
238 usage='\n\t%prog [options] -- [glretrace options] <trace>',
239 version='%%prog')
240 optparser.add_option(
241 '-r', '--retrace', metavar='PROGRAM',
242 type='string', dest='retrace', default='glretrace',
243 help='retrace command [default: %default]')
244 optparser.add_option(
José Fonseca83508372012-11-27 13:11:21 +0000245 '--ref-driver', metavar='DRIVER',
246 type='string', dest='ref_driver', default=None,
247 help='force reference driver')
248 optparser.add_option(
249 '--src-driver', metavar='DRIVER',
250 type='string', dest='src_driver', default=None,
251 help='force source driver')
252 optparser.add_option(
253 '--ref-arg', metavar='OPTION',
254 type='string', action='append', dest='ref_args', default=[],
255 help='pass argument to reference retrace')
256 optparser.add_option(
257 '--src-arg', metavar='OPTION',
258 type='string', action='append', dest='src_args', default=[],
259 help='pass argument to source retrace')
260 optparser.add_option(
José Fonseca0b956fd2011-06-04 22:51:45 +0100261 '--ref-env', metavar='NAME=VALUE',
262 type='string', action='append', dest='ref_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100263 help='add variable to reference environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100264 optparser.add_option(
265 '--src-env', metavar='NAME=VALUE',
266 type='string', action='append', dest='src_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100267 help='add variable to source environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100268 optparser.add_option(
269 '--diff-prefix', metavar='PATH',
270 type='string', dest='diff_prefix', default='.',
José Fonsecabcca5f72011-09-06 00:07:41 +0100271 help='prefix for the difference images')
José Fonseca0b956fd2011-06-04 22:51:45 +0100272 optparser.add_option(
273 '-t', '--threshold', metavar='BITS',
274 type="float", dest="threshold", default=12.0,
275 help="threshold precision [default: %default]")
276 optparser.add_option(
José Fonseca225193d2012-01-26 19:08:32 +0000277 '-S', '--snapshot-frequency', metavar='CALLSET',
José Fonseca0b956fd2011-06-04 22:51:45 +0100278 type="string", dest="snapshot_frequency", default='draw',
José Fonseca225193d2012-01-26 19:08:32 +0000279 help="calls to compare [default: %default]")
José Fonsecad8ea58f2012-02-09 14:35:27 +0000280 optparser.add_option(
José Fonseca0ef175f2013-08-22 17:39:11 +0100281 '--diff-state',
282 action='store_true', dest='diff_state', default=False,
283 help='diff state between failing calls')
284 optparser.add_option(
José Fonsecad8ea58f2012-02-09 14:35:27 +0000285 '-o', '--output', metavar='FILE',
286 type="string", dest="output",
287 help="output file [default: stdout]")
José Fonseca0b956fd2011-06-04 22:51:45 +0100288
289 (options, args) = optparser.parse_args(sys.argv[1:])
290 ref_env = parse_env(optparser, options.ref_env)
291 src_env = parse_env(optparser, options.src_env)
292 if not args:
293 optparser.error("incorrect number of arguments")
José Fonseca83508372012-11-27 13:11:21 +0000294
295 if options.ref_driver:
296 options.ref_args.insert(0, '--driver=' + options.ref_driver)
297 if options.src_driver:
298 options.src_args.insert(0, '--driver=' + options.src_driver)
José Fonseca0b956fd2011-06-04 22:51:45 +0100299
José Fonsecacad91cb2013-05-25 12:14:29 +0100300 refRetracer = Retracer(options.retrace, options.ref_args + args, ref_env)
301 srcRetracer = Retracer(options.retrace, options.src_args + args, src_env)
José Fonseca0b956fd2011-06-04 22:51:45 +0100302
José Fonsecad8ea58f2012-02-09 14:35:27 +0000303 if options.output:
304 output = open(options.output, 'wt')
305 else:
306 output = sys.stdout
307
José Fonseca01908962012-03-16 09:56:09 +0000308 highligher = AutoHighlighter(output)
José Fonsecab96ab8e2011-09-06 10:22:56 +0100309
310 highligher.write('call\tprecision\n')
José Fonseca0b956fd2011-06-04 22:51:45 +0100311
José Fonseca0b956fd2011-06-04 22:51:45 +0100312 last_bad = -1
José Fonseca36fa87c2011-09-06 00:15:32 +0100313 last_good = 0
José Fonsecacad91cb2013-05-25 12:14:29 +0100314 refRun = refRetracer.snapshot(options.snapshot_frequency)
José Fonseca0b956fd2011-06-04 22:51:45 +0100315 try:
José Fonsecacad91cb2013-05-25 12:14:29 +0100316 srcRun = srcRetracer.snapshot(options.snapshot_frequency)
José Fonseca0b956fd2011-06-04 22:51:45 +0100317 try:
José Fonsecabcca5f72011-09-06 00:07:41 +0100318 while True:
319 # Get the reference image
José Fonsecacad91cb2013-05-25 12:14:29 +0100320 refImage, refCallNo = refRun.nextSnapshot()
321 if refImage is None:
José Fonsecabcca5f72011-09-06 00:07:41 +0100322 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100323
José Fonsecabcca5f72011-09-06 00:07:41 +0100324 # Get the source image
José Fonsecacad91cb2013-05-25 12:14:29 +0100325 srcImage, srcCallNo = srcRun.nextSnapshot()
326 if srcImage is None:
José Fonsecabcca5f72011-09-06 00:07:41 +0100327 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100328
José Fonsecacad91cb2013-05-25 12:14:29 +0100329 assert refCallNo == srcCallNo
330 callNo = refCallNo
José Fonsecabcca5f72011-09-06 00:07:41 +0100331
332 # Compare the two images
José Fonseca127a7282013-09-16 14:21:39 +0100333 if isinstance(refImage, Image.Image) and isinstance(srcImage, Image.Image):
334 # Using PIL
335 numpyImages = False
336 comparer = Comparer(refImage, srcImage)
337 precision = comparer.precision()
338 else:
339 # Using numpy (for floating point images)
340 # TODO: drop PIL when numpy path becomes general enough
341 import numpy
342 assert not isinstance(refImage, Image.Image)
343 assert not isinstance(srcImage, Image.Image)
344 numpyImages = True
345 assert refImage.shape == srcImage.shape
346 diffImage = numpy.square(srcImage - refImage)
José Fonsecadd706f42014-01-21 12:46:32 +0000347
348 height, width, channels = diffImage.shape
349 square_error = numpy.sum(diffImage)
José Fonseca58aee982014-12-11 19:48:52 +0000350 square_error += numpy.finfo(numpy.float32).eps
José Fonsecadd706f42014-01-21 12:46:32 +0000351 rel_error = square_error / float(height*width*channels)
352 bits = -math.log(rel_error)/math.log(2.0)
353 precision = bits
José Fonsecabcca5f72011-09-06 00:07:41 +0100354
José Fonsecab96ab8e2011-09-06 10:22:56 +0100355 mismatch = precision < options.threshold
José Fonsecabcca5f72011-09-06 00:07:41 +0100356
José Fonsecab96ab8e2011-09-06 10:22:56 +0100357 if mismatch:
358 highligher.color(highligher.red)
359 highligher.bold()
José Fonsecacad91cb2013-05-25 12:14:29 +0100360 highligher.write('%u\t%f\n' % (callNo, precision))
José Fonsecab96ab8e2011-09-06 10:22:56 +0100361 if mismatch:
362 highligher.normal()
363
364 if mismatch:
José Fonsecab0df9712014-01-21 12:46:49 +0000365 if options.diff_prefix:
José Fonsecacad91cb2013-05-25 12:14:29 +0100366 prefix = os.path.join(options.diff_prefix, '%010u' % callNo)
José Fonsecabcca5f72011-09-06 00:07:41 +0100367 prefix_dir = os.path.dirname(prefix)
368 if not os.path.isdir(prefix_dir):
369 os.makedirs(prefix_dir)
José Fonsecab0df9712014-01-21 12:46:49 +0000370 if numpyImages:
371 dumpNumpyImage(output, refImage, prefix + '.ref.png')
372 dumpNumpyImage(output, srcImage, prefix + '.src.png')
373 else:
374 refImage.save(prefix + '.ref.png')
375 srcImage.save(prefix + '.src.png')
376 comparer.write_diff(prefix + '.diff.png')
José Fonseca0ef175f2013-08-22 17:39:11 +0100377 if last_bad < last_good and options.diff_state:
José Fonsecacad91cb2013-05-25 12:14:29 +0100378 srcRetracer.diff_state(last_good, callNo, output)
379 last_bad = callNo
José Fonsecabcca5f72011-09-06 00:07:41 +0100380 else:
José Fonsecacad91cb2013-05-25 12:14:29 +0100381 last_good = callNo
José Fonsecabcca5f72011-09-06 00:07:41 +0100382
José Fonsecab96ab8e2011-09-06 10:22:56 +0100383 highligher.flush()
José Fonseca0b956fd2011-06-04 22:51:45 +0100384 finally:
José Fonsecacad91cb2013-05-25 12:14:29 +0100385 srcRun.terminate()
José Fonseca0b956fd2011-06-04 22:51:45 +0100386 finally:
José Fonsecacad91cb2013-05-25 12:14:29 +0100387 refRun.terminate()
José Fonseca0b956fd2011-06-04 22:51:45 +0100388
389
390if __name__ == '__main__':
391 main()