hgbook

view en/examples/run-example @ 614:c8680784cdbb

Make builds more reliable.
author Bryan O'Sullivan <bos@serpentine.com>
date Tue Mar 31 15:35:44 2009 -0700 (2009-03-31)
parents a2923aa93da9
children 3b640272a966
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 glob
11 import os
12 import pty
13 import re
14 import select
15 import shutil
16 import signal
17 import stat
18 import sys
19 import tempfile
20 import time
22 xml_subs = {
23 '<': '&lt;',
24 '>': '&gt;',
25 '&': '&amp;',
26 }
28 def gensubs(s):
29 start = 0
30 for i, c in enumerate(s):
31 sub = xml_subs.get(c)
32 if sub:
33 yield s[start:i]
34 start = i + 1
35 yield sub
36 yield s[start:]
38 def xml_escape(s):
39 return ''.join(gensubs(s))
41 def maybe_unlink(name):
42 try:
43 os.unlink(name)
44 return True
45 except OSError, err:
46 if err.errno != errno.ENOENT:
47 raise
48 return False
50 def find_path_to(program):
51 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
52 name = os.path.join(p, program)
53 if os.access(name, os.X_OK):
54 return p
55 return None
57 def result_name(name):
58 return os.path.normpath(os.path.join('results', name.replace(os.sep, '-')))
60 def wopen(name):
61 path = os.path.dirname(name)
62 if path:
63 try:
64 os.makedirs(path)
65 except OSError, err:
66 if err.errno != errno.EEXIST:
67 raise
68 return open(name, 'w')
70 class example:
71 entities = dict.fromkeys(l.rstrip() for l in open('auto-snippets.xml'))
73 def __init__(self, name, verbose, keep_change):
74 self.name = os.path.normpath(name)
75 self.verbose = verbose
76 self.keep_change = keep_change
78 def status(self, s):
79 sys.stdout.write(s)
80 if not s.endswith('\n'):
81 sys.stdout.flush()
83 def rename_output(self, base, ignore=[]):
84 mangle_re = re.compile('(?:' + '|'.join(ignore) + ')')
85 def mangle(s):
86 return mangle_re.sub('', s)
87 def matchfp(fp1, fp2):
88 while True:
89 s1 = mangle(fp1.readline())
90 s2 = mangle(fp2.readline())
91 if cmp(s1, s2):
92 break
93 if not s1:
94 return True
95 return False
97 oldname = result_name(base + '.out')
98 tmpname = result_name(base + '.tmp')
99 errname = result_name(base + '.err')
100 errfp = open(errname, 'w+')
101 for line in open(tmpname):
102 errfp.write(mangle_re.sub('', line))
103 os.rename(tmpname, result_name(base + '.lxo'))
104 errfp.seek(0)
105 try:
106 oldfp = open(oldname)
107 except IOError, err:
108 if err.errno != errno.ENOENT:
109 raise
110 os.rename(errname, oldname)
111 return False
112 if matchfp(oldfp, errfp):
113 os.unlink(errname)
114 return False
115 else:
116 print >> sys.stderr, '\nOutput of %s has changed!' % base
117 if self.keep_change:
118 os.rename(errname, oldname)
119 return False
120 else:
121 os.system('diff -u %s %s 1>&2' % (oldname, errname))
122 return True
124 class static_example(example):
125 def run(self):
126 self.status('running %s\n' % self.name)
127 s = open(self.name).read().rstrip()
128 s = s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
129 ofp = wopen(result_name(self.name + '.tmp'))
130 ofp.write('<!-- BEGIN %s -->\n' % self.name)
131 ofp.write('<programlisting>')
132 ofp.write(s)
133 ofp.write('</programlisting>\n')
134 ofp.write('<!-- END %s -->\n' % self.name)
135 ofp.close()
136 self.rename_output(self.name)
137 norm = self.name.replace(os.sep, '-')
138 example.entities[
139 '<!ENTITY %s SYSTEM "results/%s.lxo">' % (norm, norm)] = 1
142 class shell_example(example):
143 shell = '/usr/bin/env bash'
144 ps1 = '__run_example_ps1__ '
145 ps2 = '__run_example_ps2__ '
146 pi_re = re.compile(r'#\$\s*(name|ignore):\s*(.*)$')
148 timeout = 10
150 def __init__(self, name, verbose, keep_change):
151 example.__init__(self, name, verbose, keep_change)
152 self.poll = select.poll()
154 def parse(self):
155 '''yield each hunk of input from the file.'''
156 fp = open(self.name)
157 cfp = cStringIO.StringIO()
158 for line in fp:
159 cfp.write(line)
160 if not line.rstrip().endswith('\\'):
161 yield cfp.getvalue()
162 cfp.seek(0)
163 cfp.truncate()
165 def send(self, s):
166 if self.verbose:
167 print >> sys.stderr, '>', self.debugrepr(s)
168 while s:
169 count = os.write(self.cfd, s)
170 s = s[count:]
172 def debugrepr(self, s):
173 rs = repr(s)
174 limit = 60
175 if len(rs) > limit:
176 return ('%s%s ... [%d bytes]' % (rs[:limit], rs[0], len(s)))
177 else:
178 return rs
180 timeout = 5
182 def read(self, hint):
183 events = self.poll.poll(self.timeout * 1000)
184 if not events:
185 print >> sys.stderr, ('[%stimed out after %d seconds]' %
186 (hint, self.timeout))
187 os.kill(self.pid, signal.SIGHUP)
188 return ''
189 return os.read(self.cfd, 1024)
191 def receive(self, hint):
192 out = cStringIO.StringIO()
193 while True:
194 try:
195 if self.verbose:
196 sys.stderr.write('< ')
197 s = self.read(hint)
198 except OSError, err:
199 if err.errno == errno.EIO:
200 return '', ''
201 raise
202 if self.verbose:
203 print >> sys.stderr, self.debugrepr(s)
204 out.write(s)
205 s = out.getvalue()
206 if s.endswith(self.ps1):
207 return self.ps1, s.replace('\r\n', '\n')[:-len(self.ps1)]
208 if s.endswith(self.ps2):
209 return self.ps2, s.replace('\r\n', '\n')[:-len(self.ps2)]
211 def sendreceive(self, s, hint):
212 self.send(s)
213 ps, r = self.receive(hint)
214 if r.startswith(s):
215 r = r[len(s):]
216 return ps, r
218 def run(self):
219 ofp = None
220 basename = os.path.basename(self.name)
221 self.status('running %s ' % basename)
222 tmpdir = tempfile.mkdtemp(prefix=basename)
224 # remove the marker file that we tell make to use to see if
225 # this run succeeded
226 maybe_unlink(self.name + '.run')
228 rcfile = os.path.join(tmpdir, '.hgrc')
229 rcfp = wopen(rcfile)
230 print >> rcfp, '[ui]'
231 print >> rcfp, "username = Bryan O'Sullivan <bos@serpentine.com>"
233 rcfile = os.path.join(tmpdir, '.bashrc')
234 rcfp = wopen(rcfile)
235 print >> rcfp, 'PS1="%s"' % self.ps1
236 print >> rcfp, 'PS2="%s"' % self.ps2
237 print >> rcfp, 'unset HISTFILE'
238 path = ['/usr/bin', '/bin']
239 hg = find_path_to('hg')
240 if hg and hg not in path:
241 path.append(hg)
242 def re_export(envar):
243 v = os.getenv(envar)
244 if v is not None:
245 print >> rcfp, 'export ' + envar + '=' + v
246 print >> rcfp, 'export PATH=' + ':'.join(path)
247 re_export('PYTHONPATH')
248 print >> rcfp, 'export EXAMPLE_DIR="%s"' % os.getcwd()
249 print >> rcfp, 'export HGMERGE=merge'
250 print >> rcfp, 'export LANG=C'
251 print >> rcfp, 'export LC_ALL=C'
252 print >> rcfp, 'export TZ=GMT'
253 print >> rcfp, 'export HGRC="%s/.hgrc"' % tmpdir
254 print >> rcfp, 'export HGRCPATH=$HGRC'
255 print >> rcfp, 'cd %s' % tmpdir
256 rcfp.close()
257 sys.stdout.flush()
258 sys.stderr.flush()
259 self.pid, self.cfd = pty.fork()
260 if self.pid == 0:
261 cmdline = ['/usr/bin/env', '-i', 'bash', '--noediting',
262 '--noprofile', '--norc']
263 try:
264 os.execv(cmdline[0], cmdline)
265 except OSError, err:
266 print >> sys.stderr, '%s: %s' % (cmdline[0], err.strerror)
267 sys.stderr.flush()
268 os._exit(0)
269 self.poll.register(self.cfd, select.POLLIN | select.POLLERR |
270 select.POLLHUP)
272 prompts = {
273 '': '',
274 self.ps1: '$',
275 self.ps2: '>',
276 }
278 ignore = [
279 r'\d+:[0-9a-f]{12}', # changeset number:hash
280 r'[0-9a-f]{40}', # long changeset hash
281 r'[0-9a-f]{12}', # short changeset hash
282 r'^(?:---|\+\+\+) .*', # diff header with dates
283 r'^date:.*', # date
284 #r'^diff -r.*', # "diff -r" is followed by hash
285 r'^# Date \d+ \d+', # hg patch header
286 ]
288 err = False
289 read_hint = ''
291 try:
292 try:
293 # eat first prompt string from shell
294 self.read(read_hint)
295 # setup env and prompt
296 ps, output = self.sendreceive('source %s\n' % rcfile,
297 read_hint)
298 for hunk in self.parse():
299 # is this line a processing instruction?
300 m = self.pi_re.match(hunk)
301 if m:
302 pi, rest = m.groups()
303 if pi == 'name':
304 self.status('.')
305 out = rest
306 if out in ('err', 'lxo', 'out', 'run', 'tmp'):
307 print >> sys.stderr, ('%s: illegal section '
308 'name %r' %
309 (self.name, out))
310 return 1
311 assert os.sep not in out
312 if ofp is not None:
313 ofp.write('</screen>\n')
314 ofp.write('<!-- END %s -->\n' % ofp_basename)
315 ofp.close()
316 err |= self.rename_output(ofp_basename, ignore)
317 if out:
318 ofp_basename = '%s.%s' % (self.name, out)
319 norm = os.path.normpath(ofp_basename)
320 example.entities[
321 '<!ENTITY interaction.%s '
322 'SYSTEM "results/%s.lxo">'
323 % (norm, norm)] = 1
324 read_hint = ofp_basename + ' '
325 ofp = wopen(result_name(ofp_basename + '.tmp'))
326 ofp.write('<!-- BEGIN %s -->\n' % ofp_basename)
327 ofp.write('<screen>')
328 else:
329 ofp = None
330 elif pi == 'ignore':
331 ignore.append(rest)
332 elif hunk.strip():
333 # it's something we should execute
334 newps, output = self.sendreceive(hunk, read_hint)
335 if not ofp:
336 continue
337 # first, print the command we ran
338 if not hunk.startswith('#'):
339 nl = hunk.endswith('\n')
340 hunk = ('<prompt>%s</prompt> '
341 '<userinput>%s</userinput>' %
342 (prompts[ps],
343 xml_escape(hunk.rstrip('\n'))))
344 if nl: hunk += '\n'
345 ofp.write(hunk)
346 # then its output
347 ofp.write(xml_escape(output))
348 ps = newps
349 self.status('\n')
350 except:
351 print >> sys.stderr, '(killed)'
352 os.kill(self.pid, signal.SIGKILL)
353 pid, rc = os.wait()
354 raise
355 else:
356 try:
357 ps, output = self.sendreceive('exit\n', read_hint)
358 if ofp is not None:
359 ofp.write(output)
360 ofp.write('</screen>\n')
361 ofp.write('<!-- END %s -->\n' % ofp_basename)
362 ofp.close()
363 err |= self.rename_output(ofp_basename, ignore)
364 os.close(self.cfd)
365 except IOError:
366 pass
367 os.kill(self.pid, signal.SIGTERM)
368 pid, rc = os.wait()
369 err = err or rc
370 if err:
371 if os.WIFEXITED(rc):
372 print >> sys.stderr, '(exit %s)' % os.WEXITSTATUS(rc)
373 elif os.WIFSIGNALED(rc):
374 print >> sys.stderr, '(signal %s)' % os.WTERMSIG(rc)
375 else:
376 wopen(result_name(self.name + '.run'))
377 return err
378 finally:
379 shutil.rmtree(tmpdir)
381 def print_help(exit, msg=None):
382 if msg:
383 print >> sys.stderr, 'Error:', msg
384 print >> sys.stderr, 'Usage: run-example [options] [test...]'
385 print >> sys.stderr, 'Options:'
386 print >> sys.stderr, ' -a --all run all examples in this directory'
387 print >> sys.stderr, ' -h --help print this help message'
388 print >> sys.stderr, ' --keep keep new output as desired output'
389 print >> sys.stderr, ' -v --verbose display extra debug output'
390 sys.exit(exit)
392 def main(path='.'):
393 if os.path.realpath(path).split(os.sep)[-1] != 'examples':
394 print >> sys.stderr, 'Not being run from the examples directory!'
395 sys.exit(1)
397 opts, args = getopt.getopt(sys.argv[1:], '?ahv',
398 ['all', 'help', 'keep', 'verbose'])
399 verbose = False
400 run_all = False
401 keep_change = False
403 for o, a in opts:
404 if o in ('-h', '-?', '--help'):
405 print_help(0)
406 if o in ('-a', '--all'):
407 run_all = True
408 if o in ('--keep',):
409 keep_change = True
410 if o in ('-v', '--verbose'):
411 verbose = True
412 errs = 0
413 if args:
414 for a in args:
415 try:
416 st = os.lstat(a)
417 except OSError, err:
418 print >> sys.stderr, '%s: %s' % (a, err.strerror)
419 errs += 1
420 continue
421 if stat.S_ISREG(st.st_mode):
422 if st.st_mode & 0111:
423 if shell_example(a, verbose, keep_change).run():
424 errs += 1
425 elif a.endswith('.lst'):
426 static_example(a, verbose, keep_change).run()
427 else:
428 print >> sys.stderr, '%s: not a file, or not executable' % a
429 errs += 1
430 elif run_all:
431 names = glob.glob("*") + glob.glob("app*/*") + glob.glob("ch*/*")
432 names.sort()
433 for name in names:
434 if name == 'run-example' or name.endswith('~'): continue
435 pathname = os.path.join(path, name)
436 try:
437 st = os.lstat(pathname)
438 except OSError, err:
439 # could be an output file that was removed while we ran
440 if err.errno != errno.ENOENT:
441 raise
442 continue
443 if stat.S_ISREG(st.st_mode):
444 if st.st_mode & 0111:
445 if shell_example(pathname, verbose, keep_change).run():
446 errs += 1
447 elif pathname.endswith('.lst'):
448 static_example(pathname, verbose, keep_change).run()
449 print >> wopen(os.path.join(path, '.run')), time.asctime()
450 else:
451 print_help(1, msg='no test names given, and --all not provided')
453 fp = wopen('auto-snippets.xml')
454 for key in sorted(example.entities.iterkeys()):
455 print >> fp, key
456 fp.close()
457 return errs
459 if __name__ == '__main__':
460 try:
461 sys.exit(main())
462 except KeyboardInterrupt:
463 print >> sys.stderr, 'interrupted!'
464 sys.exit(1)