hgbook

view en/examples/run-example @ 236:abebe72451d6

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