hgbook

view en/examples/run-example @ 146:65f6f9d18fa1

Oops! I forgot that I need the undoctored output files in the book!
Now they're named "*.lxo", instead of "*.out". Ugh.
author Bryan O'Sullivan <bos@serpentine.com>
date Tue Mar 06 21:55:48 2007 -0800 (2007-03-06)
parents d9f332f85673
children e7f48702d409
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 assert os.sep not in out
207 if ofp is not None:
208 ofp.close()
209 err = self.rename_output(ofp_basename, ignore)
210 if out:
211 ofp_basename = '%s.%s' % (self.name, out)
212 ofp = open(ofp_basename + '.tmp', 'w')
213 else:
214 ofp = None
215 elif pi == 'ignore':
216 ignore.append(rest)
217 elif hunk.strip():
218 # it's something we should execute
219 newps, output = self.sendreceive(hunk)
220 if not ofp:
221 continue
222 # first, print the command we ran
223 if not hunk.startswith('#'):
224 nl = hunk.endswith('\n')
225 hunk = ('%s \\textbf{%s}' %
226 (prompts[ps],
227 tex_escape(hunk.rstrip('\n'))))
228 if nl: hunk += '\n'
229 ofp.write(hunk)
230 # then its output
231 ofp.write(tex_escape(output))
232 ps = newps
233 self.status('\n')
234 except:
235 print >> sys.stderr, '(killed)'
236 os.kill(self.pid, signal.SIGKILL)
237 pid, rc = os.wait()
238 raise
239 else:
240 try:
241 ps, output = self.sendreceive('exit\n')
242 if ofp is not None:
243 ofp.write(output)
244 ofp.close()
245 err = self.rename_output(ofp_basename, ignore)
246 os.close(self.cfd)
247 except IOError:
248 pass
249 os.kill(self.pid, signal.SIGTERM)
250 pid, rc = os.wait()
251 if rc:
252 if os.WIFEXITED(rc):
253 print >> sys.stderr, '(exit %s)' % os.WEXITSTATUS(rc)
254 elif os.WIFSIGNALED(rc):
255 print >> sys.stderr, '(signal %s)' % os.WTERMSIG(rc)
256 else:
257 open(self.name + '.run', 'w')
258 return rc or err
259 finally:
260 shutil.rmtree(tmpdir)
262 def rename_output(self, base, ignore):
263 mangle_re = re.compile('(?:' + '|'.join(ignore) + ')')
264 def mangle(s):
265 return mangle_re.sub('', s)
266 def matchfp(fp1, fp2):
267 while True:
268 s1 = mangle(fp1.readline())
269 s2 = mangle(fp2.readline())
270 if cmp(s1, s2):
271 break
272 if not s1:
273 return True
274 return False
276 oldname = base + '.out'
277 tmpname = base + '.tmp'
278 errname = base + '.err'
279 errfp = open(errname, 'w+')
280 for line in open(tmpname):
281 errfp.write(mangle_re.sub('', line))
282 os.rename(tmpname, base + '.lxo')
283 errfp.seek(0)
284 try:
285 oldfp = open(oldname)
286 except IOError, err:
287 if err.errno != errno.ENOENT:
288 raise
289 os.rename(errname, oldname)
290 return
291 if matchfp(oldfp, errfp):
292 os.unlink(errname)
293 else:
294 print >> sys.stderr, '\nOutput of %s has changed!' % base
295 os.system('diff -u %s %s 1>&2' % (oldname, errname))
296 return True
298 def main(path='.'):
299 opts, args = getopt.getopt(sys.argv[1:], 'v', ['verbose'])
300 verbose = False
301 for o, a in opts:
302 if o in ('-v', '--verbose'):
303 verbose = True
304 errs = 0
305 if args:
306 for a in args:
307 try:
308 st = os.lstat(a)
309 except OSError, err:
310 print >> sys.stderr, '%s: %s' % (a, err.strerror)
311 errs += 1
312 continue
313 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
314 if example(a, verbose).run():
315 errs += 1
316 else:
317 print >> sys.stderr, '%s: not a file, or not executable' % a
318 errs += 1
319 return errs
320 for name in os.listdir(path):
321 if name == 'run-example' or name.startswith('.'): continue
322 if name.endswith('.out') or name.endswith('~'): continue
323 if name.endswith('.run'): continue
324 pathname = os.path.join(path, name)
325 try:
326 st = os.lstat(pathname)
327 except OSError, err:
328 # could be an output file that was removed while we ran
329 if err.errno != errno.ENOENT:
330 raise
331 continue
332 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
333 if example(pathname, verbose).run():
334 errs += 1
335 print >> open(os.path.join(path, '.run'), 'w'), time.asctime()
336 return errs
338 if __name__ == '__main__':
339 sys.exit(main())