hgbook

view en/examples/run-example @ 565:8a9c66da6fcb

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