blob: b4bad08db73588b93237cfc6b494ad0f24dbd8a5 [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
33import re
34import shutil
35import subprocess
36import platform
37import sys
38import tempfile
39
40from snapdiff import Comparer
41import 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
57 def retrace(self, snapshot_dir):
58 cmd = [
59 options.retrace,
60 '-s', snapshot_dir + os.path.sep,
61 '-S', options.snapshot_frequency,
62 ] + self.args
63 p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
64 return p
65
66 def dump_state(self, call_no):
67 '''Get the state dump at the specified call no.'''
68
69 cmd = [
70 options.retrace,
71 '-D', str(call_no),
72 ] + self.args
73 p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
74 state = jsondiff.load(p.stdout)
75 p.wait()
76 return state
77
78
79def diff_state(setup, ref_call_no, src_call_no):
80 ref_state = setup.dump_state(ref_call_no)
81 src_state = setup.dump_state(src_call_no)
82 sys.stdout.flush()
83 differ = jsondiff.Differ(sys.stdout)
84 differ.visit(ref_state, src_state)
85 sys.stdout.write('\n')
86
87
88def parse_env(optparser, entries):
89 '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
90
91 env = os.environ.copy()
92 for entry in entries:
93 try:
94 name, var = entry.split('=', 1)
95 except Exception:
96 optparser.error('invalid environment entry %r' % entry)
97 env[name] = var
98 return env
99
100
101def main():
102 '''Main program.
103 '''
104
105 global options
106
107 # Parse command line options
108 optparser = optparse.OptionParser(
109 usage='\n\t%prog [options] -- [glretrace options] <trace>',
110 version='%%prog')
111 optparser.add_option(
112 '-r', '--retrace', metavar='PROGRAM',
113 type='string', dest='retrace', default='glretrace',
114 help='retrace command [default: %default]')
115 optparser.add_option(
116 '--ref-env', metavar='NAME=VALUE',
117 type='string', action='append', dest='ref_env', default=[],
118 help='reference environment variable')
119 optparser.add_option(
120 '--src-env', metavar='NAME=VALUE',
121 type='string', action='append', dest='src_env', default=[],
122 help='reference environment variable')
123 optparser.add_option(
124 '--diff-prefix', metavar='PATH',
125 type='string', dest='diff_prefix', default='.',
126 help='reference environment variable')
127 optparser.add_option(
128 '-t', '--threshold', metavar='BITS',
129 type="float", dest="threshold", default=12.0,
130 help="threshold precision [default: %default]")
131 optparser.add_option(
132 '-S', '--snapshot-frequency', metavar='FREQUENCY',
133 type="string", dest="snapshot_frequency", default='draw',
134 help="snapshot frequency [default: %default]")
135
136 (options, args) = optparser.parse_args(sys.argv[1:])
137 ref_env = parse_env(optparser, options.ref_env)
138 src_env = parse_env(optparser, options.src_env)
139 if not args:
140 optparser.error("incorrect number of arguments")
141
142 ref_setup = Setup(args, ref_env)
143 src_setup = Setup(args, src_env)
144
145 image_re = re.compile('^Wrote (.*\.png)$')
146
147 last_good = -1
148 last_bad = -1
149 ref_snapshot_dir = tempfile.mkdtemp()
150 try:
151 src_snapshot_dir = tempfile.mkdtemp()
152 try:
153 ref_proc = ref_setup.retrace(ref_snapshot_dir)
154 try:
155 src_proc = src_setup.retrace(src_snapshot_dir)
156 try:
157 for ref_line in ref_proc.stdout:
158 # Get the reference image
159 ref_line = ref_line.rstrip()
160 mo = image_re.match(ref_line)
161 if mo:
162 ref_image = mo.group(1)
163 for src_line in src_proc.stdout:
164 # Get the source image
165 src_line = src_line.rstrip()
166 mo = image_re.match(src_line)
167 if mo:
168 src_image = mo.group(1)
169
170 root, ext = os.path.splitext(os.path.basename(src_image))
171 call_no = int(root)
172
173 # Compare the two images
174 comparer = Comparer(ref_image, src_image)
175 precision = comparer.precision()
176
177 sys.stdout.write('%u %f\n' % (call_no, precision))
178
179 if precision < options.threshold:
180 if options.diff_prefix:
181 comparer.write_diff(os.path.join(options.diff_prefix, root + '.diff.png'))
182 if last_bad < last_good:
183 diff_state(src_setup, last_good, call_no)
184 last_bad = call_no
185 else:
186 last_good = call_no
187
188 sys.stdout.flush()
189
190 os.unlink(src_image)
191 break
192 os.unlink(ref_image)
193 finally:
194 src_proc.terminate()
195 finally:
196 ref_proc.terminate()
197 finally:
198 shutil.rmtree(ref_snapshot_dir)
199 finally:
200 shutil.rmtree(src_snapshot_dir)
201
202
203if __name__ == '__main__':
204 main()