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:', 'wt')
48 NULL = open('/dev/null', 'wt')
53 def __init__(self, args, env=None):
57 def _retrace(self, args):
62 return subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
64 sys.stderr.write('error: failed to execute %s: %s\n' % (cmd[0], ex.strerror))
68 return self._retrace([
70 '-S', options.snapshot_frequency,
73 def dump_state(self, call_no):
74 '''Get the state dump at the specified call no.'''
79 state = jsondiff.load(p.stdout)
81 return state.get('parameters', {})
83 def diff_state(self, ref_call_no, src_call_no, stream):
84 '''Compare the state between two calls.'''
86 ref_state = self.dump_state(ref_call_no)
87 src_state = self.dump_state(src_call_no)
90 differ = jsondiff.Differ(stream)
91 differ.visit(ref_state, src_state)
96 '''Read a PNM from the stream, and return the image object, and the comment.'''
98 magic = stream.readline()
101 magic = magic.rstrip()
109 raise Exception('Unsupported magic `%s`' % magic)
111 line = stream.readline()
112 while line.startswith('#'):
114 line = stream.readline()
115 width, height = map(int, line.strip().split())
116 maximum = int(stream.readline().strip())
117 assert maximum == 255
118 data = stream.read(height * width * channels)
119 image = Image.frombuffer(mode, (width, height), data, 'raw', mode, 0, 1)
120 return image, comment
123 def parse_env(optparser, entries):
124 '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
126 env = os.environ.copy()
127 for entry in entries:
129 name, var = entry.split('=', 1)
131 optparser.error('invalid environment entry %r' % entry)
142 # Parse command line options
143 optparser = optparse.OptionParser(
144 usage='\n\t%prog [options] -- [glretrace options] <trace>',
146 optparser.add_option(
147 '-r', '--retrace', metavar='PROGRAM',
148 type='string', dest='retrace', default='glretrace',
149 help='retrace command [default: %default]')
150 optparser.add_option(
151 '--ref-env', metavar='NAME=VALUE',
152 type='string', action='append', dest='ref_env', default=[],
153 help='add variable to reference environment')
154 optparser.add_option(
155 '--src-env', metavar='NAME=VALUE',
156 type='string', action='append', dest='src_env', default=[],
157 help='add variable to source environment')
158 optparser.add_option(
159 '--diff-prefix', metavar='PATH',
160 type='string', dest='diff_prefix', default='.',
161 help='prefix for the difference images')
162 optparser.add_option(
163 '-t', '--threshold', metavar='BITS',
164 type="float", dest="threshold", default=12.0,
165 help="threshold precision [default: %default]")
166 optparser.add_option(
167 '-S', '--snapshot-frequency', metavar='CALLSET',
168 type="string", dest="snapshot_frequency", default='draw',
169 help="calls to compare [default: %default]")
170 optparser.add_option(
171 '-o', '--output', metavar='FILE',
172 type="string", dest="output",
173 help="output file [default: stdout]")
175 (options, args) = optparser.parse_args(sys.argv[1:])
176 ref_env = parse_env(optparser, options.ref_env)
177 src_env = parse_env(optparser, options.src_env)
179 optparser.error("incorrect number of arguments")
181 ref_setup = Setup(args, ref_env)
182 src_setup = Setup(args, src_env)
185 output = open(options.output, 'wt')
189 highligher = AutoHighlighter(output)
191 highligher.write('call\tprecision\n')
195 ref_proc = ref_setup.retrace()
197 src_proc = src_setup.retrace()
200 # Get the reference image
201 ref_image, ref_comment = read_pnm(ref_proc.stdout)
202 if ref_image is None:
205 # Get the source image
206 src_image, src_comment = read_pnm(src_proc.stdout)
207 if src_image is None:
210 assert ref_comment == src_comment
212 call_no = int(ref_comment.strip())
214 # Compare the two images
215 comparer = Comparer(ref_image, src_image)
216 precision = comparer.precision()
218 mismatch = precision < options.threshold
221 highligher.color(highligher.red)
223 highligher.write('%u\t%f\n' % (call_no, precision))
228 if options.diff_prefix:
229 prefix = os.path.join(options.diff_prefix, '%010u' % call_no)
230 prefix_dir = os.path.dirname(prefix)
231 if not os.path.isdir(prefix_dir):
232 os.makedirs(prefix_dir)
233 ref_image.save(prefix + '.ref.png')
234 src_image.save(prefix + '.src.png')
235 comparer.write_diff(prefix + '.diff.png')
236 if last_bad < last_good:
237 src_setup.diff_state(last_good, call_no, output)
249 if __name__ == '__main__':