hgbook

view en/examples/run-example @ 154:e7f48702d409

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