diff options
Diffstat (limited to 'utils/lit/TestRunner.py')
| -rw-r--r-- | utils/lit/TestRunner.py | 505 | 
1 files changed, 505 insertions, 0 deletions
diff --git a/utils/lit/TestRunner.py b/utils/lit/TestRunner.py new file mode 100644 index 000000000000..7b549ac1c615 --- /dev/null +++ b/utils/lit/TestRunner.py @@ -0,0 +1,505 @@ +import os, signal, subprocess, sys +import StringIO + +import ShUtil +import Test +import Util + +import platform +import tempfile + +class InternalShellError(Exception): +    def __init__(self, command, message): +        self.command = command +        self.message = message + +# Don't use close_fds on Windows. +kUseCloseFDs = platform.system() != 'Windows' +def executeCommand(command, cwd=None, env=None): +    p = subprocess.Popen(command, cwd=cwd, +                         stdin=subprocess.PIPE, +                         stdout=subprocess.PIPE, +                         stderr=subprocess.PIPE, +                         env=env) +    out,err = p.communicate() +    exitCode = p.wait() + +    # Detect Ctrl-C in subprocess. +    if exitCode == -signal.SIGINT: +        raise KeyboardInterrupt + +    return out, err, exitCode + +def executeShCmd(cmd, cfg, cwd, results): +    if isinstance(cmd, ShUtil.Seq): +        if cmd.op == ';': +            res = executeShCmd(cmd.lhs, cfg, cwd, results) +            return executeShCmd(cmd.rhs, cfg, cwd, results) + +        if cmd.op == '&': +            raise NotImplementedError,"unsupported test command: '&'" + +        if cmd.op == '||': +            res = executeShCmd(cmd.lhs, cfg, cwd, results) +            if res != 0: +                res = executeShCmd(cmd.rhs, cfg, cwd, results) +            return res +        if cmd.op == '&&': +            res = executeShCmd(cmd.lhs, cfg, cwd, results) +            if res is None: +                return res + +            if res == 0: +                res = executeShCmd(cmd.rhs, cfg, cwd, results) +            return res + +        raise ValueError,'Unknown shell command: %r' % cmd.op + +    assert isinstance(cmd, ShUtil.Pipeline) +    procs = [] +    input = subprocess.PIPE +    stderrTempFiles = [] +    # To avoid deadlock, we use a single stderr stream for piped +    # output. This is null until we have seen some output using +    # stderr. +    for i,j in enumerate(cmd.commands): +        redirects = [(0,), (1,), (2,)] +        for r in j.redirects: +            if r[0] == ('>',2): +                redirects[2] = [r[1], 'w', None] +            elif r[0] == ('>&',2) and r[1] in '012': +                redirects[2] = redirects[int(r[1])] +            elif r[0] == ('>&',) or r[0] == ('&>',): +                redirects[1] = redirects[2] = [r[1], 'w', None] +            elif r[0] == ('>',): +                redirects[1] = [r[1], 'w', None] +            elif r[0] == ('<',): +                redirects[0] = [r[1], 'r', None] +            else: +                raise NotImplementedError,"Unsupported redirect: %r" % (r,) + +        final_redirects = [] +        for index,r in enumerate(redirects): +            if r == (0,): +                result = input +            elif r == (1,): +                if index == 0: +                    raise NotImplementedError,"Unsupported redirect for stdin" +                elif index == 1: +                    result = subprocess.PIPE +                else: +                    result = subprocess.STDOUT +            elif r == (2,): +                if index != 2: +                    raise NotImplementedError,"Unsupported redirect on stdout" +                result = subprocess.PIPE +            else: +                if r[2] is None: +                    r[2] = open(r[0], r[1]) +                result = r[2] +            final_redirects.append(result) + +        stdin, stdout, stderr = final_redirects + +        # If stderr wants to come from stdout, but stdout isn't a pipe, then put +        # stderr on a pipe and treat it as stdout. +        if (stderr == subprocess.STDOUT and stdout != subprocess.PIPE): +            stderr = subprocess.PIPE +            stderrIsStdout = True +        else: +            stderrIsStdout = False + +            # Don't allow stderr on a PIPE except for the last +            # process, this could deadlock. +            # +            # FIXME: This is slow, but so is deadlock. +            if stderr == subprocess.PIPE and j != cmd.commands[-1]: +                stderr = tempfile.TemporaryFile(mode='w+b') +                stderrTempFiles.append((i, stderr)) + +        # Resolve the executable path ourselves. +        args = list(j.args) +        args[0] = Util.which(args[0], cfg.environment['PATH']) +        if not args[0]: +            raise InternalShellError(j, '%r: command not found' % j.args[0]) + +        procs.append(subprocess.Popen(args, cwd=cwd, +                                      stdin = stdin, +                                      stdout = stdout, +                                      stderr = stderr, +                                      env = cfg.environment, +                                      close_fds = kUseCloseFDs)) + +        # Immediately close stdin for any process taking stdin from us. +        if stdin == subprocess.PIPE: +            procs[-1].stdin.close() +            procs[-1].stdin = None + +        # Update the current stdin source. +        if stdout == subprocess.PIPE: +            input = procs[-1].stdout +        elif stderrIsStdout: +            input = procs[-1].stderr +        else: +            input = subprocess.PIPE + +    # FIXME: There is probably still deadlock potential here. Yawn. +    procData = [None] * len(procs) +    procData[-1] = procs[-1].communicate() + +    for i in range(len(procs) - 1): +        if procs[i].stdout is not None: +            out = procs[i].stdout.read() +        else: +            out = '' +        if procs[i].stderr is not None: +            err = procs[i].stderr.read() +        else: +            err = '' +        procData[i] = (out,err) +         +    # Read stderr out of the temp files. +    for i,f in stderrTempFiles: +        f.seek(0, 0) +        procData[i] = (procData[i][0], f.read()) + +    exitCode = None +    for i,(out,err) in enumerate(procData): +        res = procs[i].wait() +        # Detect Ctrl-C in subprocess. +        if res == -signal.SIGINT: +            raise KeyboardInterrupt + +        results.append((cmd.commands[i], out, err, res)) +        if cmd.pipe_err: +            # Python treats the exit code as a signed char. +            if res < 0: +                exitCode = min(exitCode, res) +            else: +                exitCode = max(exitCode, res) +        else: +            exitCode = res + +    if cmd.negate: +        exitCode = not exitCode + +    return exitCode + +def executeScriptInternal(test, litConfig, tmpBase, commands, cwd): +    ln = ' &&\n'.join(commands) +    try: +        cmd = ShUtil.ShParser(ln, litConfig.isWindows).parse() +    except: +        return (Test.FAIL, "shell parser error on: %r" % ln) + +    results = [] +    try: +        exitCode = executeShCmd(cmd, test.config, cwd, results) +    except InternalShellError,e: +        out = '' +        err = e.message +        exitCode = 255 + +    out = err = '' +    for i,(cmd, cmd_out,cmd_err,res) in enumerate(results): +        out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args)) +        out += 'Command %d Result: %r\n' % (i, res) +        out += 'Command %d Output:\n%s\n\n' % (i, cmd_out) +        out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err) + +    return out, err, exitCode + +def executeTclScriptInternal(test, litConfig, tmpBase, commands, cwd): +    import TclUtil +    cmds = [] +    for ln in commands: +        # Given the unfortunate way LLVM's test are written, the line gets +        # backslash substitution done twice. +        ln = TclUtil.TclLexer(ln).lex_unquoted(process_all = True) + +        try: +            tokens = list(TclUtil.TclLexer(ln).lex()) +        except: +            return (Test.FAIL, "Tcl lexer error on: %r" % ln) + +        # Validate there are no control tokens. +        for t in tokens: +            if not isinstance(t, str): +                return (Test.FAIL, +                        "Invalid test line: %r containing %r" % (ln, t)) + +        try: +            cmds.append(TclUtil.TclExecCommand(tokens).parse_pipeline()) +        except: +            return (Test.FAIL, "Tcl 'exec' parse error on: %r" % ln) + +    cmd = cmds[0] +    for c in cmds[1:]: +        cmd = ShUtil.Seq(cmd, '&&', c) + +    if litConfig.useTclAsSh: +        script = tmpBase + '.script' + +        # Write script file +        f = open(script,'w') +        print >>f, 'set -o pipefail' +        cmd.toShell(f, pipefail = True) +        f.close() + +        if 0: +            print >>sys.stdout, cmd +            print >>sys.stdout, open(script).read() +            print >>sys.stdout +            return '', '', 0 + +        command = ['/bin/bash', script] +        out,err,exitCode = executeCommand(command, cwd=cwd, +                                          env=test.config.environment) + +        # Tcl commands fail on standard error output. +        if err: +            exitCode = 1 +            out = 'Command has output on stderr!\n\n' + out + +        return out,err,exitCode +    else: +        results = [] +        try: +            exitCode = executeShCmd(cmd, test.config, cwd, results) +        except InternalShellError,e: +            results.append((e.command, '', e.message + '\n', 255)) +            exitCode = 255 + +    out = err = '' + +    # Tcl commands fail on standard error output. +    if [True for _,_,err,res in results if err]: +        exitCode = 1 +        out += 'Command has output on stderr!\n\n' + +    for i,(cmd, cmd_out, cmd_err, res) in enumerate(results): +        out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args)) +        out += 'Command %d Result: %r\n' % (i, res) +        out += 'Command %d Output:\n%s\n\n' % (i, cmd_out) +        out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err) + +    return out, err, exitCode + +def executeScript(test, litConfig, tmpBase, commands, cwd): +    script = tmpBase + '.script' +    if litConfig.isWindows: +        script += '.bat' + +    # Write script file +    f = open(script,'w') +    if litConfig.isWindows: +        f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands)) +    else: +        f.write(' &&\n'.join(commands)) +    f.write('\n') +    f.close() + +    if litConfig.isWindows: +        command = ['cmd','/c', script] +    else: +        command = ['/bin/sh', script] +        if litConfig.useValgrind: +            # FIXME: Running valgrind on sh is overkill. We probably could just +            # run on clang with no real loss. +            valgrindArgs = ['valgrind', '-q', +                            '--tool=memcheck', '--trace-children=yes', +                            '--error-exitcode=123'] +            valgrindArgs.extend(litConfig.valgrindArgs) + +            command = valgrindArgs + command + +    return executeCommand(command, cwd=cwd, env=test.config.environment) + +def parseIntegratedTestScript(test, xfailHasColon, requireAndAnd): +    """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test +    script and extract the lines to 'RUN' as well as 'XFAIL' and 'XTARGET' +    information. The RUN lines also will have variable substitution performed. +    """ + +    # Get the temporary location, this is always relative to the test suite +    # root, not test source root. +    # +    # FIXME: This should not be here? +    sourcepath = test.getSourcePath() +    execpath = test.getExecPath() +    execdir,execbase = os.path.split(execpath) +    tmpBase = os.path.join(execdir, 'Output', execbase) + +    # We use #_MARKER_# to hide %% while we do the other substitutions. +    substitutions = [('%%', '#_MARKER_#')] +    substitutions.extend(test.config.substitutions) +    substitutions.extend([('%s', sourcepath), +                          ('%S', os.path.dirname(sourcepath)), +                          ('%p', os.path.dirname(sourcepath)), +                          ('%t', tmpBase + '.tmp'), +                          # FIXME: Remove this once we kill DejaGNU. +                          ('%abs_tmp', tmpBase + '.tmp'), +                          ('#_MARKER_#', '%')]) + +    # Collect the test lines from the script. +    script = [] +    xfails = [] +    xtargets = [] +    for ln in open(sourcepath): +        if 'RUN:' in ln: +            # Isolate the command to run. +            index = ln.index('RUN:') +            ln = ln[index+4:] + +            # Trim trailing whitespace. +            ln = ln.rstrip() + +            # Collapse lines with trailing '\\'. +            if script and script[-1][-1] == '\\': +                script[-1] = script[-1][:-1] + ln +            else: +                script.append(ln) +        elif xfailHasColon and 'XFAIL:' in ln: +            items = ln[ln.index('XFAIL:') + 6:].split(',') +            xfails.extend([s.strip() for s in items]) +        elif not xfailHasColon and 'XFAIL' in ln: +            items = ln[ln.index('XFAIL') + 5:].split(',') +            xfails.extend([s.strip() for s in items]) +        elif 'XTARGET:' in ln: +            items = ln[ln.index('XTARGET:') + 8:].split(',') +            xtargets.extend([s.strip() for s in items]) +        elif 'END.' in ln: +            # Check for END. lines. +            if ln[ln.index('END.'):].strip() == 'END.': +                break + +    # Apply substitutions to the script. +    def processLine(ln): +        # Apply substitutions +        for a,b in substitutions: +            ln = ln.replace(a,b) + +        # Strip the trailing newline and any extra whitespace. +        return ln.strip() +    script = map(processLine, script) + +    # Verify the script contains a run line. +    if not script: +        return (Test.UNRESOLVED, "Test has no run line!") + +    if script[-1][-1] == '\\': +        return (Test.UNRESOLVED, "Test has unterminated run lines (with '\\')") + +    # Validate interior lines for '&&', a lovely historical artifact. +    if requireAndAnd: +        for i in range(len(script) - 1): +            ln = script[i] + +            if not ln.endswith('&&'): +                return (Test.FAIL, +                        ("MISSING \'&&\': %s\n"  + +                         "FOLLOWED BY   : %s\n") % (ln, script[i + 1])) + +            # Strip off '&&' +            script[i] = ln[:-2] + +    return script,xfails,xtargets,tmpBase,execdir + +def formatTestOutput(status, out, err, exitCode, script): +    output = StringIO.StringIO() +    print >>output, "Script:" +    print >>output, "--" +    print >>output, '\n'.join(script) +    print >>output, "--" +    print >>output, "Exit Code: %r" % exitCode +    print >>output, "Command Output (stdout):" +    print >>output, "--" +    output.write(out) +    print >>output, "--" +    print >>output, "Command Output (stderr):" +    print >>output, "--" +    output.write(err) +    print >>output, "--" +    return (status, output.getvalue()) + +def executeTclTest(test, litConfig): +    if test.config.unsupported: +        return (Test.UNSUPPORTED, 'Test is unsupported') + +    res = parseIntegratedTestScript(test, True, False) +    if len(res) == 2: +        return res + +    script, xfails, xtargets, tmpBase, execdir = res + +    if litConfig.noExecute: +        return (Test.PASS, '') + +    # Create the output directory if it does not already exist. +    Util.mkdir_p(os.path.dirname(tmpBase)) + +    res = executeTclScriptInternal(test, litConfig, tmpBase, script, execdir) +    if len(res) == 2: +        return res + +    isXFail = False +    for item in xfails: +        if item == '*' or item in test.suite.config.target_triple: +            isXFail = True +            break + +    # If this is XFAIL, see if it is expected to pass on this target. +    if isXFail: +        for item in xtargets: +            if item == '*' or item in test.suite.config.target_triple: +                isXFail = False +                break + +    out,err,exitCode = res +    if isXFail: +        ok = exitCode != 0 +        status = (Test.XPASS, Test.XFAIL)[ok] +    else: +        ok = exitCode == 0 +        status = (Test.FAIL, Test.PASS)[ok] + +    if ok: +        return (status,'') + +    return formatTestOutput(status, out, err, exitCode, script) + +def executeShTest(test, litConfig, useExternalSh, requireAndAnd): +    if test.config.unsupported: +        return (Test.UNSUPPORTED, 'Test is unsupported') + +    res = parseIntegratedTestScript(test, False, requireAndAnd) +    if len(res) == 2: +        return res + +    script, xfails, xtargets, tmpBase, execdir = res + +    if litConfig.noExecute: +        return (Test.PASS, '') + +    # Create the output directory if it does not already exist. +    Util.mkdir_p(os.path.dirname(tmpBase)) + +    if useExternalSh: +        res = executeScript(test, litConfig, tmpBase, script, execdir) +    else: +        res = executeScriptInternal(test, litConfig, tmpBase, script, execdir) +    if len(res) == 2: +        return res + +    out,err,exitCode = res +    if xfails: +        ok = exitCode != 0 +        status = (Test.XPASS, Test.XFAIL)[ok] +    else: +        ok = exitCode == 0 +        status = (Test.FAIL, Test.PASS)[ok] + +    if ok: +        return (status,'') + +    return formatTestOutput(status, out, err, exitCode, script)  | 
