hgbook

view en/examples/run-example @ 167:e67251ac336f

Small portability change. "grep -P" doesn't work on Debian.
author Bryan O'Sullivan <bos@serpentine.com>
date Tue Mar 27 01:15:34 2007 -0500 (2007-03-27)
parents 8f4c9ae918af
children f8b5b782e150
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*(drop_output|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 drop_output = False
194 try:
195 try:
196 # eat first prompt string from shell
197 self.read()
198 # setup env and prompt
199 ps, output = self.sendreceive('source %s\n' % rcfile)
200 for hunk in self.parse():
201 # is this line a processing instruction?
202 m = self.pi_re.match(hunk)
203 if m:
204 pi, rest = m.groups()
205 if pi == 'name':
206 self.status('.')
207 out = rest
208 if out in ('err', 'lxo', 'out', 'run', 'tmp'):
209 print >> sys.stderr, ('%s: illegal section '
210 'name %r' %
211 (self.name, out))
212 return 1
213 assert os.sep not in out
214 if ofp is not None:
215 ofp.close()
216 err |= self.rename_output(ofp_basename, ignore)
217 if out:
218 ofp_basename = '%s.%s' % (self.name, out)
219 ofp = open(ofp_basename + '.tmp', 'w')
220 else:
221 ofp = None
222 elif pi == 'ignore':
223 ignore.append(rest)
224 elif pi == 'drop_output':
225 drop_output = dict(yes=1,no=0)[rest.lower()]
226 elif hunk.strip():
227 # it's something we should execute
228 newps, output = self.sendreceive(hunk)
229 if not ofp or drop_output:
230 continue
231 # first, print the command we ran
232 if not hunk.startswith('#'):
233 nl = hunk.endswith('\n')
234 hunk = ('%s \\textbf{%s}' %
235 (prompts[ps],
236 tex_escape(hunk.rstrip('\n'))))
237 if nl: hunk += '\n'
238 ofp.write(hunk)
239 # then its output
240 ofp.write(tex_escape(output))
241 ps = newps
242 self.status('\n')
243 except:
244 print >> sys.stderr, '(killed)'
245 os.kill(self.pid, signal.SIGKILL)
246 pid, rc = os.wait()
247 raise
248 else:
249 try:
250 ps, output = self.sendreceive('exit\n')
251 if ofp is not None:
252 ofp.write(output)
253 ofp.close()
254 err |= self.rename_output(ofp_basename, ignore)
255 os.close(self.cfd)
256 except IOError:
257 pass
258 os.kill(self.pid, signal.SIGTERM)
259 pid, rc = os.wait()
260 err = err or rc
261 if err:
262 if os.WIFEXITED(rc):
263 print >> sys.stderr, '(exit %s)' % os.WEXITSTATUS(rc)
264 elif os.WIFSIGNALED(rc):
265 print >> sys.stderr, '(signal %s)' % os.WTERMSIG(rc)
266 else:
267 open(self.name + '.run', 'w')
268 return err
269 finally:
270 shutil.rmtree(tmpdir)
272 def rename_output(self, base, ignore):
273 mangle_re = re.compile('(?:' + '|'.join(ignore) + ')')
274 def mangle(s):
275 return mangle_re.sub('', s)
276 def matchfp(fp1, fp2):
277 while True:
278 s1 = mangle(fp1.readline())
279 s2 = mangle(fp2.readline())
280 if cmp(s1, s2):
281 break
282 if not s1:
283 return True
284 return False
286 oldname = base + '.out'
287 tmpname = base + '.tmp'
288 errname = base + '.err'
289 errfp = open(errname, 'w+')
290 for line in open(tmpname):
291 errfp.write(mangle_re.sub('', line))
292 os.rename(tmpname, base + '.lxo')
293 errfp.seek(0)
294 try:
295 oldfp = open(oldname)
296 except IOError, err:
297 if err.errno != errno.ENOENT:
298 raise
299 os.rename(errname, oldname)
300 return False
301 if matchfp(oldfp, errfp):
302 os.unlink(errname)
303 return False
304 else:
305 print >> sys.stderr, '\nOutput of %s has changed!' % base
306 os.system('diff -u %s %s 1>&2' % (oldname, errname))
307 return True
309 def main(path='.'):
310 opts, args = getopt.getopt(sys.argv[1:], 'v', ['verbose'])
311 verbose = False
312 for o, a in opts:
313 if o in ('-v', '--verbose'):
314 verbose = True
315 errs = 0
316 if args:
317 for a in args:
318 try:
319 st = os.lstat(a)
320 except OSError, err:
321 print >> sys.stderr, '%s: %s' % (a, err.strerror)
322 errs += 1
323 continue
324 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
325 if example(a, verbose).run():
326 errs += 1
327 else:
328 print >> sys.stderr, '%s: not a file, or not executable' % a
329 errs += 1
330 return errs
331 names = os.listdir(path)
332 names.sort()
333 for name in names:
334 if name == 'run-example' or name.startswith('.'): continue
335 if name.endswith('.out') or name.endswith('~'): continue
336 if name.endswith('.run'): continue
337 pathname = os.path.join(path, name)
338 try:
339 st = os.lstat(pathname)
340 except OSError, err:
341 # could be an output file that was removed while we ran
342 if err.errno != errno.ENOENT:
343 raise
344 continue
345 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
346 if example(pathname, verbose).run():
347 errs += 1
348 print >> open(os.path.join(path, '.run'), 'w'), time.asctime()
349 return errs
351 if __name__ == '__main__':
352 sys.exit(main())