blob: 171622d03a1ea942b2073e53ee92a2f3feaf264e [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é Fonseca01908962012-03-16 09:56:09 +000040from highlight import AutoHighlighter
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
José Fonseca83508372012-11-27 13:11:21 +000061 if self.env:
62 for name, value in self.env.iteritems():
63 sys.stderr.write('%s=%s ' % (name, value))
64 sys.stderr.write(' '.join(cmd) + '\n')
José Fonseca20303032011-10-20 16:12:10 +020065 try:
66 return subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
67 except OSError, ex:
68 sys.stderr.write('error: failed to execute %s: %s\n' % (cmd[0], ex.strerror))
69 sys.exit(1)
70
71 def retrace(self):
72 return self._retrace([
José Fonsecabcca5f72011-09-06 00:07:41 +010073 '-s', '-',
José Fonseca0b956fd2011-06-04 22:51:45 +010074 '-S', options.snapshot_frequency,
José Fonseca20303032011-10-20 16:12:10 +020075 ])
José Fonseca0b956fd2011-06-04 22:51:45 +010076
77 def dump_state(self, call_no):
78 '''Get the state dump at the specified call no.'''
79
José Fonseca20303032011-10-20 16:12:10 +020080 p = self._retrace([
José Fonseca0b956fd2011-06-04 22:51:45 +010081 '-D', str(call_no),
José Fonseca20303032011-10-20 16:12:10 +020082 ])
José Fonseca0b956fd2011-06-04 22:51:45 +010083 state = jsondiff.load(p.stdout)
84 p.wait()
José Fonsecab96ab8e2011-09-06 10:22:56 +010085 return state.get('parameters', {})
José Fonseca0b956fd2011-06-04 22:51:45 +010086
José Fonsecad8ea58f2012-02-09 14:35:27 +000087 def diff_state(self, ref_call_no, src_call_no, stream):
José Fonseca36fa87c2011-09-06 00:15:32 +010088 '''Compare the state between two calls.'''
89
90 ref_state = self.dump_state(ref_call_no)
91 src_state = self.dump_state(src_call_no)
José Fonsecab96ab8e2011-09-06 10:22:56 +010092
José Fonsecad8ea58f2012-02-09 14:35:27 +000093 stream.flush()
94 differ = jsondiff.Differ(stream)
José Fonseca36fa87c2011-09-06 00:15:32 +010095 differ.visit(ref_state, src_state)
José Fonsecad8ea58f2012-02-09 14:35:27 +000096 stream.write('\n')
José Fonseca36fa87c2011-09-06 00:15:32 +010097
José Fonseca0b956fd2011-06-04 22:51:45 +010098
José Fonsecabcca5f72011-09-06 00:07:41 +010099def read_pnm(stream):
100 '''Read a PNM from the stream, and return the image object, and the comment.'''
101
102 magic = stream.readline()
103 if not magic:
104 return None, None
José Fonseca0ee87892012-10-30 15:54:04 +0000105 magic = magic.rstrip()
106 if magic == 'P5':
107 channels = 1
108 mode = 'L'
109 elif magic == 'P6':
110 channels = 3
111 mode = 'RGB'
112 else:
113 raise Exception('Unsupported magic `%s`' % magic)
José Fonsecabcca5f72011-09-06 00:07:41 +0100114 comment = ''
115 line = stream.readline()
116 while line.startswith('#'):
117 comment += line[1:]
118 line = stream.readline()
119 width, height = map(int, line.strip().split())
120 maximum = int(stream.readline().strip())
121 assert maximum == 255
José Fonseca0ee87892012-10-30 15:54:04 +0000122 data = stream.read(height * width * channels)
123 image = Image.frombuffer(mode, (width, height), data, 'raw', mode, 0, 1)
José Fonsecabcca5f72011-09-06 00:07:41 +0100124 return image, comment
125
126
José Fonseca0b956fd2011-06-04 22:51:45 +0100127def parse_env(optparser, entries):
128 '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
129
José Fonseca83508372012-11-27 13:11:21 +0000130 if not entries:
131 return None
132
José Fonseca0b956fd2011-06-04 22:51:45 +0100133 env = os.environ.copy()
134 for entry in entries:
135 try:
136 name, var = entry.split('=', 1)
137 except Exception:
138 optparser.error('invalid environment entry %r' % entry)
139 env[name] = var
140 return env
141
142
143def main():
144 '''Main program.
145 '''
146
147 global options
148
149 # Parse command line options
150 optparser = optparse.OptionParser(
151 usage='\n\t%prog [options] -- [glretrace options] <trace>',
152 version='%%prog')
153 optparser.add_option(
154 '-r', '--retrace', metavar='PROGRAM',
155 type='string', dest='retrace', default='glretrace',
156 help='retrace command [default: %default]')
157 optparser.add_option(
José Fonseca83508372012-11-27 13:11:21 +0000158 '--ref-driver', metavar='DRIVER',
159 type='string', dest='ref_driver', default=None,
160 help='force reference driver')
161 optparser.add_option(
162 '--src-driver', metavar='DRIVER',
163 type='string', dest='src_driver', default=None,
164 help='force source driver')
165 optparser.add_option(
166 '--ref-arg', metavar='OPTION',
167 type='string', action='append', dest='ref_args', default=[],
168 help='pass argument to reference retrace')
169 optparser.add_option(
170 '--src-arg', metavar='OPTION',
171 type='string', action='append', dest='src_args', default=[],
172 help='pass argument to source retrace')
173 optparser.add_option(
José Fonseca0b956fd2011-06-04 22:51:45 +0100174 '--ref-env', metavar='NAME=VALUE',
175 type='string', action='append', dest='ref_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100176 help='add variable to reference environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100177 optparser.add_option(
178 '--src-env', metavar='NAME=VALUE',
179 type='string', action='append', dest='src_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100180 help='add variable to source environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100181 optparser.add_option(
182 '--diff-prefix', metavar='PATH',
183 type='string', dest='diff_prefix', default='.',
José Fonsecabcca5f72011-09-06 00:07:41 +0100184 help='prefix for the difference images')
José Fonseca0b956fd2011-06-04 22:51:45 +0100185 optparser.add_option(
186 '-t', '--threshold', metavar='BITS',
187 type="float", dest="threshold", default=12.0,
188 help="threshold precision [default: %default]")
189 optparser.add_option(
José Fonseca225193d2012-01-26 19:08:32 +0000190 '-S', '--snapshot-frequency', metavar='CALLSET',
José Fonseca0b956fd2011-06-04 22:51:45 +0100191 type="string", dest="snapshot_frequency", default='draw',
José Fonseca225193d2012-01-26 19:08:32 +0000192 help="calls to compare [default: %default]")
José Fonsecad8ea58f2012-02-09 14:35:27 +0000193 optparser.add_option(
194 '-o', '--output', metavar='FILE',
195 type="string", dest="output",
196 help="output file [default: stdout]")
José Fonseca0b956fd2011-06-04 22:51:45 +0100197
198 (options, args) = optparser.parse_args(sys.argv[1:])
199 ref_env = parse_env(optparser, options.ref_env)
200 src_env = parse_env(optparser, options.src_env)
201 if not args:
202 optparser.error("incorrect number of arguments")
José Fonseca83508372012-11-27 13:11:21 +0000203
204 if options.ref_driver:
205 options.ref_args.insert(0, '--driver=' + options.ref_driver)
206 if options.src_driver:
207 options.src_args.insert(0, '--driver=' + options.src_driver)
José Fonseca0b956fd2011-06-04 22:51:45 +0100208
José Fonseca83508372012-11-27 13:11:21 +0000209 ref_setup = Setup(options.ref_args + args, ref_env)
210 src_setup = Setup(options.src_args + args, src_env)
José Fonseca0b956fd2011-06-04 22:51:45 +0100211
José Fonsecad8ea58f2012-02-09 14:35:27 +0000212 if options.output:
213 output = open(options.output, 'wt')
214 else:
215 output = sys.stdout
216
José Fonseca01908962012-03-16 09:56:09 +0000217 highligher = AutoHighlighter(output)
José Fonsecab96ab8e2011-09-06 10:22:56 +0100218
219 highligher.write('call\tprecision\n')
José Fonseca0b956fd2011-06-04 22:51:45 +0100220
José Fonseca0b956fd2011-06-04 22:51:45 +0100221 last_bad = -1
José Fonseca36fa87c2011-09-06 00:15:32 +0100222 last_good = 0
José Fonsecabcca5f72011-09-06 00:07:41 +0100223 ref_proc = ref_setup.retrace()
José Fonseca0b956fd2011-06-04 22:51:45 +0100224 try:
José Fonsecabcca5f72011-09-06 00:07:41 +0100225 src_proc = src_setup.retrace()
José Fonseca0b956fd2011-06-04 22:51:45 +0100226 try:
José Fonsecabcca5f72011-09-06 00:07:41 +0100227 while True:
228 # Get the reference image
229 ref_image, ref_comment = read_pnm(ref_proc.stdout)
230 if ref_image is None:
231 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100232
José Fonsecabcca5f72011-09-06 00:07:41 +0100233 # Get the source image
234 src_image, src_comment = read_pnm(src_proc.stdout)
235 if src_image is None:
236 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100237
José Fonsecabcca5f72011-09-06 00:07:41 +0100238 assert ref_comment == src_comment
José Fonseca0b956fd2011-06-04 22:51:45 +0100239
José Fonsecabcca5f72011-09-06 00:07:41 +0100240 call_no = int(ref_comment.strip())
241
242 # Compare the two images
243 comparer = Comparer(ref_image, src_image)
244 precision = comparer.precision()
245
José Fonsecab96ab8e2011-09-06 10:22:56 +0100246 mismatch = precision < options.threshold
José Fonsecabcca5f72011-09-06 00:07:41 +0100247
José Fonsecab96ab8e2011-09-06 10:22:56 +0100248 if mismatch:
249 highligher.color(highligher.red)
250 highligher.bold()
251 highligher.write('%u\t%f\n' % (call_no, precision))
252 if mismatch:
253 highligher.normal()
254
255 if mismatch:
José Fonsecabcca5f72011-09-06 00:07:41 +0100256 if options.diff_prefix:
257 prefix = os.path.join(options.diff_prefix, '%010u' % call_no)
258 prefix_dir = os.path.dirname(prefix)
259 if not os.path.isdir(prefix_dir):
260 os.makedirs(prefix_dir)
261 ref_image.save(prefix + '.ref.png')
262 src_image.save(prefix + '.src.png')
263 comparer.write_diff(prefix + '.diff.png')
264 if last_bad < last_good:
José Fonsecad8ea58f2012-02-09 14:35:27 +0000265 src_setup.diff_state(last_good, call_no, output)
José Fonsecabcca5f72011-09-06 00:07:41 +0100266 last_bad = call_no
267 else:
268 last_good = call_no
269
José Fonsecab96ab8e2011-09-06 10:22:56 +0100270 highligher.flush()
José Fonseca0b956fd2011-06-04 22:51:45 +0100271 finally:
José Fonseca10d04a22013-04-18 13:38:37 +0100272 try:
273 src_proc.terminate()
274 except OSError:
275 # Avoid http://bugs.python.org/issue14252
276 pass
José Fonseca0b956fd2011-06-04 22:51:45 +0100277 finally:
José Fonseca10d04a22013-04-18 13:38:37 +0100278 try:
279 ref_proc.terminate()
280 except OSError:
281 # Avoid http://bugs.python.org/issue14252
282 pass
José Fonseca0b956fd2011-06-04 22:51:45 +0100283
284
285if __name__ == '__main__':
286 main()