hgbook

view en/examples/run-example @ 1114:527b86d55d4a

inotify: update installation information

inotify is shipped in Mercurial since 1.0, which greatly simplifies the installation process
author Nicolas Dumazet <nicdumz.commits@gmail.com>
date Sun Dec 13 16:35:56 2009 +0900 (2009-12-13)
parents c82ff69f0935
children
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 glob
11 import os
12 import pty
13 import re
14 import select
15 import shutil
16 import signal
17 import stat
18 import sys
19 import tempfile
20 import time
22 xml_subs = {
23 '<': '&lt;',
24 '>': '&gt;',
25 '&': '&amp;',
26 }
28 def gensubs(s):
29 start = 0
30 for i, c in enumerate(s):
31 sub = xml_subs.get(c)
32 if sub:
33 yield s[start:i]
34 start = i + 1
35 yield sub
36 yield s[start:]
38 def xml_escape(s):
39 return ''.join(gensubs(s))
41 def maybe_unlink(name):
42 try:
43 os.unlink(name)
44 return True
45 except OSError, err:
46 if err.errno != errno.ENOENT:
47 raise
48 return False
50 def find_path_to(program):
51 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
52 name = os.path.join(p, program)
53 if os.access(name, os.X_OK):
54 return p
55 return None
57 def result_name(name):
58 return os.path.normpath(os.path.join('results', name.replace(os.sep, '-')))
60 def wopen(name):
61 path = os.path.dirname(name)
62 if path:
63 try:
64 os.makedirs(path)
65 except OSError, err:
66 if err.errno != errno.EEXIST:
67 raise
68 return open(name, 'w')
70 class example:
71 entities = dict.fromkeys(l.rstrip() for l in open('auto-snippets.xml'))
73 def __init__(self, name, verbose, keep_change):
74 self.name = os.path.normpath(name)
75 self.verbose = verbose
76 self.keep_change = keep_change
78 def status(self, s):
79 sys.stdout.write(s)
80 if not s.endswith('\n'):
81 sys.stdout.flush()
83 def rename_output(self, base, ignore=[]):
84 mangle_re = re.compile('(?:' + '|'.join(ignore) + ')')
85 def mangle(s):
86 return mangle_re.sub('', s)
87 def matchfp(fp1, fp2):
88 while True:
89 s1 = mangle(fp1.readline())
90 s2 = mangle(fp2.readline())
91 if cmp(s1, s2):
92 break
93 if not s1:
94 return True
95 return False
97 oldname = result_name(base + '.out')
98 tmpname = result_name(base + '.tmp')
99 errname = result_name(base + '.err')
100 errfp = open(errname, 'w+')
101 for line in open(tmpname):
102 errfp.write(mangle_re.sub('', line))
103 os.rename(tmpname, result_name(base + '.lxo'))
104 errfp.seek(0)
105 try:
106 oldfp = open(oldname)
107 except IOError, err:
108 if err.errno != errno.ENOENT:
109 raise
110 os.rename(errname, oldname)
111 return False
112 if matchfp(oldfp, errfp):
113 os.unlink(errname)
114 return False
115 else:
116 print >> sys.stderr, '\nOutput of %s has changed!' % base
117 if self.keep_change:
118 os.rename(errname, oldname)
119 return False
120 else:
121 os.system('diff -u %s %s 1>&2' % (oldname, errname))
122 return True
124 class static_example(example):
125 def run(self):
126 self.status('running %s\n' % self.name)
127 s = open(self.name).read().rstrip()
128 s = s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
129 ofp = wopen(result_name(self.name + '.tmp'))
130 ofp.write('<!-- BEGIN %s -->\n' % self.name)
131 ofp.write('<programlisting>')
132 ofp.write(s)
133 ofp.write('</programlisting>\n')
134 ofp.write('<!-- END %s -->\n' % self.name)
135 ofp.close()
136 self.rename_output(self.name)
137 norm = self.name.replace(os.sep, '-')
138 example.entities[
139 '<!ENTITY %s SYSTEM "results/%s.lxo">' % (norm, norm)] = 1
142 class shell_example(example):
143 shell = '/usr/bin/env bash'
144 ps1 = '__run_example_ps1__ '
145 ps2 = '__run_example_ps2__ '
146 pi_re = re.compile(r'#\$\s*(name|ignore):\s*(.*)$')
148 timeout = 10
150 def __init__(self, name, verbose, keep_change):
151 example.__init__(self, name, verbose, keep_change)
152 self.poll = select.poll()
154 def parse(self):
155 '''yield each hunk of input from the file.'''
156 fp = open(self.name)
157 cfp = cStringIO.StringIO()
158 for line in fp:
159 cfp.write(line)
160 if not line.rstrip().endswith('\\'):
161 yield cfp.getvalue()
162 cfp.seek(0)
163 cfp.truncate()
165 def send(self, s):
166 if self.verbose:
167 print >> sys.stderr, '>', self.debugrepr(s)
168 while s:
169 count = os.write(self.cfd, s)
170 s = s[count:]
172 def debugrepr(self, s):
173 rs = repr(s)
174 limit = 60
175 if len(rs) > limit:
176 return ('%s%s ... [%d bytes]' % (rs[:limit], rs[0], len(s)))
177 else:
178 return rs
180 timeout = 5
182 def read(self, hint):
183 events = self.poll.poll(self.timeout * 1000)
184 if not events:
185 print >> sys.stderr, ('[%stimed out after %d seconds]' %
186 (hint, self.timeout))
187 os.kill(self.pid, signal.SIGHUP)
188 return ''
189 return os.read(self.cfd, 1024)
191 def receive(self, hint):
192 out = cStringIO.StringIO()
193 while True:
194 try:
195 if self.verbose:
196 sys.stderr.write('< ')
197 s = self.read(hint)
198 except OSError, err:
199 if err.errno == errno.EIO:
200 return '', ''
201 raise
202 if self.verbose:
203 print >> sys.stderr, self.debugrepr(s)
204 out.write(s)
205 s = out.getvalue()
206 if s.endswith(self.ps1):
207 return self.ps1, s.replace('\r\n', '\n')[:-len(self.ps1)]
208 if s.endswith(self.ps2):
209 return self.ps2, s.replace('\r\n', '\n')[:-len(self.ps2)]
211 def sendreceive(self, s, hint):
212 self.send(s)
213 ps, r = self.receive(hint)
214 if r.startswith(s):
215 r = r[len(s):]
216 return ps, r
218 def run(self):
219 ofp = None
220 basename = os.path.basename(self.name)
221 self.status('running %s ' % basename)
222 tmpdir = tempfile.mkdtemp(prefix=basename)
224 # remove the marker file that we tell make to use to see if
225 # this run succeeded
226 maybe_unlink(self.name + '.run')
228 rcfile = os.path.join(tmpdir, '.hgrc')
229 rcfp = wopen(rcfile)
230 print >> rcfp, '[ui]'
231 print >> rcfp, "username = Bryan O'Sullivan <bos@serpentine.com>"
233 rcfile = os.path.join(tmpdir, '.bashrc')
234 rcfp = wopen(rcfile)
235 print >> rcfp, 'PS1="%s"' % self.ps1
236 print >> rcfp, 'PS2="%s"' % self.ps2
237 print >> rcfp, 'unset HISTFILE'
238 path = ['/usr/bin', '/bin']
239 hg = find_path_to('hg')
240 if hg and hg not in path:
241 path.append(hg)
242 def re_export(envar):
243 v = os.getenv(envar)
244 if v is not None:
245 print >> rcfp, 'export ' + envar + '=' + v
246 print >> rcfp, 'export PATH=' + ':'.join(path)
247 re_export('PYTHONPATH')
248 print >> rcfp, 'export EXAMPLE_DIR="%s"' % os.getcwd()
249 print >> rcfp, 'export HGMERGE=merge'
250 print >> rcfp, 'export LANG=C'
251 print >> rcfp, 'export LC_ALL=C'
252 print >> rcfp, 'export TZ=GMT'
253 print >> rcfp, 'export HGRC="%s/.hgrc"' % tmpdir
254 print >> rcfp, 'export HGRCPATH=$HGRC'
255 print >> rcfp, 'cd %s' % tmpdir
256 rcfp.close()
257 sys.stdout.flush()
258 sys.stderr.flush()
259 self.pid, self.cfd = pty.fork()
260 if self.pid == 0:
261 cmdline = ['/usr/bin/env', '-i', 'bash', '--noediting',
262 '--noprofile', '--norc']
263 try:
264 os.execv(cmdline[0], cmdline)
265 except OSError, err:
266 print >> sys.stderr, '%s: %s' % (cmdline[0], err.strerror)
267 sys.stderr.flush()
268 os._exit(0)
269 self.poll.register(self.cfd, select.POLLIN | select.POLLERR |
270 select.POLLHUP)
272 prompts = {
273 '': '',
274 self.ps1: '$',
275 self.ps2: '>',
276 }
278 ignore = [
279 r'\d+:[0-9a-f]{12}', # changeset number:hash
280 r'[0-9a-f]{40}', # long changeset hash
281 r'[0-9a-f]{12}', # short changeset hash
282 r'^(?:---|\+\+\+) .*', # diff header with dates
283 r'^date:.*', # date
284 #r'^diff -r.*', # "diff -r" is followed by hash
285 r'^# Date \d+ \d+', # hg patch header
286 ]
288 err = False
289 read_hint = ''
291 try:
292 try:
293 # eat first prompt string from shell
294 self.read(read_hint)
295 # setup env and prompt
296 ps, output = self.sendreceive('source %s\n' % rcfile,
297 read_hint)
298 for hunk in self.parse():
299 # is this line a processing instruction?
300 m = self.pi_re.match(hunk)
301 if m:
302 pi, rest = m.groups()
303 if pi == 'name':
304 self.status('.')
305 out = rest
306 if out in ('err', 'lxo', 'out', 'run', 'tmp'):
307 print >> sys.stderr, ('%s: illegal section '
308 'name %r' %
309 (self.name, out))
310 return 1
311 assert os.sep not in out
312 if ofp is not None:
313 ofp.write('</screen>\n')
314 ofp.write('<!-- END %s -->\n' % ofp_basename)
315 ofp.close()
316 err |= self.rename_output(ofp_basename, ignore)
317 if out:
318 ofp_basename = '%s.%s' % (self.name, out)
319 norm = os.path.normpath(ofp_basename)
320 norm = norm.replace(os.sep, '-')
321 example.entities[
322 '<!ENTITY interaction.%s '
323 'SYSTEM "results/%s.lxo">'
324 % (norm, norm)] = 1
325 read_hint = ofp_basename + ' '
326 ofp = wopen(result_name(ofp_basename + '.tmp'))
327 ofp.write('<!-- BEGIN %s -->\n' % ofp_basename)
328 ofp.write('<screen>')
329 else:
330 ofp = None
331 elif pi == 'ignore':
332 ignore.append(rest)
333 elif hunk.strip():
334 # it's something we should execute
335 newps, output = self.sendreceive(hunk, read_hint)
336 if not ofp:
337 continue
338 # first, print the command we ran
339 if not hunk.startswith('#'):
340 nl = hunk.endswith('\n')
341 hunk = ('<prompt>%s</prompt> '
342 '<userinput>%s</userinput>' %
343 (prompts[ps],
344 xml_escape(hunk.rstrip('\n'))))
345 if nl: hunk += '\n'
346 ofp.write(hunk)
347 # then its output
348 ofp.write(xml_escape(output))
349 ps = newps
350 self.status('\n')
351 except:
352 print >> sys.stderr, '(killed)'
353 os.kill(self.pid, signal.SIGKILL)
354 pid, rc = os.wait()
355 raise
356 else:
357 try:
358 ps, output = self.sendreceive('exit\n', read_hint)
359 if ofp is not None:
360 ofp.write(output)
361 ofp.write('</screen>\n')
362 ofp.write('<!-- END %s -->\n' % ofp_basename)
363 ofp.close()
364 err |= self.rename_output(ofp_basename, ignore)
365 os.close(self.cfd)
366 except IOError:
367 pass
368 os.kill(self.pid, signal.SIGTERM)
369 pid, rc = os.wait()
370 err = err or rc
371 if err:
372 if os.WIFEXITED(rc):
373 print >> sys.stderr, '(exit %s)' % os.WEXITSTATUS(rc)
374 elif os.WIFSIGNALED(rc):
375 print >> sys.stderr, '(signal %s)' % os.WTERMSIG(rc)
376 else:
377 wopen(result_name(self.name + '.run'))
378 return err
379 finally:
380 shutil.rmtree(tmpdir)
382 def print_help(exit, msg=None):
383 if msg:
384 print >> sys.stderr, 'Error:', msg
385 print >> sys.stderr, 'Usage: run-example [options] [test...]'
386 print >> sys.stderr, 'Options:'
387 print >> sys.stderr, ' -a --all run all examples in this directory'
388 print >> sys.stderr, ' -h --help print this help message'
389 print >> sys.stderr, ' --keep keep new output as desired output'
390 print >> sys.stderr, ' -v --verbose display extra debug output'
391 sys.exit(exit)
393 def main(path='.'):
394 if os.path.realpath(path).split(os.sep)[-1] != 'examples':
395 print >> sys.stderr, 'Not being run from the examples directory!'
396 sys.exit(1)
398 opts, args = getopt.getopt(sys.argv[1:], '?ahv',
399 ['all', 'help', 'keep', 'verbose'])
400 verbose = False
401 run_all = False
402 keep_change = False
404 for o, a in opts:
405 if o in ('-h', '-?', '--help'):
406 print_help(0)
407 if o in ('-a', '--all'):
408 run_all = True
409 if o in ('--keep',):
410 keep_change = True
411 if o in ('-v', '--verbose'):
412 verbose = True
413 errs = 0
414 if args:
415 for a in args:
416 try:
417 st = os.lstat(a)
418 except OSError, err:
419 print >> sys.stderr, '%s: %s' % (a, err.strerror)
420 errs += 1
421 continue
422 if stat.S_ISREG(st.st_mode):
423 if st.st_mode & 0111:
424 if shell_example(a, verbose, keep_change).run():
425 errs += 1
426 elif a.endswith('.lst'):
427 static_example(a, verbose, keep_change).run()
428 else:
429 print >> sys.stderr, '%s: not a file, or not executable' % a
430 errs += 1
431 elif run_all:
432 names = glob.glob("*") + glob.glob("app*/*") + glob.glob("ch*/*")
433 names.sort()
434 for name in names:
435 if name == 'run-example' or name.endswith('~'): continue
436 pathname = os.path.join(path, name)
437 try:
438 st = os.lstat(pathname)
439 except OSError, err:
440 # could be an output file that was removed while we ran
441 if err.errno != errno.ENOENT:
442 raise
443 continue
444 if stat.S_ISREG(st.st_mode):
445 if st.st_mode & 0111:
446 if shell_example(pathname, verbose, keep_change).run():
447 errs += 1
448 elif pathname.endswith('.lst'):
449 static_example(pathname, verbose, keep_change).run()
450 print >> wopen(os.path.join(path, '.run')), time.asctime()
451 else:
452 print_help(1, msg='no test names given, and --all not provided')
454 fp = wopen('auto-snippets.xml')
455 for key in sorted(example.entities.iterkeys()):
456 print >> fp, key
457 fp.close()
458 return errs
460 if __name__ == '__main__':
461 try:
462 sys.exit(main())
463 except KeyboardInterrupt:
464 print >> sys.stderr, 'interrupted!'
465 sys.exit(1)