hgbook

view en/examples/run-example @ 155:914babdc99c8

run-example: better error if bogus section name found.
Fix all such bogus names in sources.
author Bryan O'Sullivan <bos@serpentine.com>
date Mon Mar 12 23:10:32 2007 -0700 (2007-03-12)
parents e7f48702d409
children 745ff473c8c4
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 if rc:
257 if os.WIFEXITED(rc):
258 print >> sys.stderr, '(exit %s)' % os.WEXITSTATUS(rc)
259 elif os.WIFSIGNALED(rc):
260 print >> sys.stderr, '(signal %s)' % os.WTERMSIG(rc)
261 else:
262 open(self.name + '.run', 'w')
263 return rc or err
264 finally:
265 shutil.rmtree(tmpdir)
267 def rename_output(self, base, ignore):
268 mangle_re = re.compile('(?:' + '|'.join(ignore) + ')')
269 def mangle(s):
270 return mangle_re.sub('', s)
271 def matchfp(fp1, fp2):
272 while True:
273 s1 = mangle(fp1.readline())
274 s2 = mangle(fp2.readline())
275 if cmp(s1, s2):
276 break
277 if not s1:
278 return True
279 return False
281 oldname = base + '.out'
282 tmpname = base + '.tmp'
283 errname = base + '.err'
284 errfp = open(errname, 'w+')
285 for line in open(tmpname):
286 errfp.write(mangle_re.sub('', line))
287 os.rename(tmpname, base + '.lxo')
288 errfp.seek(0)
289 try:
290 oldfp = open(oldname)
291 except IOError, err:
292 if err.errno != errno.ENOENT:
293 raise
294 os.rename(errname, oldname)
295 return
296 if matchfp(oldfp, errfp):
297 os.unlink(errname)
298 else:
299 print >> sys.stderr, '\nOutput of %s has changed!' % base
300 os.system('diff -u %s %s 1>&2' % (oldname, errname))
301 return True
303 def main(path='.'):
304 opts, args = getopt.getopt(sys.argv[1:], 'v', ['verbose'])
305 verbose = False
306 for o, a in opts:
307 if o in ('-v', '--verbose'):
308 verbose = True
309 errs = 0
310 if args:
311 for a in args:
312 try:
313 st = os.lstat(a)
314 except OSError, err:
315 print >> sys.stderr, '%s: %s' % (a, err.strerror)
316 errs += 1
317 continue
318 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
319 if example(a, verbose).run():
320 errs += 1
321 else:
322 print >> sys.stderr, '%s: not a file, or not executable' % a
323 errs += 1
324 return errs
325 for name in os.listdir(path):
326 if name == 'run-example' or name.startswith('.'): continue
327 if name.endswith('.out') or name.endswith('~'): continue
328 if name.endswith('.run'): continue
329 pathname = os.path.join(path, name)
330 try:
331 st = os.lstat(pathname)
332 except OSError, err:
333 # could be an output file that was removed while we ran
334 if err.errno != errno.ENOENT:
335 raise
336 continue
337 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
338 if example(pathname, verbose).run():
339 errs += 1
340 print >> open(os.path.join(path, '.run'), 'w'), time.asctime()
341 return errs
343 if __name__ == '__main__':
344 sys.exit(main())