hgbook

view en/examples/run-example @ 545:d8913b7869b5

Add --keep option to run-example
author Bryan O'Sullivan <bos@serpentine.com>
date Thu Jan 29 22:11:10 2009 -0800 (2009-01-29)
parents 1a55ba6ceca1
children 0d5935744f87
line source
1 #!/usr/bin/env python
2 #
3 # This program takes something that resembles a shell script and runs
4 # it, spitting input (commands from the script) and output into text
5 # files, for use in examples.
7 import cStringIO
8 import errno
9 import getopt
10 import os
11 import pty
12 import re
13 import select
14 import shutil
15 import signal
16 import stat
17 import sys
18 import tempfile
19 import time
21 tex_subs = {
22 '\\': '\\textbackslash{}',
23 '{': '\\{',
24 '}': '\\}',
25 }
27 def gensubs(s):
28 start = 0
29 for i, c in enumerate(s):
30 sub = tex_subs.get(c)
31 if sub:
32 yield s[start:i]
33 start = i + 1
34 yield sub
35 yield s[start:]
37 def tex_escape(s):
38 return ''.join(gensubs(s))
40 def maybe_unlink(name):
41 try:
42 os.unlink(name)
43 return True
44 except OSError, err:
45 if err.errno != errno.ENOENT:
46 raise
47 return False
49 def find_path_to(program):
50 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
51 name = os.path.join(p, program)
52 if os.access(name, os.X_OK):
53 return p
54 return None
56 class example:
57 shell = '/usr/bin/env bash'
58 ps1 = '__run_example_ps1__ '
59 ps2 = '__run_example_ps2__ '
60 pi_re = re.compile(r'#\$\s*(name|ignore):\s*(.*)$')
62 timeout = 10
64 def __init__(self, name, verbose, keep_change):
65 self.name = name
66 self.verbose = verbose
67 self.keep_change = keep_change
68 self.poll = select.poll()
70 def parse(self):
71 '''yield each hunk of input from the file.'''
72 fp = open(self.name)
73 cfp = cStringIO.StringIO()
74 for line in fp:
75 cfp.write(line)
76 if not line.rstrip().endswith('\\'):
77 yield cfp.getvalue()
78 cfp.seek(0)
79 cfp.truncate()
81 def status(self, s):
82 sys.stdout.write(s)
83 if not s.endswith('\n'):
84 sys.stdout.flush()
86 def send(self, s):
87 if self.verbose:
88 print >> sys.stderr, '>', self.debugrepr(s)
89 while s:
90 count = os.write(self.cfd, s)
91 s = s[count:]
93 def debugrepr(self, s):
94 rs = repr(s)
95 limit = 60
96 if len(rs) > limit:
97 return ('%s%s ... [%d bytes]' % (rs[:limit], rs[0], len(s)))
98 else:
99 return rs
101 timeout = 5
103 def read(self, hint):
104 events = self.poll.poll(self.timeout * 1000)
105 if not events:
106 print >> sys.stderr, ('[%stimed out after %d seconds]' %
107 (hint, self.timeout))
108 os.kill(self.pid, signal.SIGHUP)
109 return ''
110 return os.read(self.cfd, 1024)
112 def receive(self, hint):
113 out = cStringIO.StringIO()
114 while True:
115 try:
116 if self.verbose:
117 sys.stderr.write('< ')
118 s = self.read(hint)
119 except OSError, err:
120 if err.errno == errno.EIO:
121 return '', ''
122 raise
123 if self.verbose:
124 print >> sys.stderr, self.debugrepr(s)
125 out.write(s)
126 s = out.getvalue()
127 if s.endswith(self.ps1):
128 return self.ps1, s.replace('\r\n', '\n')[:-len(self.ps1)]
129 if s.endswith(self.ps2):
130 return self.ps2, s.replace('\r\n', '\n')[:-len(self.ps2)]
132 def sendreceive(self, s, hint):
133 self.send(s)
134 ps, r = self.receive(hint)
135 if r.startswith(s):
136 r = r[len(s):]
137 return ps, r
139 def run(self):
140 ofp = None
141 basename = os.path.basename(self.name)
142 self.status('running %s ' % basename)
143 tmpdir = tempfile.mkdtemp(prefix=basename)
145 # remove the marker file that we tell make to use to see if
146 # this run succeeded
147 maybe_unlink(self.name + '.run')
149 rcfile = os.path.join(tmpdir, '.hgrc')
150 rcfp = open(rcfile, 'w')
151 print >> rcfp, '[ui]'
152 print >> rcfp, "username = Bryan O'Sullivan <bos@serpentine.com>"
154 rcfile = os.path.join(tmpdir, '.bashrc')
155 rcfp = open(rcfile, 'w')
156 print >> rcfp, 'PS1="%s"' % self.ps1
157 print >> rcfp, 'PS2="%s"' % self.ps2
158 print >> rcfp, 'unset HISTFILE'
159 path = ['/usr/bin', '/bin']
160 hg = find_path_to('hg')
161 if hg and hg not in path:
162 path.append(hg)
163 def re_export(envar):
164 v = os.getenv(envar)
165 if v is not None:
166 print >> rcfp, 'export ' + envar + '=' + v
167 print >> rcfp, 'export PATH=' + ':'.join(path)
168 re_export('PYTHONPATH')
169 print >> rcfp, 'export EXAMPLE_DIR="%s"' % os.getcwd()
170 print >> rcfp, 'export HGMERGE=merge'
171 print >> rcfp, 'export LANG=C'
172 print >> rcfp, 'export LC_ALL=C'
173 print >> rcfp, 'export TZ=GMT'
174 print >> rcfp, 'export HGRC="%s/.hgrc"' % tmpdir
175 print >> rcfp, 'export HGRCPATH=$HGRC'
176 print >> rcfp, 'cd %s' % tmpdir
177 rcfp.close()
178 sys.stdout.flush()
179 sys.stderr.flush()
180 self.pid, self.cfd = pty.fork()
181 if self.pid == 0:
182 cmdline = ['/usr/bin/env', '-i', 'bash', '--noediting',
183 '--noprofile', '--norc']
184 try:
185 os.execv(cmdline[0], cmdline)
186 except OSError, err:
187 print >> sys.stderr, '%s: %s' % (cmdline[0], err.strerror)
188 sys.stderr.flush()
189 os._exit(0)
190 self.poll.register(self.cfd, select.POLLIN | select.POLLERR |
191 select.POLLHUP)
193 prompts = {
194 '': '',
195 self.ps1: '$',
196 self.ps2: '>',
197 }
199 ignore = [
200 r'\d+:[0-9a-f]{12}', # changeset number:hash
201 r'[0-9a-f]{40}', # long changeset hash
202 r'[0-9a-f]{12}', # short changeset hash
203 r'^(?:---|\+\+\+) .*', # diff header with dates
204 r'^date:.*', # date
205 #r'^diff -r.*', # "diff -r" is followed by hash
206 r'^# Date \d+ \d+', # hg patch header
207 ]
209 err = False
210 read_hint = ''
212 try:
213 try:
214 # eat first prompt string from shell
215 self.read(read_hint)
216 # setup env and prompt
217 ps, output = self.sendreceive('source %s\n' % rcfile,
218 read_hint)
219 for hunk in self.parse():
220 # is this line a processing instruction?
221 m = self.pi_re.match(hunk)
222 if m:
223 pi, rest = m.groups()
224 if pi == 'name':
225 self.status('.')
226 out = rest
227 if out in ('err', 'lxo', 'out', 'run', 'tmp'):
228 print >> sys.stderr, ('%s: illegal section '
229 'name %r' %
230 (self.name, out))
231 return 1
232 assert os.sep not in out
233 if ofp is not None:
234 ofp.close()
235 err |= self.rename_output(ofp_basename, ignore)
236 if out:
237 ofp_basename = '%s.%s' % (self.name, out)
238 read_hint = ofp_basename + ' '
239 ofp = open(ofp_basename + '.tmp', 'w')
240 else:
241 ofp = None
242 elif pi == 'ignore':
243 ignore.append(rest)
244 elif hunk.strip():
245 # it's something we should execute
246 newps, output = self.sendreceive(hunk, read_hint)
247 if not ofp:
248 continue
249 # first, print the command we ran
250 if not hunk.startswith('#'):
251 nl = hunk.endswith('\n')
252 hunk = ('%s \\textbf{%s}' %
253 (prompts[ps],
254 tex_escape(hunk.rstrip('\n'))))
255 if nl: hunk += '\n'
256 ofp.write(hunk)
257 # then its output
258 ofp.write(tex_escape(output))
259 ps = newps
260 self.status('\n')
261 except:
262 print >> sys.stderr, '(killed)'
263 os.kill(self.pid, signal.SIGKILL)
264 pid, rc = os.wait()
265 raise
266 else:
267 try:
268 ps, output = self.sendreceive('exit\n', read_hint)
269 if ofp is not None:
270 ofp.write(output)
271 ofp.close()
272 err |= self.rename_output(ofp_basename, ignore)
273 os.close(self.cfd)
274 except IOError:
275 pass
276 os.kill(self.pid, signal.SIGTERM)
277 pid, rc = os.wait()
278 err = err or rc
279 if err:
280 if os.WIFEXITED(rc):
281 print >> sys.stderr, '(exit %s)' % os.WEXITSTATUS(rc)
282 elif os.WIFSIGNALED(rc):
283 print >> sys.stderr, '(signal %s)' % os.WTERMSIG(rc)
284 else:
285 open(self.name + '.run', 'w')
286 return err
287 finally:
288 shutil.rmtree(tmpdir)
290 def rename_output(self, base, ignore):
291 mangle_re = re.compile('(?:' + '|'.join(ignore) + ')')
292 def mangle(s):
293 return mangle_re.sub('', s)
294 def matchfp(fp1, fp2):
295 while True:
296 s1 = mangle(fp1.readline())
297 s2 = mangle(fp2.readline())
298 if cmp(s1, s2):
299 break
300 if not s1:
301 return True
302 return False
304 oldname = base + '.out'
305 tmpname = base + '.tmp'
306 errname = base + '.err'
307 errfp = open(errname, 'w+')
308 for line in open(tmpname):
309 errfp.write(mangle_re.sub('', line))
310 os.rename(tmpname, base + '.lxo')
311 errfp.seek(0)
312 try:
313 oldfp = open(oldname)
314 except IOError, err:
315 if err.errno != errno.ENOENT:
316 raise
317 os.rename(errname, oldname)
318 return False
319 if matchfp(oldfp, errfp):
320 os.unlink(errname)
321 return False
322 else:
323 print >> sys.stderr, '\nOutput of %s has changed!' % base
324 if self.keep_change:
325 os.rename(errname, oldname)
326 return False
327 else:
328 os.system('diff -u %s %s 1>&2' % (oldname, errname))
329 return True
331 def print_help(exit, msg=None):
332 if msg:
333 print >> sys.stderr, 'Error:', msg
334 print >> sys.stderr, 'Usage: run-example [options] [test...]'
335 print >> sys.stderr, 'Options:'
336 print >> sys.stderr, ' -a --all run all tests in this directory'
337 print >> sys.stderr, ' -h --help print this help message'
338 print >> sys.stderr, ' --help keep new output as desired output'
339 print >> sys.stderr, ' -v --verbose display extra debug output'
340 sys.exit(exit)
342 def main(path='.'):
343 opts, args = getopt.getopt(sys.argv[1:], '?ahv',
344 ['all', 'help', 'keep', 'verbose'])
345 verbose = False
346 run_all = False
347 keep_change = False
348 for o, a in opts:
349 if o in ('-h', '-?', '--help'):
350 print_help(0)
351 if o in ('-a', '--all'):
352 run_all = True
353 if o in ('--keep',):
354 keep_change = True
355 if o in ('-v', '--verbose'):
356 verbose = True
357 errs = 0
358 if args:
359 for a in args:
360 try:
361 st = os.lstat(a)
362 except OSError, err:
363 print >> sys.stderr, '%s: %s' % (a, err.strerror)
364 errs += 1
365 continue
366 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
367 if example(a, verbose, keep_change).run():
368 errs += 1
369 else:
370 print >> sys.stderr, '%s: not a file, or not executable' % a
371 errs += 1
372 elif run_all:
373 names = os.listdir(path)
374 names.sort()
375 for name in names:
376 if name == 'run-example' or name.startswith('.'): continue
377 if name.endswith('.out') or name.endswith('~'): continue
378 if name.endswith('.run'): continue
379 pathname = os.path.join(path, name)
380 try:
381 st = os.lstat(pathname)
382 except OSError, err:
383 # could be an output file that was removed while we ran
384 if err.errno != errno.ENOENT:
385 raise
386 continue
387 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
388 if example(pathname, verbose, keep_change).run():
389 errs += 1
390 print >> open(os.path.join(path, '.run'), 'w'), time.asctime()
391 else:
392 print_help(1, msg='no test names given, and --all not provided')
393 return errs
395 if __name__ == '__main__':
396 try:
397 sys.exit(main())
398 except KeyboardInterrupt:
399 print >> sys.stderr, 'interrupted!'
400 sys.exit(1)