blob: 42c36bbac3c1b3e5d110285bed065668a6f333de [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
40import jsondiff
41
42
43# Null file, to use when we're not interested in subprocesses output
44if platform.system() == 'Windows':
45 NULL = open('NUL:', 'wt')
46else:
47 NULL = open('/dev/null', 'wt')
48
49
50class Setup:
51
52 def __init__(self, args, env=None):
53 self.args = args
54 self.env = env
55
José Fonsecabcca5f72011-09-06 00:07:41 +010056 def retrace(self):
José Fonseca0b956fd2011-06-04 22:51:45 +010057 cmd = [
58 options.retrace,
José Fonsecabcca5f72011-09-06 00:07:41 +010059 '-s', '-',
José Fonseca0b956fd2011-06-04 22:51:45 +010060 '-S', options.snapshot_frequency,
61 ] + self.args
62 p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
63 return p
64
65 def dump_state(self, call_no):
66 '''Get the state dump at the specified call no.'''
67
68 cmd = [
69 options.retrace,
70 '-D', str(call_no),
71 ] + self.args
72 p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
73 state = jsondiff.load(p.stdout)
74 p.wait()
75 return state
76
José Fonseca36fa87c2011-09-06 00:15:32 +010077 def diff_state(self, ref_call_no, src_call_no):
78 '''Compare the state between two calls.'''
79
80 ref_state = self.dump_state(ref_call_no)
81 src_state = self.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
José Fonseca0b956fd2011-06-04 22:51:45 +010087
José Fonsecabcca5f72011-09-06 00:07:41 +010088def read_pnm(stream):
89 '''Read a PNM from the stream, and return the image object, and the comment.'''
90
91 magic = stream.readline()
92 if not magic:
93 return None, None
94 assert magic.rstrip() == 'P6'
95 comment = ''
96 line = stream.readline()
97 while line.startswith('#'):
98 comment += line[1:]
99 line = stream.readline()
100 width, height = map(int, line.strip().split())
101 maximum = int(stream.readline().strip())
102 assert maximum == 255
103 data = stream.read(height * width * 3)
104 image = Image.frombuffer('RGB', (width, height), data, 'raw', 'RGB', 0, 1)
105 return image, comment
106
107
José Fonseca0b956fd2011-06-04 22:51:45 +0100108def parse_env(optparser, entries):
109 '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
110
111 env = os.environ.copy()
112 for entry in entries:
113 try:
114 name, var = entry.split('=', 1)
115 except Exception:
116 optparser.error('invalid environment entry %r' % entry)
117 env[name] = var
118 return env
119
120
121def main():
122 '''Main program.
123 '''
124
125 global options
126
127 # Parse command line options
128 optparser = optparse.OptionParser(
129 usage='\n\t%prog [options] -- [glretrace options] <trace>',
130 version='%%prog')
131 optparser.add_option(
132 '-r', '--retrace', metavar='PROGRAM',
133 type='string', dest='retrace', default='glretrace',
134 help='retrace command [default: %default]')
135 optparser.add_option(
136 '--ref-env', metavar='NAME=VALUE',
137 type='string', action='append', dest='ref_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100138 help='add variable to reference environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100139 optparser.add_option(
140 '--src-env', metavar='NAME=VALUE',
141 type='string', action='append', dest='src_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100142 help='add variable to source environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100143 optparser.add_option(
144 '--diff-prefix', metavar='PATH',
145 type='string', dest='diff_prefix', default='.',
José Fonsecabcca5f72011-09-06 00:07:41 +0100146 help='prefix for the difference images')
José Fonseca0b956fd2011-06-04 22:51:45 +0100147 optparser.add_option(
148 '-t', '--threshold', metavar='BITS',
149 type="float", dest="threshold", default=12.0,
150 help="threshold precision [default: %default]")
151 optparser.add_option(
152 '-S', '--snapshot-frequency', metavar='FREQUENCY',
153 type="string", dest="snapshot_frequency", default='draw',
José Fonsecabcca5f72011-09-06 00:07:41 +0100154 help="snapshot frequency: frame, framebuffer, or draw [default: %default]")
José Fonseca0b956fd2011-06-04 22:51:45 +0100155
156 (options, args) = optparser.parse_args(sys.argv[1:])
157 ref_env = parse_env(optparser, options.ref_env)
158 src_env = parse_env(optparser, options.src_env)
159 if not args:
160 optparser.error("incorrect number of arguments")
161
162 ref_setup = Setup(args, ref_env)
163 src_setup = Setup(args, src_env)
164
José Fonseca36fa87c2011-09-06 00:15:32 +0100165 sys.stdout.write('call\tprecision\n')
José Fonseca0b956fd2011-06-04 22:51:45 +0100166
José Fonseca0b956fd2011-06-04 22:51:45 +0100167 last_bad = -1
José Fonseca36fa87c2011-09-06 00:15:32 +0100168 last_good = 0
José Fonsecabcca5f72011-09-06 00:07:41 +0100169 ref_proc = ref_setup.retrace()
José Fonseca0b956fd2011-06-04 22:51:45 +0100170 try:
José Fonsecabcca5f72011-09-06 00:07:41 +0100171 src_proc = src_setup.retrace()
José Fonseca0b956fd2011-06-04 22:51:45 +0100172 try:
José Fonsecabcca5f72011-09-06 00:07:41 +0100173 while True:
174 # Get the reference image
175 ref_image, ref_comment = read_pnm(ref_proc.stdout)
176 if ref_image is None:
177 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100178
José Fonsecabcca5f72011-09-06 00:07:41 +0100179 # Get the source image
180 src_image, src_comment = read_pnm(src_proc.stdout)
181 if src_image is None:
182 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100183
José Fonsecabcca5f72011-09-06 00:07:41 +0100184 assert ref_comment == src_comment
José Fonseca0b956fd2011-06-04 22:51:45 +0100185
José Fonsecabcca5f72011-09-06 00:07:41 +0100186 call_no = int(ref_comment.strip())
187
188 # Compare the two images
189 comparer = Comparer(ref_image, src_image)
190 precision = comparer.precision()
191
José Fonseca36fa87c2011-09-06 00:15:32 +0100192 sys.stdout.write('%u\t%f\n' % (call_no, precision))
José Fonsecabcca5f72011-09-06 00:07:41 +0100193
194 if precision < options.threshold:
195 if options.diff_prefix:
196 prefix = os.path.join(options.diff_prefix, '%010u' % call_no)
197 prefix_dir = os.path.dirname(prefix)
198 if not os.path.isdir(prefix_dir):
199 os.makedirs(prefix_dir)
200 ref_image.save(prefix + '.ref.png')
201 src_image.save(prefix + '.src.png')
202 comparer.write_diff(prefix + '.diff.png')
203 if last_bad < last_good:
José Fonseca36fa87c2011-09-06 00:15:32 +0100204 src_setup.diff_state(last_good, call_no)
José Fonsecabcca5f72011-09-06 00:07:41 +0100205 last_bad = call_no
206 else:
207 last_good = call_no
208
209 sys.stdout.flush()
José Fonseca0b956fd2011-06-04 22:51:45 +0100210 finally:
José Fonsecabcca5f72011-09-06 00:07:41 +0100211 src_proc.terminate()
José Fonseca0b956fd2011-06-04 22:51:45 +0100212 finally:
José Fonsecabcca5f72011-09-06 00:07:41 +0100213 ref_proc.terminate()
José Fonseca0b956fd2011-06-04 22:51:45 +0100214
215
216if __name__ == '__main__':
217 main()