hgbook

view en/examples/run-example @ 160:745ff473c8c4

Catch more errors.
author Bryan O'Sullivan <bos@serpentine.com>
date Mon Mar 26 21:25:34 2007 -0700 (2007-03-26)
parents 914babdc99c8
children 8f4c9ae918af
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 class example:
50 shell = '/usr/bin/env bash'
51 ps1 = '__run_example_ps1__ '
52 ps2 = '__run_example_ps2__ '
53 pi_re = re.compile(r'#\$\s*(name|ignore):\s*(.*)$')
55 timeout = 5
57 def __init__(self, name, verbose):
58 self.name = name
59 self.verbose = verbose
60 self.poll = select.poll()
62 def parse(self):
63 '''yield each hunk of input from the file.'''
64 fp = open(self.name)
65 cfp = cStringIO.StringIO()
66 for line in fp:
67 cfp.write(line)
68 if not line.rstrip().endswith('\\'):
69 yield cfp.getvalue()
70 cfp.seek(0)
71 cfp.truncate()
73 def status(self, s):
74 sys.stdout.write(s)
75 if not s.endswith('\n'):
76 sys.stdout.flush()
78 def send(self, s):
79 if self.verbose:
80 print >> sys.stderr, '>', self.debugrepr(s)
81 while s:
82 count = os.write(self.cfd, s)
83 s = s[count:]
85 def debugrepr(self, s):
86 rs = repr(s)
87 limit = 60
88 if len(rs) > limit:
89 return ('%s%s ... [%d bytes]' % (rs[:limit], rs[0], len(s)))
90 else:
91 return rs
93 timeout = 5
95 def read(self):
96 events = self.poll.poll(self.timeout * 1000)
97 if not events:
98 print >> sys.stderr, '[timed out after %d seconds]' % self.timeout
99 os.kill(self.pid, signal.SIGHUP)
100 return ''
101 return os.read(self.cfd, 1024)
103 def receive(self):
104 out = cStringIO.StringIO()
105 while True:
106 try:
107 if self.verbose:
108 sys.stderr.write('< ')
109 s = self.read()
110 except OSError, err:
111 if err.errno == errno.EIO:
112 return '', ''
113 raise
114 if self.verbose:
115 print >> sys.stderr, self.debugrepr(s)
116 out.write(s)
117 s = out.getvalue()
118 if s.endswith(self.ps1):
119 return self.ps1, s.replace('\r\n', '\n')[:-len(self.ps1)]
120 if s.endswith(self.ps2):
121 return self.ps2, s.replace('\r\n', '\n')[:-len(self.ps2)]
123 def sendreceive(self, s):
124 self.send(s)
125 ps, r = self.receive()
126 if r.startswith(s):
127 r = r[len(s):]
128 return ps, r
130 def run(self):
131 ofp = None
132 basename = os.path.basename(self.name)
133 self.status('running %s ' % basename)
134 tmpdir = tempfile.mkdtemp(prefix=basename)
136 # remove the marker file that we tell make to use to see if
137 # this run succeeded
138 maybe_unlink(self.name + '.run')
140 rcfile = os.path.join(tmpdir, '.hgrc')
141 rcfp = open(rcfile, 'w')
142 print >> rcfp, '[ui]'
143 print >> rcfp, "username = Bryan O'Sullivan <bos@serpentine.com>"
145 rcfile = os.path.join(tmpdir, '.bashrc')
146 rcfp = open(rcfile, 'w')
147 print >> rcfp, 'PS1="%s"' % self.ps1
148 print >> rcfp, 'PS2="%s"' % self.ps2
149 print >> rcfp, 'unset HISTFILE'
150 print >> rcfp, 'export EXAMPLE_DIR="%s"' % os.getcwd()
151 print >> rcfp, 'export HGMERGE=merge'
152 print >> rcfp, 'export LANG=C'
153 print >> rcfp, 'export LC_ALL=C'
154 print >> rcfp, 'export TZ=GMT'
155 print >> rcfp, 'export HGRC="%s/.hgrc"' % tmpdir
156 print >> rcfp, 'export HGRCPATH=$HGRC'
157 print >> rcfp, 'cd %s' % tmpdir
158 rcfp.close()
159 sys.stdout.flush()
160 sys.stderr.flush()
161 self.pid, self.cfd = pty.fork()
162 if self.pid == 0:
163 cmdline = ['/usr/bin/env', 'bash', '--noediting', '--noprofile',
164 '--norc']
165 try:
166 os.execv(cmdline[0], cmdline)
167 except OSError, err:
168 print >> sys.stderr, '%s: %s' % (cmdline[0], err.strerror)
169 sys.stderr.flush()
170 os._exit(0)
171 self.poll.register(self.cfd, select.POLLIN | select.POLLERR |
172 select.POLLHUP)
174 prompts = {
175 '': '',
176 self.ps1: '$',
177 self.ps2: '>',
178 }
180 ignore = [
181 r'\d+:[0-9a-f]{12}', # changeset number:hash
182 r'[0-9a-f]{40}', # long changeset hash
183 r'[0-9a-f]{12}', # short changeset hash
184 r'^(?:---|\+\+\+) .*', # diff header with dates
185 r'^date:.*', # date
186 #r'^diff -r.*', # "diff -r" is followed by hash
187 r'^# Date \d+ \d+', # hg patch header
188 ]
190 err = False
192 try:
193 try:
194 # eat first prompt string from shell
195 self.read()
196 # setup env and prompt
197 ps, output = self.sendreceive('source %s\n' % rcfile)
198 for hunk in self.parse():
199 # is this line a processing instruction?
200 m = self.pi_re.match(hunk)
201 if m:
202 pi, rest = m.groups()
203 if pi == 'name':
204 self.status('.')
205 out = rest
206 if out in ('err', 'lxo', 'out', 'run', 'tmp'):
207 print >> sys.stderr, ('%s: illegal section '
208 'name %r' %
209 (self.name, out))
210 return 1
211 assert os.sep not in out
212 if ofp is not None:
213 ofp.close()
214 err |= self.rename_output(ofp_basename, ignore)
215 if out:
216 ofp_basename = '%s.%s' % (self.name, out)
217 ofp = open(ofp_basename + '.tmp', 'w')
218 else:
219 ofp = None
220 elif pi == 'ignore':
221 ignore.append(rest)
222 elif hunk.strip():
223 # it's something we should execute
224 newps, output = self.sendreceive(hunk)
225 if not ofp:
226 continue
227 # first, print the command we ran
228 if not hunk.startswith('#'):
229 nl = hunk.endswith('\n')
230 hunk = ('%s \\textbf{%s}' %
231 (prompts[ps],
232 tex_escape(hunk.rstrip('\n'))))
233 if nl: hunk += '\n'
234 ofp.write(hunk)
235 # then its output
236 ofp.write(tex_escape(output))
237 ps = newps
238 self.status('\n')
239 except:
240 print >> sys.stderr, '(killed)'
241 os.kill(self.pid, signal.SIGKILL)
242 pid, rc = os.wait()
243 raise
244 else:
245 try:
246 ps, output = self.sendreceive('exit\n')
247 if ofp is not None:
248 ofp.write(output)
249 ofp.close()
250 err |= self.rename_output(ofp_basename, ignore)
251 os.close(self.cfd)
252 except IOError:
253 pass
254 os.kill(self.pid, signal.SIGTERM)
255 pid, rc = os.wait()
256 err = err or rc
257 if err:
258 if os.WIFEXITED(rc):
259 print >> sys.stderr, '(exit %s)' % os.WEXITSTATUS(rc)
260 elif os.WIFSIGNALED(rc):
261 print >> sys.stderr, '(signal %s)' % os.WTERMSIG(rc)
262 else:
263 open(self.name + '.run', 'w')
264 return err
265 finally:
266 shutil.rmtree(tmpdir)
268 def rename_output(self, base, ignore):
269 mangle_re = re.compile('(?:' + '|'.join(ignore) + ')')
270 def mangle(s):
271 return mangle_re.sub('', s)
272 def matchfp(fp1, fp2):
273 while True:
274 s1 = mangle(fp1.readline())
275 s2 = mangle(fp2.readline())
276 if cmp(s1, s2):
277 break
278 if not s1:
279 return True
280 return False
282 oldname = base + '.out'
283 tmpname = base + '.tmp'
284 errname = base + '.err'
285 errfp = open(errname, 'w+')
286 for line in open(tmpname):
287 errfp.write(mangle_re.sub('', line))
288 os.rename(tmpname, base + '.lxo')
289 errfp.seek(0)
290 try:
291 oldfp = open(oldname)
292 except IOError, err:
293 if err.errno != errno.ENOENT:
294 raise
295 os.rename(errname, oldname)
296 return False
297 if matchfp(oldfp, errfp):
298 os.unlink(errname)
299 return False
300 else:
301 print >> sys.stderr, '\nOutput of %s has changed!' % base
302 os.system('diff -u %s %s 1>&2' % (oldname, errname))
303 return True
305 def main(path='.'):
306 opts, args = getopt.getopt(sys.argv[1:], 'v', ['verbose'])
307 verbose = False
308 for o, a in opts:
309 if o in ('-v', '--verbose'):
310 verbose = True
311 errs = 0
312 if args:
313 for a in args:
314 try:
315 st = os.lstat(a)
316 except OSError, err:
317 print >> sys.stderr, '%s: %s' % (a, err.strerror)
318 errs += 1
319 continue
320 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
321 if example(a, verbose).run():
322 errs += 1
323 else:
324 print >> sys.stderr, '%s: not a file, or not executable' % a
325 errs += 1
326 return errs
327 for name in os.listdir(path):
328 if name == 'run-example' or name.startswith('.'): continue
329 if name.endswith('.out') or name.endswith('~'): continue
330 if name.endswith('.run'): continue
331 pathname = os.path.join(path, name)
332 try:
333 st = os.lstat(pathname)
334 except OSError, err:
335 # could be an output file that was removed while we ran
336 if err.errno != errno.ENOENT:
337 raise
338 continue
339 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
340 if example(pathname, verbose).run():
341 errs += 1
342 print >> open(os.path.join(path, '.run'), 'w'), time.asctime()
343 return errs
345 if __name__ == '__main__':
346 sys.exit(main())