view en/examples/run-example @ 71:ddf533d41c09

Propagate errors correctly.
author Bryan O'Sullivan <bos@serpentine.com>
date Tue, 29 Aug 2006 22:25:18 -0700
parents 365b41b6a15e
children 12df31afb4e1
line wrap: on
line source

#!/usr/bin/env python
#
# This program takes something that resembles a shell script and runs
# it, spitting input (commands from the script) and output into text
# files, for use in examples.

import cStringIO
import os
import pty
import re
import shutil
import signal
import stat
import sys
import tempfile
import time

def tex_escape(s):
    if '\\' in s:
        s = s.replace('\\', '\\\\')
    if '{' in s:
        s = s.replace('{', '\\{')
    if '}' in s:
        s = s.replace('}', '\\}')
    return s
        
class example:
    shell = '/usr/bin/env bash'
    prompt = '__run_example_prompt__\n'
    pi_re = re.compile(r'#\$\s*(name):\s*(.*)$')
    
    def __init__(self, name):
        self.name = name

    def parse(self):
        '''yield each hunk of input from the file.'''
        fp = open(self.name)
        cfp = cStringIO.StringIO()
        for line in fp:
            cfp.write(line)
            if not line.rstrip().endswith('\\'):
                yield cfp.getvalue()
                cfp.seek(0)
                cfp.truncate()
        
    def status(self, s):
        sys.stdout.write(s)
        if not s.endswith('\n'):
            sys.stdout.flush()

    def send(self, s):
        self.cfp.write(s)
        self.cfp.flush()

    def receive(self):
        out = cStringIO.StringIO()
        while True:
            s = self.cfp.readline().replace('\r\n', '\n')
            if not s or s == self.prompt:
                break
            out.write(s)
        return out.getvalue()
        
    def sendreceive(self, s):
        self.send(s)
        r = self.receive()
        if r.startswith(s):
            r = r[len(s):]
        return r
    
    def run(self):
        ofp = None
        basename = os.path.basename(self.name)
        self.status('running %s ' % basename)
        tmpdir = tempfile.mkdtemp(prefix=basename)
        rcfile = os.path.join(tmpdir, '.bashrc')
        rcfp = open(rcfile, 'w')
        print >> rcfp, 'PS1="%s"' % self.prompt
        print >> rcfp, 'unset HISTFILE'
        print >> rcfp, 'export EXAMPLE_DIR="%s"' % os.getcwd()
        print >> rcfp, 'export LANG=C'
        print >> rcfp, 'export LC_ALL=C'
        print >> rcfp, 'export TZ=GMT'
        print >> rcfp, 'export HGRC="%s/.hgrc"' % tmpdir
        print >> rcfp, 'export HGRCPATH=$HGRC'
        print >> rcfp, 'cd %s' % tmpdir
        rcfp.close()
        sys.stdout.flush()
        sys.stderr.flush()
        pid, fd = pty.fork()
        if pid == 0:
            cmdline = ['/usr/bin/env', 'bash', '--noediting', '--noprofile',
                       '--norc']
            try:
                os.execv(cmdline[0], cmdline)
            except OSError, err:
                print >> sys.stderr, '%s: %s' % (cmdline[0], err.strerror)
                sys.stderr.flush()
                os._exit(0)
        self.cfp = os.fdopen(fd, 'w+')
        try:
            clean_exit = True
            try:
                # setup env and prompt
                self.sendreceive('source %s\n\n' % rcfile)
                for hunk in self.parse():
                    # is this line a processing instruction?
                    m = self.pi_re.match(hunk)
                    if m:
                        pi, rest = m.groups()
                        if pi == 'name':
                            self.status('.')
                            out = rest
                            assert os.sep not in out
                            if out:
                                ofp = open('%s.%s.out' % (self.name, out), 'w')
                            else:
                                ofp = None
                    elif hunk.strip():
                        # it's something we should execute
                        output = self.sendreceive(hunk)
                        if not ofp:
                            continue
                        # first, print the command we ran
                        if not hunk.startswith('#'):
                            nl = hunk.endswith('\n')
                            hunk = ('$ \\textbf{%s}' %
                                    tex_escape(hunk.rstrip('\n')))
                            if nl: hunk += '\n'
                        ofp.write(hunk)
                        # then its output
                        ofp.write(tex_escape(output))
                self.status('\n')
                open(self.name + '.run', 'w')
            except:
                clean_exit = False
                raise
        finally:
            if clean_exit:
                try:
                    output = self.sendreceive('exit\n')
                    if ofp:
                        ofp.write(output)
                    self.cfp.close()
                except IOError:
                    pass
            os.kill(pid, signal.SIGTERM)
            time.sleep(0.1)
            os.kill(pid, signal.SIGKILL)
            pid, rc = os.wait()
            if rc:
                if os.WIFEXITED(rc):
                    print >> sys.stderr, '(exit %s)' % os.WEXITSTATUS(rc)
                elif os.WIFSIGNALED(rc):
                    print >> sys.stderr, '(signal %s)' % os.WTERMSIG(rc)
            shutil.rmtree(tmpdir)
            return rc

def main(path='.'):
    args = sys.argv[1:]
    errs = 0
    if args:
        for a in args:
            if example(a).run():
                errs += 1
        return errs
    for name in os.listdir(path):
        if name == 'run-example' or name.startswith('.'): continue
        if name.endswith('.out') or name.endswith('~'): continue
        if name.endswith('.run'): continue
        pathname = os.path.join(path, name)
        st = os.lstat(pathname)
        if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
            if example(pathname).run():
                errs += 1
    print >> open(os.path.join(path, '.run'), 'w'), time.asctime()
    return errs

if __name__ == '__main__':
    sys.exit(main())