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.
40 from snapdiff import Comparer
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, snapshot_dir):
60 '-s', snapshot_dir + os.path.sep,
61 '-S', options.snapshot_frequency,
63 p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
66 def dump_state(self, call_no):
67 '''Get the state dump at the specified call no.'''
73 p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
74 state = jsondiff.load(p.stdout)
79 def 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)
83 differ = jsondiff.Differ(sys.stdout)
84 differ.visit(ref_state, src_state)
85 sys.stdout.write('\n')
88 def parse_env(optparser, entries):
89 '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
91 env = os.environ.copy()
94 name, var = entry.split('=', 1)
96 optparser.error('invalid environment entry %r' % entry)
107 # Parse command line options
108 optparser = optparse.OptionParser(
109 usage='\n\t%prog [options] -- [glretrace options] <trace>',
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]")
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)
140 optparser.error("incorrect number of arguments")
142 ref_setup = Setup(args, ref_env)
143 src_setup = Setup(args, src_env)
145 image_re = re.compile('^Wrote (.*\.png)$')
149 ref_snapshot_dir = tempfile.mkdtemp()
151 src_snapshot_dir = tempfile.mkdtemp()
153 ref_proc = ref_setup.retrace(ref_snapshot_dir)
155 src_proc = src_setup.retrace(src_snapshot_dir)
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)
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)
168 src_image = mo.group(1)
170 root, ext = os.path.splitext(os.path.basename(src_image))
173 # Compare the two images
174 comparer = Comparer(ref_image, src_image)
175 precision = comparer.precision()
177 sys.stdout.write('%u %f\n' % (call_no, precision))
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)
198 shutil.rmtree(ref_snapshot_dir)
200 shutil.rmtree(src_snapshot_dir)
203 if __name__ == '__main__':