2 ##########################################################################
4 # Copyright 2011 Jose Fonseca
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:
14 # The above copyright notice and this permission notice shall be included in
15 # all copies or substantial portions of the Software.
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
25 ##########################################################################/
27 '''Run two retrace instances in parallel, comparing generated snapshots.
39 from snapdiff import Comparer
40 from highlight import AutoHighlighter
44 # Null file, to use when we're not interested in subprocesses output
45 if platform.system() == 'Windows':
46 NULL = open('NUL:', 'wb')
48 NULL = open('/dev/null', 'wb')
53 def __init__(self, process):
54 self.process = process
56 def nextSnapshot(self):
57 image, comment = read_pnm(self.process.stdout)
61 callNo = int(comment.strip())
67 self.process.terminate()
69 # Avoid http://bugs.python.org/issue14252
75 def __init__(self, retraceExe, args, env=None):
76 self.retraceExe = retraceExe
80 def _retrace(self, args, stdout=subprocess.PIPE):
85 for name, value in self.env.iteritems():
86 sys.stderr.write('%s=%s ' % (name, value))
87 sys.stderr.write(' '.join(cmd) + '\n')
89 return subprocess.Popen(cmd, env=self.env, stdout=stdout, stderr=NULL)
91 sys.stderr.write('error: failed to execute %s: %s\n' % (cmd[0], ex.strerror))
94 def retrace(self, args):
99 def snapshot(self, call_nos):
100 process = self._retrace([
104 return RetraceRun(process)
106 def dump_state(self, call_no):
107 '''Get the state dump at the specified call no.'''
112 state = jsondiff.load(p.stdout)
114 return state.get('parameters', {})
116 def diff_state(self, ref_call_no, src_call_no, stream):
117 '''Compare the state between two calls.'''
119 ref_state = self.dump_state(ref_call_no)
120 src_state = self.dump_state(src_call_no)
123 differ = jsondiff.Differ(stream)
124 differ.visit(ref_state, src_state)
128 def read_pnm(stream):
129 '''Read a PNM from the stream, and return the image object, and the comment.'''
131 magic = stream.readline()
134 magic = magic.rstrip()
156 raise Exception('Unsupported magic `%s`' % magic)
158 line = stream.readline()
159 while line.startswith('#'):
161 line = stream.readline()
162 width, height = map(int, line.strip().split())
163 maximum = int(stream.readline().strip())
164 if bytesPerChannel == 1:
165 assert maximum == 255
168 data = stream.read(height * width * channels * bytesPerChannel)
169 if bytesPerChannel == 4:
170 # Image magic only supports single channel floating point images, so
171 # represent the image as numpy arrays
174 pixels = numpy.fromstring(data, dtype=numpy.float32)
175 pixels.resize((height, width, channels))
176 return pixels, comment
178 image = Image.frombuffer(mode, (width, height), data, 'raw', mode, 0, 1)
179 return image, comment
182 def dumpNumpyImage(output, pixels):
183 height, width, channels = pixels.shape
184 for y in range(height):
186 for x in range(width):
187 for c in range(channels):
188 output.write('%0.9g,' % pixels[y, x, c])
193 def parse_env(optparser, entries):
194 '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
199 env = os.environ.copy()
200 for entry in entries:
202 name, var = entry.split('=', 1)
204 optparser.error('invalid environment entry %r' % entry)
215 # Parse command line options
216 optparser = optparse.OptionParser(
217 usage='\n\t%prog [options] -- [glretrace options] <trace>',
219 optparser.add_option(
220 '-r', '--retrace', metavar='PROGRAM',
221 type='string', dest='retrace', default='glretrace',
222 help='retrace command [default: %default]')
223 optparser.add_option(
224 '--ref-driver', metavar='DRIVER',
225 type='string', dest='ref_driver', default=None,
226 help='force reference driver')
227 optparser.add_option(
228 '--src-driver', metavar='DRIVER',
229 type='string', dest='src_driver', default=None,
230 help='force source driver')
231 optparser.add_option(
232 '--ref-arg', metavar='OPTION',
233 type='string', action='append', dest='ref_args', default=[],
234 help='pass argument to reference retrace')
235 optparser.add_option(
236 '--src-arg', metavar='OPTION',
237 type='string', action='append', dest='src_args', default=[],
238 help='pass argument to source retrace')
239 optparser.add_option(
240 '--ref-env', metavar='NAME=VALUE',
241 type='string', action='append', dest='ref_env', default=[],
242 help='add variable to reference environment')
243 optparser.add_option(
244 '--src-env', metavar='NAME=VALUE',
245 type='string', action='append', dest='src_env', default=[],
246 help='add variable to source environment')
247 optparser.add_option(
248 '--diff-prefix', metavar='PATH',
249 type='string', dest='diff_prefix', default='.',
250 help='prefix for the difference images')
251 optparser.add_option(
252 '-t', '--threshold', metavar='BITS',
253 type="float", dest="threshold", default=12.0,
254 help="threshold precision [default: %default]")
255 optparser.add_option(
256 '-S', '--snapshot-frequency', metavar='CALLSET',
257 type="string", dest="snapshot_frequency", default='draw',
258 help="calls to compare [default: %default]")
259 optparser.add_option(
261 action='store_true', dest='diff_state', default=False,
262 help='diff state between failing calls')
263 optparser.add_option(
264 '-o', '--output', metavar='FILE',
265 type="string", dest="output",
266 help="output file [default: stdout]")
268 (options, args) = optparser.parse_args(sys.argv[1:])
269 ref_env = parse_env(optparser, options.ref_env)
270 src_env = parse_env(optparser, options.src_env)
272 optparser.error("incorrect number of arguments")
274 if options.ref_driver:
275 options.ref_args.insert(0, '--driver=' + options.ref_driver)
276 if options.src_driver:
277 options.src_args.insert(0, '--driver=' + options.src_driver)
279 refRetracer = Retracer(options.retrace, options.ref_args + args, ref_env)
280 srcRetracer = Retracer(options.retrace, options.src_args + args, src_env)
283 output = open(options.output, 'wt')
287 highligher = AutoHighlighter(output)
289 highligher.write('call\tprecision\n')
293 refRun = refRetracer.snapshot(options.snapshot_frequency)
295 srcRun = srcRetracer.snapshot(options.snapshot_frequency)
298 # Get the reference image
299 refImage, refCallNo = refRun.nextSnapshot()
303 # Get the source image
304 srcImage, srcCallNo = srcRun.nextSnapshot()
308 assert refCallNo == srcCallNo
311 # Compare the two images
312 if isinstance(refImage, Image.Image) and isinstance(srcImage, Image.Image):
315 comparer = Comparer(refImage, srcImage)
316 precision = comparer.precision()
318 # Using numpy (for floating point images)
319 # TODO: drop PIL when numpy path becomes general enough
321 assert not isinstance(refImage, Image.Image)
322 assert not isinstance(srcImage, Image.Image)
324 assert refImage.shape == srcImage.shape
325 diffImage = numpy.square(srcImage - refImage)
326 match = numpy.all(diffImage == 0)
332 mismatch = precision < options.threshold
335 highligher.color(highligher.red)
337 highligher.write('%u\t%f\n' % (callNo, precision))
343 dumpNumpyImage(output, refImage)
345 dumpNumpyImage(output, srcImage)
346 if options.diff_prefix and not numpyImages:
347 prefix = os.path.join(options.diff_prefix, '%010u' % callNo)
348 prefix_dir = os.path.dirname(prefix)
349 if not os.path.isdir(prefix_dir):
350 os.makedirs(prefix_dir)
351 refImage.save(prefix + '.ref.png')
352 srcImage.save(prefix + '.src.png')
353 comparer.write_diff(prefix + '.diff.png')
354 if last_bad < last_good and options.diff_state:
355 srcRetracer.diff_state(last_good, callNo, output)
367 if __name__ == '__main__':