blob: 1b2d2eb81d8dcd5e8269fffba7a89a24c8991920 [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
José Fonsecabcca5f72011-09-06 00:07:41 +010040from PIL import Image
41
José Fonseca0b956fd2011-06-04 22:51:45 +010042from snapdiff import Comparer
43import jsondiff
44
45
46# Null file, to use when we're not interested in subprocesses output
47if platform.system() == 'Windows':
48 NULL = open('NUL:', 'wt')
49else:
50 NULL = open('/dev/null', 'wt')
51
52
53class Setup:
54
55 def __init__(self, args, env=None):
56 self.args = args
57 self.env = env
58
José Fonsecabcca5f72011-09-06 00:07:41 +010059 def retrace(self):
José Fonseca0b956fd2011-06-04 22:51:45 +010060 cmd = [
61 options.retrace,
José Fonsecabcca5f72011-09-06 00:07:41 +010062 '-s', '-',
José Fonseca0b956fd2011-06-04 22:51:45 +010063 '-S', options.snapshot_frequency,
64 ] + self.args
65 p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
66 return p
67
68 def dump_state(self, call_no):
69 '''Get the state dump at the specified call no.'''
70
71 cmd = [
72 options.retrace,
73 '-D', str(call_no),
74 ] + self.args
75 p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
76 state = jsondiff.load(p.stdout)
77 p.wait()
78 return state
79
80
José Fonsecabcca5f72011-09-06 00:07:41 +010081def read_pnm(stream):
82 '''Read a PNM from the stream, and return the image object, and the comment.'''
83
84 magic = stream.readline()
85 if not magic:
86 return None, None
87 assert magic.rstrip() == 'P6'
88 comment = ''
89 line = stream.readline()
90 while line.startswith('#'):
91 comment += line[1:]
92 line = stream.readline()
93 width, height = map(int, line.strip().split())
94 maximum = int(stream.readline().strip())
95 assert maximum == 255
96 data = stream.read(height * width * 3)
97 image = Image.frombuffer('RGB', (width, height), data, 'raw', 'RGB', 0, 1)
98 return image, comment
99
100
José Fonseca0b956fd2011-06-04 22:51:45 +0100101def diff_state(setup, ref_call_no, src_call_no):
José Fonsecabcca5f72011-09-06 00:07:41 +0100102 '''Compare the state between two calls.'''
103
José Fonseca0b956fd2011-06-04 22:51:45 +0100104 ref_state = setup.dump_state(ref_call_no)
105 src_state = setup.dump_state(src_call_no)
106 sys.stdout.flush()
107 differ = jsondiff.Differ(sys.stdout)
108 differ.visit(ref_state, src_state)
109 sys.stdout.write('\n')
110
111
112def parse_env(optparser, entries):
113 '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
114
115 env = os.environ.copy()
116 for entry in entries:
117 try:
118 name, var = entry.split('=', 1)
119 except Exception:
120 optparser.error('invalid environment entry %r' % entry)
121 env[name] = var
122 return env
123
124
125def main():
126 '''Main program.
127 '''
128
129 global options
130
131 # Parse command line options
132 optparser = optparse.OptionParser(
133 usage='\n\t%prog [options] -- [glretrace options] <trace>',
134 version='%%prog')
135 optparser.add_option(
136 '-r', '--retrace', metavar='PROGRAM',
137 type='string', dest='retrace', default='glretrace',
138 help='retrace command [default: %default]')
139 optparser.add_option(
140 '--ref-env', metavar='NAME=VALUE',
141 type='string', action='append', dest='ref_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100142 help='add variable to reference environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100143 optparser.add_option(
144 '--src-env', metavar='NAME=VALUE',
145 type='string', action='append', dest='src_env', default=[],
José Fonsecabcca5f72011-09-06 00:07:41 +0100146 help='add variable to source environment')
José Fonseca0b956fd2011-06-04 22:51:45 +0100147 optparser.add_option(
148 '--diff-prefix', metavar='PATH',
149 type='string', dest='diff_prefix', default='.',
José Fonsecabcca5f72011-09-06 00:07:41 +0100150 help='prefix for the difference images')
José Fonseca0b956fd2011-06-04 22:51:45 +0100151 optparser.add_option(
152 '-t', '--threshold', metavar='BITS',
153 type="float", dest="threshold", default=12.0,
154 help="threshold precision [default: %default]")
155 optparser.add_option(
156 '-S', '--snapshot-frequency', metavar='FREQUENCY',
157 type="string", dest="snapshot_frequency", default='draw',
José Fonsecabcca5f72011-09-06 00:07:41 +0100158 help="snapshot frequency: frame, framebuffer, or draw [default: %default]")
José Fonseca0b956fd2011-06-04 22:51:45 +0100159
160 (options, args) = optparser.parse_args(sys.argv[1:])
161 ref_env = parse_env(optparser, options.ref_env)
162 src_env = parse_env(optparser, options.src_env)
163 if not args:
164 optparser.error("incorrect number of arguments")
165
166 ref_setup = Setup(args, ref_env)
167 src_setup = Setup(args, src_env)
168
169 image_re = re.compile('^Wrote (.*\.png)$')
170
171 last_good = -1
172 last_bad = -1
José Fonsecabcca5f72011-09-06 00:07:41 +0100173 ref_proc = ref_setup.retrace()
José Fonseca0b956fd2011-06-04 22:51:45 +0100174 try:
José Fonsecabcca5f72011-09-06 00:07:41 +0100175 src_proc = src_setup.retrace()
José Fonseca0b956fd2011-06-04 22:51:45 +0100176 try:
José Fonsecabcca5f72011-09-06 00:07:41 +0100177 while True:
178 # Get the reference image
179 ref_image, ref_comment = read_pnm(ref_proc.stdout)
180 if ref_image is None:
181 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100182
José Fonsecabcca5f72011-09-06 00:07:41 +0100183 # Get the source image
184 src_image, src_comment = read_pnm(src_proc.stdout)
185 if src_image is None:
186 break
José Fonseca0b956fd2011-06-04 22:51:45 +0100187
José Fonsecabcca5f72011-09-06 00:07:41 +0100188 assert ref_comment == src_comment
José Fonseca0b956fd2011-06-04 22:51:45 +0100189
José Fonsecabcca5f72011-09-06 00:07:41 +0100190 call_no = int(ref_comment.strip())
191
192 # Compare the two images
193 comparer = Comparer(ref_image, src_image)
194 precision = comparer.precision()
195
196 sys.stdout.write('%u %f\n' % (call_no, precision))
197
198 if precision < options.threshold:
199 if options.diff_prefix:
200 prefix = os.path.join(options.diff_prefix, '%010u' % call_no)
201 prefix_dir = os.path.dirname(prefix)
202 if not os.path.isdir(prefix_dir):
203 os.makedirs(prefix_dir)
204 ref_image.save(prefix + '.ref.png')
205 src_image.save(prefix + '.src.png')
206 comparer.write_diff(prefix + '.diff.png')
207 if last_bad < last_good:
208 diff_state(src_setup, last_good, call_no)
209 last_bad = call_no
210 else:
211 last_good = call_no
212
213 sys.stdout.flush()
José Fonseca0b956fd2011-06-04 22:51:45 +0100214 finally:
José Fonsecabcca5f72011-09-06 00:07:41 +0100215 src_proc.terminate()
José Fonseca0b956fd2011-06-04 22:51:45 +0100216 finally:
José Fonsecabcca5f72011-09-06 00:07:41 +0100217 ref_proc.terminate()
José Fonseca0b956fd2011-06-04 22:51:45 +0100218
219
220if __name__ == '__main__':
221 main()