hgbook

view en/examples/run-example @ 137:9d7dffe74b2c

Save "good" example output so we can see if something has broken.
author Bryan O'Sullivan <bos@serpentine.com>
date Mon Mar 05 23:56:30 2007 -0800 (2007-03-05)
parents 7b5894fffc37
children d374685eb7fa
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'^(?:---|\+\+\+) .*', # diff header with dates
183 r'^date:.*', # date
184 r'^diff -r.*', # "diff -r" is followed by hash
185 ]
187 try:
188 try:
189 # eat first prompt string from shell
190 self.read()
191 # setup env and prompt
192 ps, output = self.sendreceive('source %s\n' % rcfile)
193 for hunk in self.parse():
194 # is this line a processing instruction?
195 m = self.pi_re.match(hunk)
196 if m:
197 pi, rest = m.groups()
198 if pi == 'name':
199 self.status('.')
200 out = rest
201 assert os.sep not in out
202 if ofp is not None:
203 ofp.close()
204 self.rename_output(ofp_basename, ignore)
205 if out:
206 ofp_basename = '%s.%s' % (self.name, out)
207 ofp = open(ofp_basename + '.tmp', 'w')
208 else:
209 ofp = None
210 elif pi == 'ignore':
211 ignore.append(rest)
212 elif hunk.strip():
213 # it's something we should execute
214 newps, output = self.sendreceive(hunk)
215 if not ofp:
216 continue
217 # first, print the command we ran
218 if not hunk.startswith('#'):
219 nl = hunk.endswith('\n')
220 hunk = ('%s \\textbf{%s}' %
221 (prompts[ps],
222 tex_escape(hunk.rstrip('\n'))))
223 if nl: hunk += '\n'
224 ofp.write(hunk)
225 # then its output
226 ofp.write(tex_escape(output))
227 ps = newps
228 self.status('\n')
229 except:
230 print >> sys.stderr, '(killed)'
231 os.kill(self.pid, signal.SIGKILL)
232 pid, rc = os.wait()
233 raise
234 else:
235 try:
236 ps, output = self.sendreceive('exit\n')
237 if ofp:
238 ofp.write(output)
239 os.close(self.cfd)
240 except IOError:
241 pass
242 os.kill(self.pid, signal.SIGTERM)
243 pid, rc = os.wait()
244 if rc:
245 if os.WIFEXITED(rc):
246 print >> sys.stderr, '(exit %s)' % os.WEXITSTATUS(rc)
247 elif os.WIFSIGNALED(rc):
248 print >> sys.stderr, '(signal %s)' % os.WTERMSIG(rc)
249 else:
250 open(self.name + '.run', 'w')
251 return rc
252 finally:
253 shutil.rmtree(tmpdir)
255 def rename_output(self, base, ignore):
256 mangle_re = re.compile('(?:' + '|'.join(ignore) + ')')
257 def mangle(s):
258 return mangle_re.sub('', s)
259 def matchfp(fp1, fp2):
260 while True:
261 s1 = mangle(fp1.readline())
262 s2 = mangle(fp2.readline())
263 if cmp(s1, s2):
264 break
265 if not s1:
266 return True
267 return False
269 oldname = base + '.out'
270 tmpname = base + '.tmp'
271 errname = base + '.err'
272 errfp = open(errname, 'w+')
273 for line in open(tmpname):
274 errfp.write(mangle_re.sub('', line))
275 os.unlink(tmpname)
276 errfp.seek(0)
277 try:
278 oldfp = open(oldname)
279 except IOError, err:
280 if err.errno != errno.ENOENT:
281 raise
282 os.rename(errname, oldname)
283 return
284 if matchfp(oldfp, errfp):
285 os.unlink(errname)
286 else:
287 print >> sys.stderr, '\nOutput of %s has changed!' % base
288 os.system('diff -u %s %s 1>&2' % (oldname, errname))
290 def main(path='.'):
291 opts, args = getopt.getopt(sys.argv[1:], 'v', ['verbose'])
292 verbose = False
293 for o, a in opts:
294 if o in ('-v', '--verbose'):
295 verbose = True
296 errs = 0
297 if args:
298 for a in args:
299 try:
300 st = os.lstat(a)
301 except OSError, err:
302 print >> sys.stderr, '%s: %s' % (a, err.strerror)
303 errs += 1
304 continue
305 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
306 if example(a, verbose).run():
307 errs += 1
308 else:
309 print >> sys.stderr, '%s: not a file, or not executable' % a
310 errs += 1
311 return errs
312 for name in os.listdir(path):
313 if name == 'run-example' or name.startswith('.'): continue
314 if name.endswith('.out') or name.endswith('~'): continue
315 if name.endswith('.run'): continue
316 pathname = os.path.join(path, name)
317 st = os.lstat(pathname)
318 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
319 if example(pathname, verbose).run():
320 errs += 1
321 print >> open(os.path.join(path, '.run'), 'w'), time.asctime()
322 return errs
324 if __name__ == '__main__':
325 sys.exit(main())