Mercurial > hgbook
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) |