comparison ja/examples/run-example @ 290:b0db5adf11c1 ja_root

fork Japanese translation.
author Yoshiki Yazawa <yaz@cc.rim.or.jp>
date Wed, 06 Feb 2008 17:43:11 +0900
parents en/examples/run-example@1a55ba6ceca1
children 32d33b238b7e
comparison
equal deleted inserted replaced
289:7be02466421b 290:b0db5adf11c1
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 finally:
287 shutil.rmtree(tmpdir)
288
289 def rename_output(self, base, ignore):
290 mangle_re = re.compile('(?:' + '|'.join(ignore) + ')')
291 def mangle(s):
292 return mangle_re.sub('', s)
293 def matchfp(fp1, fp2):
294 while True:
295 s1 = mangle(fp1.readline())
296 s2 = mangle(fp2.readline())
297 if cmp(s1, s2):
298 break
299 if not s1:
300 return True
301 return False
302
303 oldname = base + '.out'
304 tmpname = base + '.tmp'
305 errname = base + '.err'
306 errfp = open(errname, 'w+')
307 for line in open(tmpname):
308 errfp.write(mangle_re.sub('', line))
309 os.rename(tmpname, base + '.lxo')
310 errfp.seek(0)
311 try:
312 oldfp = open(oldname)
313 except IOError, err:
314 if err.errno != errno.ENOENT:
315 raise
316 os.rename(errname, oldname)
317 return False
318 if matchfp(oldfp, errfp):
319 os.unlink(errname)
320 return False
321 else:
322 print >> sys.stderr, '\nOutput of %s has changed!' % base
323 os.system('diff -u %s %s 1>&2' % (oldname, errname))
324 return True
325
326 def print_help(exit, msg=None):
327 if msg:
328 print >> sys.stderr, 'Error:', msg
329 print >> sys.stderr, 'Usage: run-example [options] [test...]'
330 print >> sys.stderr, 'Options:'
331 print >> sys.stderr, ' -a --all run all tests in this directory'
332 print >> sys.stderr, ' -h --help print this help message'
333 print >> sys.stderr, ' -v --verbose display extra debug output'
334 sys.exit(exit)
335
336 def main(path='.'):
337 opts, args = getopt.getopt(sys.argv[1:], '?ahv',
338 ['all', 'help', 'verbose'])
339 verbose = False
340 run_all = False
341 for o, a in opts:
342 if o in ('-h', '-?', '--help'):
343 print_help(0)
344 if o in ('-a', '--all'):
345 run_all = True
346 if o in ('-v', '--verbose'):
347 verbose = True
348 errs = 0
349 if args:
350 for a in args:
351 try:
352 st = os.lstat(a)
353 except OSError, err:
354 print >> sys.stderr, '%s: %s' % (a, err.strerror)
355 errs += 1
356 continue
357 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
358 if example(a, verbose).run():
359 errs += 1
360 else:
361 print >> sys.stderr, '%s: not a file, or not executable' % a
362 errs += 1
363 elif run_all:
364 names = os.listdir(path)
365 names.sort()
366 for name in names:
367 if name == 'run-example' or name.startswith('.'): continue
368 if name.endswith('.out') or name.endswith('~'): continue
369 if name.endswith('.run'): continue
370 pathname = os.path.join(path, name)
371 try:
372 st = os.lstat(pathname)
373 except OSError, err:
374 # could be an output file that was removed while we ran
375 if err.errno != errno.ENOENT:
376 raise
377 continue
378 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
379 if example(pathname, verbose).run():
380 errs += 1
381 print >> open(os.path.join(path, '.run'), 'w'), time.asctime()
382 else:
383 print_help(1, msg='no test names given, and --all not provided')
384 return errs
385
386 if __name__ == '__main__':
387 try:
388 sys.exit(main())
389 except KeyboardInterrupt:
390 print >> sys.stderr, 'interrupted!'
391 sys.exit(1)