comparison ja/examples/run-example-tex @ 789:4cf1bbfe4d2c

restore the script to generate examples for tex.
author Yoshiki Yazawa <yaz@honeyplanet.jp>
date Tue, 09 Jun 2009 15:09:39 +0900
parents
children
comparison
equal deleted inserted replaced
788:b3c384b30e11 789:4cf1bbfe4d2c
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.
6
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
20
21 tex_subs = {
22 '\\': '\\textbackslash{}',
23 '{': '\\{',
24 '}': '\\}',
25 }
26
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:]
36
37 def tex_escape(s):
38 return ''.join(gensubs(s))
39
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
48
49 def find_path_to(program):
50 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
51 name = os.path.join(p, program)
52 if os.access(name, os.X_OK):
53 return p
54 return None
55
56 class example:
57 shell = '/usr/bin/env bash'
58 ps1 = '__run_example_ps1__ '
59 ps2 = '__run_example_ps2__ '
60 pi_re = re.compile(r'#\$\s*(name|ignore):\s*(.*)$')
61
62 timeout = 10
63
64 def __init__(self, name, verbose):
65 self.name = name
66 self.verbose = verbose
67 self.poll = select.poll()
68
69 def parse(self):
70 '''yield each hunk of input from the file.'''
71 fp = open(self.name)
72 cfp = cStringIO.StringIO()
73 for line in fp:
74 cfp.write(line)
75 if not line.rstrip().endswith('\\'):
76 yield cfp.getvalue()
77 cfp.seek(0)
78 cfp.truncate()
79
80 def status(self, s):
81 sys.stdout.write(s)
82 if not s.endswith('\n'):
83 sys.stdout.flush()
84
85 def send(self, s):
86 if self.verbose:
87 print >> sys.stderr, '>', self.debugrepr(s)
88 while s:
89 count = os.write(self.cfd, s)
90 s = s[count:]
91
92 def debugrepr(self, s):
93 rs = repr(s)
94 limit = 60
95 if len(rs) > limit:
96 return ('%s%s ... [%d bytes]' % (rs[:limit], rs[0], len(s)))
97 else:
98 return rs
99
100 timeout = 5
101
102 def read(self, hint):
103 events = self.poll.poll(self.timeout * 1000)
104 if not events:
105 print >> sys.stderr, ('[%stimed out after %d seconds]' %
106 (hint, self.timeout))
107 os.kill(self.pid, signal.SIGHUP)
108 return ''
109 return os.read(self.cfd, 1024)
110
111 def receive(self, hint):
112 out = cStringIO.StringIO()
113 while True:
114 try:
115 if self.verbose:
116 sys.stderr.write('< ')
117 s = self.read(hint)
118 except OSError, err:
119 if err.errno == errno.EIO:
120 return '', ''
121 raise
122 if self.verbose:
123 print >> sys.stderr, self.debugrepr(s)
124 out.write(s)
125 s = out.getvalue()
126 if s.endswith(self.ps1):
127 return self.ps1, s.replace('\r\n', '\n')[:-len(self.ps1)]
128 if s.endswith(self.ps2):
129 return self.ps2, s.replace('\r\n', '\n')[:-len(self.ps2)]
130
131 def sendreceive(self, s, hint):
132 self.send(s)
133 ps, r = self.receive(hint)
134 if r.startswith(s):
135 r = r[len(s):]
136 return ps, r
137
138 def run(self):
139 ofp = None
140 basename = os.path.basename(self.name)
141 self.status('running %s ' % basename)
142 tmpdir = tempfile.mkdtemp(prefix=basename)
143
144 # remove the marker file that we tell make to use to see if
145 # this run succeeded
146 maybe_unlink(self.name + '.run')
147
148 rcfile = os.path.join(tmpdir, '.hgrc')
149 rcfp = open(rcfile, 'w')
150 print >> rcfp, '[ui]'
151 print >> rcfp, "username = Bryan O'Sullivan <bos@serpentine.com>"
152
153 rcfile = os.path.join(tmpdir, '.bashrc')
154 rcfp = open(rcfile, 'w')
155 print >> rcfp, 'PS1="%s"' % self.ps1
156 print >> rcfp, 'PS2="%s"' % self.ps2
157 print >> rcfp, 'unset HISTFILE'
158 path = ['/usr/bin', '/bin']
159 hg = find_path_to('hg')
160 if hg and hg not in path:
161 path.append(hg)
162 def re_export(envar):
163 v = os.getenv(envar)
164 if v is not None:
165 print >> rcfp, 'export ' + envar + '=' + v
166 print >> rcfp, 'export PATH=' + ':'.join(path)
167 re_export('PYTHONPATH')
168 print >> rcfp, 'export EXAMPLE_DIR="%s"' % os.getcwd()
169 print >> rcfp, 'export HGMERGE=merge'
170 print >> rcfp, 'export LANG=C'
171 print >> rcfp, 'export LC_ALL=C'
172 print >> rcfp, 'export TZ=GMT'
173 print >> rcfp, 'export HGRC="%s/.hgrc"' % tmpdir
174 print >> rcfp, 'export HGRCPATH=$HGRC'
175 print >> rcfp, 'cd %s' % tmpdir
176 rcfp.close()
177 sys.stdout.flush()
178 sys.stderr.flush()
179 self.pid, self.cfd = pty.fork()
180 if self.pid == 0:
181 cmdline = ['/usr/bin/env', '-i', 'bash', '--noediting',
182 '--noprofile', '--norc']
183 try:
184 os.execv(cmdline[0], cmdline)
185 except OSError, err:
186 print >> sys.stderr, '%s: %s' % (cmdline[0], err.strerror)
187 sys.stderr.flush()
188 os._exit(0)
189 self.poll.register(self.cfd, select.POLLIN | select.POLLERR |
190 select.POLLHUP)
191
192 prompts = {
193 '': '',
194 self.ps1: '$',
195 self.ps2: '>',
196 }
197
198 ignore = [
199 r'\d+:[0-9a-f]{12}', # changeset number:hash
200 r'[0-9a-f]{40}', # long changeset hash
201 r'[0-9a-f]{12}', # short changeset hash
202 r'^(?:---|\+\+\+) .*', # diff header with dates
203 r'^date:.*', # date
204 #r'^diff -r.*', # "diff -r" is followed by hash
205 r'^# Date \d+ \d+', # hg patch header
206 ]
207
208 err = False
209 read_hint = ''
210
211 try:
212 try:
213 # eat first prompt string from shell
214 self.read(read_hint)
215 # setup env and prompt
216 ps, output = self.sendreceive('source %s\n' % rcfile,
217 read_hint)
218 for hunk in self.parse():
219 # is this line a processing instruction?
220 m = self.pi_re.match(hunk)
221 if m:
222 pi, rest = m.groups()
223 if pi == 'name':
224 self.status('.')
225 out = rest
226 if out in ('err', 'lxo', 'out', 'run', 'tmp'):
227 print >> sys.stderr, ('%s: illegal section '
228 'name %r' %
229 (self.name, out))
230 return 1
231 assert os.sep not in out
232 if ofp is not None:
233 ofp.close()
234 err |= self.rename_output(ofp_basename, ignore)
235 if out:
236 ofp_basename = '%s.%s' % (self.name, out)
237 read_hint = ofp_basename + ' '
238 ofp = open(ofp_basename + '.tmp', 'w')
239 else:
240 ofp = None
241 elif pi == 'ignore':
242 ignore.append(rest)
243 elif hunk.strip():
244 # it's something we should execute
245 newps, output = self.sendreceive(hunk, read_hint)
246 if not ofp:
247 continue
248 # first, print the command we ran
249 if not hunk.startswith('#'):
250 nl = hunk.endswith('\n')
251 hunk = ('%s \\textbf{%s}' %
252 (prompts[ps],
253 tex_escape(hunk.rstrip('\n'))))
254 if nl: hunk += '\n'
255 ofp.write(hunk)
256 # then its output
257 ofp.write(tex_escape(output))
258 ps = newps
259 self.status('\n')
260 except:
261 print >> sys.stderr, '(killed)'
262 os.kill(self.pid, signal.SIGKILL)
263 pid, rc = os.wait()
264 raise
265 else:
266 try:
267 ps, output = self.sendreceive('exit\n', read_hint)
268 if ofp is not None:
269 ofp.write(output)
270 ofp.close()
271 err |= self.rename_output(ofp_basename, ignore)
272 os.close(self.cfd)
273 except IOError:
274 pass
275 os.kill(self.pid, signal.SIGTERM)
276 pid, rc = os.wait()
277 err = err or rc
278 if err:
279 if os.WIFEXITED(rc):
280 print >> sys.stderr, '(exit %s)' % os.WEXITSTATUS(rc)
281 elif os.WIFSIGNALED(rc):
282 print >> sys.stderr, '(signal %s)' % os.WTERMSIG(rc)
283 else:
284 open(self.name + '.run', 'w')
285 # return err
286 return 0
287 finally:
288 shutil.rmtree(tmpdir)
289
290 def rename_output(self, base, ignore):
291 mangle_re = re.compile('(?:' + '|'.join(ignore) + ')')
292 def mangle(s):
293 return mangle_re.sub('', s)
294 def matchfp(fp1, fp2):
295 while True:
296 s1 = mangle(fp1.readline())
297 s2 = mangle(fp2.readline())
298 if cmp(s1, s2):
299 break
300 if not s1:
301 return True
302 return False
303
304 oldname = base + '.out'
305 tmpname = base + '.tmp'
306 errname = base + '.err'
307 errfp = open(errname, 'w+')
308 for line in open(tmpname):
309 errfp.write(mangle_re.sub('', line))
310 os.rename(tmpname, base + '.lxo')
311 errfp.seek(0)
312 try:
313 oldfp = open(oldname)
314 except IOError, err:
315 if err.errno != errno.ENOENT:
316 raise
317 os.rename(errname, oldname)
318 return False
319 if matchfp(oldfp, errfp):
320 os.unlink(errname)
321 return False
322 else:
323 print >> sys.stderr, '\nOutput of %s has changed!' % base
324 os.system('diff -u %s %s 1>&2' % (oldname, errname))
325 return True
326
327 def print_help(exit, msg=None):
328 if msg:
329 print >> sys.stderr, 'Error:', msg
330 print >> sys.stderr, 'Usage: run-example [options] [test...]'
331 print >> sys.stderr, 'Options:'
332 print >> sys.stderr, ' -a --all run all tests in this directory'
333 print >> sys.stderr, ' -h --help print this help message'
334 print >> sys.stderr, ' -v --verbose display extra debug output'
335 sys.exit(exit)
336
337 def main(path='.'):
338 opts, args = getopt.getopt(sys.argv[1:], '?ahv',
339 ['all', 'help', 'verbose'])
340 verbose = False
341 run_all = False
342 for o, a in opts:
343 if o in ('-h', '-?', '--help'):
344 print_help(0)
345 if o in ('-a', '--all'):
346 run_all = True
347 if o in ('-v', '--verbose'):
348 verbose = True
349 errs = 0
350 if args:
351 for a in args:
352 try:
353 st = os.lstat(a)
354 except OSError, err:
355 print >> sys.stderr, '%s: %s' % (a, err.strerror)
356 errs += 1
357 continue
358 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
359 if example(a, verbose).run():
360 errs += 1
361 else:
362 print >> sys.stderr, '%s: not a file, or not executable' % a
363 errs += 1
364 elif run_all:
365 names = os.listdir(path)
366 names.sort()
367 for name in names:
368 if name == 'run-example' or name.startswith('.'): continue
369 if name.endswith('.out') or name.endswith('~'): continue
370 if name.endswith('.run'): continue
371 pathname = os.path.join(path, name)
372 try:
373 st = os.lstat(pathname)
374 except OSError, err:
375 # could be an output file that was removed while we ran
376 if err.errno != errno.ENOENT:
377 raise
378 continue
379 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
380 if example(pathname, verbose).run():
381 errs += 1
382 print >> open(os.path.join(path, '.run'), 'w'), time.asctime()
383 else:
384 print_help(1, msg='no test names given, and --all not provided')
385 return errs
386
387 if __name__ == '__main__':
388 try:
389 sys.exit(main())
390 except KeyboardInterrupt:
391 print >> sys.stderr, 'interrupted!'
392 sys.exit(1)