blob: 34959753fc0acf9b137e07394a6e60e621028e9d [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é Fonsecab96ab8e2011-09-06 10:22:56 +010040from highlight import Highlighter
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':
46 NULL = open('NUL:', 'wt')
47else:
48 NULL = open('/dev/null', 'wt')
49
50
51class Setup:
52
53 def __init__(self, args, env=None):
54 self.args = args
55 self.env = env
56
José Fonseca20303032011-10-20 16:12:10 +020057 def _retrace(self, args):
José Fonseca0b956fd2011-06-04 22:51:45 +010058 cmd = [
59 options.retrace,
José Fonseca20303032011-10-20 16:12:10 +020060 ] + args + self.args
61 try:
62 return subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
63 except OSError, ex:
64 sys.stderr.write('error: failed to execute %s: %s\n' % (cmd[0], ex.strerror))
65 sys.exit(1)
66
67 def retrace(self):
68 return self._retrace([
José Fonsecabcca5f72011-09-06 00:07:41 +010069 '-s', '-',
José Fonseca0b956fd2011-06-04 22:51:45 +010070 '-S', options.snapshot_frequency,
José Fonseca20303032011-10-20 16:12:10 +020071 ])
José Fonseca0b956fd2011-06-04 22:51:45 +010072
73 def dump_state(self, call_no):
74 '''Get the state dump at the specified call no.'''
75
José Fonseca20303032011-10-20 16:12:10 +020076 p = self._retrace([
José Fonseca0b956fd2011-06-04 22:51:45 +010077 '-D', str(call_no),
José Fonseca20303032011-10-20 16:12:10 +020078 ])
José Fonseca0b956fd2011-06-04 22:51:45 +010079 state = jsondiff.load(p.stdout)
80 p.wait()
José Fonsecab96ab8e2011-09-06 10:22:56 +010081 return state.get('parameters', {})
José Fonseca0b956fd2011-06-04 22:51:45 +010082
José Fonseca36fa87c2011-09-06 00:15:32 +010083 def diff_state(self, ref_call_no, src_call_no):
84 '''Compare the state between two calls.'''
85
86 ref_state = self.dump_state(ref_call_no)
87 src_state = self.dump_state(src_call_no)
José Fonsecab96ab8e2011-09-06 10:22:56 +010088
José Fonseca36fa87c2011-09-06 00:15:32 +010089 sys.stdout.flush()
90 differ = jsondiff.Differ(sys.stdout)
91 differ.visit(ref_state, src_state)
92 sys.stdout.write('\n')
93
José Fonseca0b956fd2011-06-04 22:51:45 +010094
José Fonsecabcca5f72011-09-06 00:07:41 +010095def read_pnm(stream):
96 '''Read a PNM from the stream, and return the image object, and the comment.'''
97
98 magic = stream.readline()
99 if not magic:
100 return None, None
101 assert magic.rstrip() == 'P6'
102 comment = ''
103 line = stream.readline()
104 while line.startswith('#'):
105 comment += line[1:]
106 line = stream.readline()
107 width, height = map(int, line.strip().split())
108 maximum = int(stream.readline().strip())
109 assert maximum == 255
110 data = stream.read(height * width * 3)
111 image = Image.frombuffer('RGB', (width, height), data, 'raw', 'RGB', 0, 1)
112 return image, comment
113
114
José Fonseca0b956fd2011-06-04 22:51:45 +0100115def parse_env(optparser, entries):
116 '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
117
118 env = os.environ.copy()
119 for entry in entries:
120 try:
121 name, var = entry.split('=', 1)
122 except Exception:
123 optparser.error('invalid environment entry %r' % entry)
124 env[name] = var
125 return env
126
127
128def main():
129 '''Main program.
130 '''
131
132 global options
133
134 # Parse command line options
135 optparser = optparse.OptionParser(
136 usage='\n\t%prog [options] -- [glretrace options] <trace>',
137 version='%%prog')
138 optparser.add_option(
139 '-r', '--retrace', metavar='PROGRAM',
140 type='string', dest='retrace', default='glretrace',
141 help='retrace command [default: %default]')
142 optparser.add_option(
143 '--ref-env', metavar='NAME=VALUE',
144 type='string', action='append', dest='ref_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100145 help='add variable to reference environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100146 optparser.add_option(
147 '--src-env', metavar='NAME=VALUE',
148 type='string', action='append', dest='src_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100149 help='add variable to source environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100150 optparser.add_option(
151 '--diff-prefix', metavar='PATH',
152 type='string', dest='diff_prefix', default='.',
José Fonsecabcca5f72011-09-06 00:07:41 +0100153 help='prefix for the difference images')
José Fonseca0b956fd2011-06-04 22:51:45 +0100154 optparser.add_option(
155 '-t', '--threshold', metavar='BITS',
156 type="float", dest="threshold", default=12.0,
157 help="threshold precision [default: %default]")
158 optparser.add_option(
José Fonseca225193d2012-01-26 19:08:32 +0000159 '-S', '--snapshot-frequency', metavar='CALLSET',
José Fonseca0b956fd2011-06-04 22:51:45 +0100160 type="string", dest="snapshot_frequency", default='draw',
José Fonseca225193d2012-01-26 19:08:32 +0000161 help="calls to compare [default: %default]")
José Fonseca0b956fd2011-06-04 22:51:45 +0100162
163 (options, args) = optparser.parse_args(sys.argv[1:])
164 ref_env = parse_env(optparser, options.ref_env)
165 src_env = parse_env(optparser, options.src_env)
166 if not args:
167 optparser.error("incorrect number of arguments")
168
169 ref_setup = Setup(args, ref_env)
170 src_setup = Setup(args, src_env)
171
José Fonsecab96ab8e2011-09-06 10:22:56 +0100172 highligher = Highlighter(sys.stdout)
173
174 highligher.write('call\tprecision\n')
José Fonseca0b956fd2011-06-04 22:51:45 +0100175
José Fonseca0b956fd2011-06-04 22:51:45 +0100176 last_bad = -1
José Fonseca36fa87c2011-09-06 00:15:32 +0100177 last_good = 0
José Fonsecabcca5f72011-09-06 00:07:41 +0100178 ref_proc = ref_setup.retrace()
José Fonseca0b956fd2011-06-04 22:51:45 +0100179 try:
José Fonsecabcca5f72011-09-06 00:07:41 +0100180 src_proc = src_setup.retrace()
José Fonseca0b956fd2011-06-04 22:51:45 +0100181 try:
José Fonsecabcca5f72011-09-06 00:07:41 +0100182 while True:
183 # Get the reference image
184 ref_image, ref_comment = read_pnm(ref_proc.stdout)
185 if ref_image is None:
186 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100187
José Fonsecabcca5f72011-09-06 00:07:41 +0100188 # Get the source image
189 src_image, src_comment = read_pnm(src_proc.stdout)
190 if src_image is None:
191 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100192
José Fonsecabcca5f72011-09-06 00:07:41 +0100193 assert ref_comment == src_comment
José Fonseca0b956fd2011-06-04 22:51:45 +0100194
José Fonsecabcca5f72011-09-06 00:07:41 +0100195 call_no = int(ref_comment.strip())
196
197 # Compare the two images
198 comparer = Comparer(ref_image, src_image)
199 precision = comparer.precision()
200
José Fonsecab96ab8e2011-09-06 10:22:56 +0100201 mismatch = precision < options.threshold
José Fonsecabcca5f72011-09-06 00:07:41 +0100202
José Fonsecab96ab8e2011-09-06 10:22:56 +0100203 if mismatch:
204 highligher.color(highligher.red)
205 highligher.bold()
206 highligher.write('%u\t%f\n' % (call_no, precision))
207 if mismatch:
208 highligher.normal()
209
210 if mismatch:
José Fonsecabcca5f72011-09-06 00:07:41 +0100211 if options.diff_prefix:
212 prefix = os.path.join(options.diff_prefix, '%010u' % call_no)
213 prefix_dir = os.path.dirname(prefix)
214 if not os.path.isdir(prefix_dir):
215 os.makedirs(prefix_dir)
216 ref_image.save(prefix + '.ref.png')
217 src_image.save(prefix + '.src.png')
218 comparer.write_diff(prefix + '.diff.png')
219 if last_bad < last_good:
José Fonseca36fa87c2011-09-06 00:15:32 +0100220 src_setup.diff_state(last_good, call_no)
José Fonsecabcca5f72011-09-06 00:07:41 +0100221 last_bad = call_no
222 else:
223 last_good = call_no
224
José Fonsecab96ab8e2011-09-06 10:22:56 +0100225 highligher.flush()
José Fonseca0b956fd2011-06-04 22:51:45 +0100226 finally:
José Fonsecabcca5f72011-09-06 00:07:41 +0100227 src_proc.terminate()
José Fonseca0b956fd2011-06-04 22:51:45 +0100228 finally:
José Fonsecabcca5f72011-09-06 00:07:41 +0100229 ref_proc.terminate()
José Fonseca0b956fd2011-06-04 22:51:45 +0100230
231
232if __name__ == '__main__':
233 main()