--- /dev/null
+#!/usr/bin/env python
+##########################################################################
+#
+# Copyright 2011 Jose Fonseca
+# All Rights Reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the 'Software'), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+##########################################################################/
+
+'''Run two retrace instances in parallel, comparing generated snapshots.
+'''
+
+
+import optparse
+import os.path
+import re
+import shutil
+import subprocess
+import platform
+import sys
+import tempfile
+
+from snapdiff import Comparer
+import jsondiff
+
+
+# Null file, to use when we're not interested in subprocesses output
+if platform.system() == 'Windows':
+ NULL = open('NUL:', 'wt')
+else:
+ NULL = open('/dev/null', 'wt')
+
+
+class Setup:
+
+ def __init__(self, args, env=None):
+ self.args = args
+ self.env = env
+
+ def retrace(self, snapshot_dir):
+ cmd = [
+ options.retrace,
+ '-s', snapshot_dir + os.path.sep,
+ '-S', options.snapshot_frequency,
+ ] + self.args
+ p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
+ return p
+
+ def dump_state(self, call_no):
+ '''Get the state dump at the specified call no.'''
+
+ cmd = [
+ options.retrace,
+ '-D', str(call_no),
+ ] + self.args
+ p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
+ state = jsondiff.load(p.stdout)
+ p.wait()
+ return state
+
+
+def diff_state(setup, ref_call_no, src_call_no):
+ ref_state = setup.dump_state(ref_call_no)
+ src_state = setup.dump_state(src_call_no)
+ sys.stdout.flush()
+ differ = jsondiff.Differ(sys.stdout)
+ differ.visit(ref_state, src_state)
+ sys.stdout.write('\n')
+
+
+def parse_env(optparser, entries):
+ '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
+
+ env = os.environ.copy()
+ for entry in entries:
+ try:
+ name, var = entry.split('=', 1)
+ except Exception:
+ optparser.error('invalid environment entry %r' % entry)
+ env[name] = var
+ return env
+
+
+def main():
+ '''Main program.
+ '''
+
+ global options
+
+ # Parse command line options
+ optparser = optparse.OptionParser(
+ usage='\n\t%prog [options] -- [glretrace options] <trace>',
+ version='%%prog')
+ optparser.add_option(
+ '-r', '--retrace', metavar='PROGRAM',
+ type='string', dest='retrace', default='glretrace',
+ help='retrace command [default: %default]')
+ optparser.add_option(
+ '--ref-env', metavar='NAME=VALUE',
+ type='string', action='append', dest='ref_env', default=[],
+ help='reference environment variable')
+ optparser.add_option(
+ '--src-env', metavar='NAME=VALUE',
+ type='string', action='append', dest='src_env', default=[],
+ help='reference environment variable')
+ optparser.add_option(
+ '--diff-prefix', metavar='PATH',
+ type='string', dest='diff_prefix', default='.',
+ help='reference environment variable')
+ optparser.add_option(
+ '-t', '--threshold', metavar='BITS',
+ type="float", dest="threshold", default=12.0,
+ help="threshold precision [default: %default]")
+ optparser.add_option(
+ '-S', '--snapshot-frequency', metavar='FREQUENCY',
+ type="string", dest="snapshot_frequency", default='draw',
+ help="snapshot frequency [default: %default]")
+
+ (options, args) = optparser.parse_args(sys.argv[1:])
+ ref_env = parse_env(optparser, options.ref_env)
+ src_env = parse_env(optparser, options.src_env)
+ if not args:
+ optparser.error("incorrect number of arguments")
+
+ ref_setup = Setup(args, ref_env)
+ src_setup = Setup(args, src_env)
+
+ image_re = re.compile('^Wrote (.*\.png)$')
+
+ last_good = -1
+ last_bad = -1
+ ref_snapshot_dir = tempfile.mkdtemp()
+ try:
+ src_snapshot_dir = tempfile.mkdtemp()
+ try:
+ ref_proc = ref_setup.retrace(ref_snapshot_dir)
+ try:
+ src_proc = src_setup.retrace(src_snapshot_dir)
+ try:
+ for ref_line in ref_proc.stdout:
+ # Get the reference image
+ ref_line = ref_line.rstrip()
+ mo = image_re.match(ref_line)
+ if mo:
+ ref_image = mo.group(1)
+ for src_line in src_proc.stdout:
+ # Get the source image
+ src_line = src_line.rstrip()
+ mo = image_re.match(src_line)
+ if mo:
+ src_image = mo.group(1)
+
+ root, ext = os.path.splitext(os.path.basename(src_image))
+ call_no = int(root)
+
+ # Compare the two images
+ comparer = Comparer(ref_image, src_image)
+ precision = comparer.precision()
+
+ sys.stdout.write('%u %f\n' % (call_no, precision))
+
+ if precision < options.threshold:
+ if options.diff_prefix:
+ comparer.write_diff(os.path.join(options.diff_prefix, root + '.diff.png'))
+ if last_bad < last_good:
+ diff_state(src_setup, last_good, call_no)
+ last_bad = call_no
+ else:
+ last_good = call_no
+
+ sys.stdout.flush()
+
+ os.unlink(src_image)
+ break
+ os.unlink(ref_image)
+ finally:
+ src_proc.terminate()
+ finally:
+ ref_proc.terminate()
+ finally:
+ shutil.rmtree(ref_snapshot_dir)
+ finally:
+ shutil.rmtree(src_snapshot_dir)
+
+
+if __name__ == '__main__':
+ main()
thumb_size = 320, 320
-def compare(ref_image, src_image, delta_image):
- ref_im = Image.open(ref_image)
- src_im = Image.open(src_image)
-
- ref_im = ref_im.convert('RGB')
- src_im = src_im.convert('RGB')
-
- diff = ImageChops.difference(src_im, ref_im)
-
- # make a difference image similar to ImageMagick's compare utility
- mask = ImageEnhance.Brightness(diff).enhance(1.0/options.fuzz)
- mask = mask.convert('L')
-
- lowlight = Image.new('RGB', src_im.size, (0xff, 0xff, 0xff))
- highlight = Image.new('RGB', src_im.size, (0xf1, 0x00, 0x1e))
- delta_im = Image.composite(highlight, lowlight, mask)
-
- delta_im = Image.blend(src_im, delta_im, 0xcc/255.0)
- delta_im.save(delta_image)
-
- # See also http://effbot.org/zone/pil-comparing-images.htm
- # TODO: this is approximate due to the grayscale conversion
- h = diff.convert('L').histogram()
- ae = sum(h[int(255 * options.fuzz) + 1 : 256])
- return ae
+class Comparer:
+ '''Image comparer.'''
+
+ def __init__(self, ref_image, src_image, alpha = False):
+ self.ref_im = Image.open(ref_image)
+ self.src_im = Image.open(src_image)
+
+ # Ignore
+ if not alpha:
+ self.ref_im = self.ref_im.convert('RGB')
+ self.src_im = self.src_im.convert('RGB')
+
+ self.diff = ImageChops.difference(self.src_im, self.ref_im)
+
+ def write_diff(self, diff_image, fuzz = 0.05):
+ # make a difference image similar to ImageMagick's compare utility
+ mask = ImageEnhance.Brightness(self.diff).enhance(1.0/fuzz)
+ mask = mask.convert('L')
+
+ lowlight = Image.new('RGB', self.src_im.size, (0xff, 0xff, 0xff))
+ highlight = Image.new('RGB', self.src_im.size, (0xf1, 0x00, 0x1e))
+ diff_im = Image.composite(highlight, lowlight, mask)
+
+ diff_im = Image.blend(self.src_im, diff_im, 0xcc/255.0)
+ diff_im.save(diff_image)
+
+ def precision(self):
+ # See also http://effbot.org/zone/pil-comparing-images.htm
+ h = self.diff.histogram()
+ square_error = 0
+ for i in range(1, 256):
+ square_error += sum(h[i : 3*256: 256])*i*i
+ rel_error = float(square_error*2 + 1) / float(self.diff.size[0]*self.diff.size[1]*3*255*255*2)
+ bits = -math.log(rel_error)/math.log(2.0)
+ return bits
+
+ def ae(self):
+ # Compute absolute error
+ # TODO: this is approximate due to the grayscale conversion
+ h = self.diff.convert('L').histogram()
+ ae = sum(h[int(255 * fuzz) + 1 : 256])
+ return ae
def surface(html, image):
help="output filename [default: %default]")
optparser.add_option(
'-f', '--fuzz',
- type="float", dest="fuzz", default=.05,
+ type="float", dest="fuzz", default=0.05,
help="fuzz ratio [default: %default]")
optparser.add_option(
'--overwrite',
or not os.path.exists(delta_image) \
or (os.path.getmtime(delta_image) < os.path.getmtime(ref_image) \
and os.path.getmtime(delta_image) < os.path.getmtime(src_image)):
- compare(ref_image, src_image, delta_image)
+
+ comparer = Comparer(ref_image, src_image)
+ comparer.write_diff(delta_image, fuzz=options.fuzz)
html.write(' <tr>\n')
surface(html, ref_image)