Home | History | Annotate | Download | only in lit
      1 import os, signal, subprocess, sys
      2 import StringIO
      3 
      4 import ShUtil
      5 import Test
      6 import Util
      7 
      8 import platform
      9 import tempfile
     10 
     11 import re
     12 
     13 class InternalShellError(Exception):
     14     def __init__(self, command, message):
     15         self.command = command
     16         self.message = message
     17 
     18 kIsWindows = platform.system() == 'Windows'
     19 
     20 # Don't use close_fds on Windows.
     21 kUseCloseFDs = not kIsWindows
     22 
     23 # Use temporary files to replace /dev/null on Windows.
     24 kAvoidDevNull = kIsWindows
     25 
     26 def executeCommand(command, cwd=None, env=None):
     27     p = subprocess.Popen(command, cwd=cwd,
     28                          stdin=subprocess.PIPE,
     29                          stdout=subprocess.PIPE,
     30                          stderr=subprocess.PIPE,
     31                          env=env)
     32     out,err = p.communicate()
     33     exitCode = p.wait()
     34 
     35     # Detect Ctrl-C in subprocess.
     36     if exitCode == -signal.SIGINT:
     37         raise KeyboardInterrupt
     38 
     39     return out, err, exitCode
     40 
     41 def executeShCmd(cmd, cfg, cwd, results):
     42     if isinstance(cmd, ShUtil.Seq):
     43         if cmd.op == ';':
     44             res = executeShCmd(cmd.lhs, cfg, cwd, results)
     45             return executeShCmd(cmd.rhs, cfg, cwd, results)
     46 
     47         if cmd.op == '&':
     48             raise NotImplementedError,"unsupported test command: '&'"
     49 
     50         if cmd.op == '||':
     51             res = executeShCmd(cmd.lhs, cfg, cwd, results)
     52             if res != 0:
     53                 res = executeShCmd(cmd.rhs, cfg, cwd, results)
     54             return res
     55         if cmd.op == '&&':
     56             res = executeShCmd(cmd.lhs, cfg, cwd, results)
     57             if res is None:
     58                 return res
     59 
     60             if res == 0:
     61                 res = executeShCmd(cmd.rhs, cfg, cwd, results)
     62             return res
     63 
     64         raise ValueError,'Unknown shell command: %r' % cmd.op
     65 
     66     assert isinstance(cmd, ShUtil.Pipeline)
     67     procs = []
     68     input = subprocess.PIPE
     69     stderrTempFiles = []
     70     opened_files = []
     71     named_temp_files = []
     72     # To avoid deadlock, we use a single stderr stream for piped
     73     # output. This is null until we have seen some output using
     74     # stderr.
     75     for i,j in enumerate(cmd.commands):
     76         # Apply the redirections, we use (N,) as a sentinal to indicate stdin,
     77         # stdout, stderr for N equal to 0, 1, or 2 respectively. Redirects to or
     78         # from a file are represented with a list [file, mode, file-object]
     79         # where file-object is initially None.
     80         redirects = [(0,), (1,), (2,)]
     81         for r in j.redirects:
     82             if r[0] == ('>',2):
     83                 redirects[2] = [r[1], 'w', None]
     84             elif r[0] == ('>>',2):
     85                 redirects[2] = [r[1], 'a', None]
     86             elif r[0] == ('>&',2) and r[1] in '012':
     87                 redirects[2] = redirects[int(r[1])]
     88             elif r[0] == ('>&',) or r[0] == ('&>',):
     89                 redirects[1] = redirects[2] = [r[1], 'w', None]
     90             elif r[0] == ('>',):
     91                 redirects[1] = [r[1], 'w', None]
     92             elif r[0] == ('>>',):
     93                 redirects[1] = [r[1], 'a', None]
     94             elif r[0] == ('<',):
     95                 redirects[0] = [r[1], 'r', None]
     96             else:
     97                 raise NotImplementedError,"Unsupported redirect: %r" % (r,)
     98 
     99         # Map from the final redirections to something subprocess can handle.
    100         final_redirects = []
    101         for index,r in enumerate(redirects):
    102             if r == (0,):
    103                 result = input
    104             elif r == (1,):
    105                 if index == 0:
    106                     raise NotImplementedError,"Unsupported redirect for stdin"
    107                 elif index == 1:
    108                     result = subprocess.PIPE
    109                 else:
    110                     result = subprocess.STDOUT
    111             elif r == (2,):
    112                 if index != 2:
    113                     raise NotImplementedError,"Unsupported redirect on stdout"
    114                 result = subprocess.PIPE
    115             else:
    116                 if r[2] is None:
    117                     if kAvoidDevNull and r[0] == '/dev/null':
    118                         r[2] = tempfile.TemporaryFile(mode=r[1])
    119                     else:
    120                         r[2] = open(r[0], r[1])
    121                     # Workaround a Win32 and/or subprocess bug when appending.
    122                     #
    123                     # FIXME: Actually, this is probably an instance of PR6753.
    124                     if r[1] == 'a':
    125                         r[2].seek(0, 2)
    126                     opened_files.append(r[2])
    127                 result = r[2]
    128             final_redirects.append(result)
    129 
    130         stdin, stdout, stderr = final_redirects
    131 
    132         # If stderr wants to come from stdout, but stdout isn't a pipe, then put
    133         # stderr on a pipe and treat it as stdout.
    134         if (stderr == subprocess.STDOUT and stdout != subprocess.PIPE):
    135             stderr = subprocess.PIPE
    136             stderrIsStdout = True
    137         else:
    138             stderrIsStdout = False
    139 
    140             # Don't allow stderr on a PIPE except for the last
    141             # process, this could deadlock.
    142             #
    143             # FIXME: This is slow, but so is deadlock.
    144             if stderr == subprocess.PIPE and j != cmd.commands[-1]:
    145                 stderr = tempfile.TemporaryFile(mode='w+b')
    146                 stderrTempFiles.append((i, stderr))
    147 
    148         # Resolve the executable path ourselves.
    149         args = list(j.args)
    150         args[0] = Util.which(args[0], cfg.environment['PATH'])
    151         if not args[0]:
    152             raise InternalShellError(j, '%r: command not found' % j.args[0])
    153 
    154         # Replace uses of /dev/null with temporary files.
    155         if kAvoidDevNull:
    156             for i,arg in enumerate(args):
    157                 if arg == "/dev/null":
    158                     f = tempfile.NamedTemporaryFile(delete=False)
    159                     f.close()
    160                     named_temp_files.append(f.name)
    161                     args[i] = f.name
    162 
    163         procs.append(subprocess.Popen(args, cwd=cwd,
    164                                       stdin = stdin,
    165                                       stdout = stdout,
    166                                       stderr = stderr,
    167                                       env = cfg.environment,
    168                                       close_fds = kUseCloseFDs))
    169 
    170         # Immediately close stdin for any process taking stdin from us.
    171         if stdin == subprocess.PIPE:
    172             procs[-1].stdin.close()
    173             procs[-1].stdin = None
    174 
    175         # Update the current stdin source.
    176         if stdout == subprocess.PIPE:
    177             input = procs[-1].stdout
    178         elif stderrIsStdout:
    179             input = procs[-1].stderr
    180         else:
    181             input = subprocess.PIPE
    182 
    183     # Explicitly close any redirected files. We need to do this now because we
    184     # need to release any handles we may have on the temporary files (important
    185     # on Win32, for example). Since we have already spawned the subprocess, our
    186     # handles have already been transferred so we do not need them anymore.
    187     for f in opened_files:
    188         f.close()
    189 
    190     # FIXME: There is probably still deadlock potential here. Yawn.
    191     procData = [None] * len(procs)
    192     procData[-1] = procs[-1].communicate()
    193 
    194     for i in range(len(procs) - 1):
    195         if procs[i].stdout is not None:
    196             out = procs[i].stdout.read()
    197         else:
    198             out = ''
    199         if procs[i].stderr is not None:
    200             err = procs[i].stderr.read()
    201         else:
    202             err = ''
    203         procData[i] = (out,err)
    204 
    205     # Read stderr out of the temp files.
    206     for i,f in stderrTempFiles:
    207         f.seek(0, 0)
    208         procData[i] = (procData[i][0], f.read())
    209 
    210     exitCode = None
    211     for i,(out,err) in enumerate(procData):
    212         res = procs[i].wait()
    213         # Detect Ctrl-C in subprocess.
    214         if res == -signal.SIGINT:
    215             raise KeyboardInterrupt
    216 
    217         results.append((cmd.commands[i], out, err, res))
    218         if cmd.pipe_err:
    219             # Python treats the exit code as a signed char.
    220             if res < 0:
    221                 exitCode = min(exitCode, res)
    222             else:
    223                 exitCode = max(exitCode, res)
    224         else:
    225             exitCode = res
    226 
    227     # Remove any named temporary files we created.
    228     for f in named_temp_files:
    229         try:
    230             os.remove(f)
    231         except OSError:
    232             pass
    233 
    234     if cmd.negate:
    235         exitCode = not exitCode
    236 
    237     return exitCode
    238 
    239 def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
    240     ln = ' &&\n'.join(commands)
    241     try:
    242         cmd = ShUtil.ShParser(ln, litConfig.isWindows).parse()
    243     except:
    244         return (Test.FAIL, "shell parser error on: %r" % ln)
    245 
    246     results = []
    247     try:
    248         exitCode = executeShCmd(cmd, test.config, cwd, results)
    249     except InternalShellError,e:
    250         out = ''
    251         err = e.message
    252         exitCode = 255
    253 
    254     out = err = ''
    255     for i,(cmd, cmd_out,cmd_err,res) in enumerate(results):
    256         out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
    257         out += 'Command %d Result: %r\n' % (i, res)
    258         out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
    259         out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
    260 
    261     return out, err, exitCode
    262 
    263 def executeTclScriptInternal(test, litConfig, tmpBase, commands, cwd):
    264     import TclUtil
    265     cmds = []
    266     for ln in commands:
    267         # Given the unfortunate way LLVM's test are written, the line gets
    268         # backslash substitution done twice.
    269         ln = TclUtil.TclLexer(ln).lex_unquoted(process_all = True)
    270 
    271         try:
    272             tokens = list(TclUtil.TclLexer(ln).lex())
    273         except:
    274             return (Test.FAIL, "Tcl lexer error on: %r" % ln)
    275 
    276         # Validate there are no control tokens.
    277         for t in tokens:
    278             if not isinstance(t, str):
    279                 return (Test.FAIL,
    280                         "Invalid test line: %r containing %r" % (ln, t))
    281 
    282         try:
    283             cmds.append(TclUtil.TclExecCommand(tokens).parse_pipeline())
    284         except:
    285             return (Test.FAIL, "Tcl 'exec' parse error on: %r" % ln)
    286 
    287     if litConfig.useValgrind:
    288         for pipeline in cmds:
    289             if pipeline.commands:
    290                 # Only valgrind the first command in each pipeline, to avoid
    291                 # valgrinding things like grep, not, and FileCheck.
    292                 cmd = pipeline.commands[0]
    293                 cmd.args = litConfig.valgrindArgs + cmd.args
    294 
    295     cmd = cmds[0]
    296     for c in cmds[1:]:
    297         cmd = ShUtil.Seq(cmd, '&&', c)
    298 
    299     # FIXME: This is lame, we shouldn't need bash. See PR5240.
    300     bashPath = litConfig.getBashPath()
    301     if litConfig.useTclAsSh and bashPath:
    302         script = tmpBase + '.script'
    303 
    304         # Write script file
    305         f = open(script,'w')
    306         print >>f, 'set -o pipefail'
    307         cmd.toShell(f, pipefail = True)
    308         f.close()
    309 
    310         if 0:
    311             print >>sys.stdout, cmd
    312             print >>sys.stdout, open(script).read()
    313             print >>sys.stdout
    314             return '', '', 0
    315 
    316         command = [litConfig.getBashPath(), script]
    317         out,err,exitCode = executeCommand(command, cwd=cwd,
    318                                           env=test.config.environment)
    319 
    320         return out,err,exitCode
    321     else:
    322         results = []
    323         try:
    324             exitCode = executeShCmd(cmd, test.config, cwd, results)
    325         except InternalShellError,e:
    326             results.append((e.command, '', e.message + '\n', 255))
    327             exitCode = 255
    328 
    329     out = err = ''
    330 
    331     for i,(cmd, cmd_out, cmd_err, res) in enumerate(results):
    332         out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
    333         out += 'Command %d Result: %r\n' % (i, res)
    334         out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
    335         out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
    336 
    337     return out, err, exitCode
    338 
    339 def executeScript(test, litConfig, tmpBase, commands, cwd):
    340     bashPath = litConfig.getBashPath();
    341     isWin32CMDEXE = (litConfig.isWindows and not bashPath)
    342     script = tmpBase + '.script'
    343     if isWin32CMDEXE:
    344         script += '.bat'
    345 
    346     # Write script file
    347     f = open(script,'w')
    348     if isWin32CMDEXE:
    349         f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
    350     else:
    351         f.write(' &&\n'.join(commands))
    352     f.write('\n')
    353     f.close()
    354 
    355     if isWin32CMDEXE:
    356         command = ['cmd','/c', script]
    357     else:
    358         if bashPath:
    359             command = [bashPath, script]
    360         else:
    361             command = ['/bin/sh', script]
    362         if litConfig.useValgrind:
    363             # FIXME: Running valgrind on sh is overkill. We probably could just
    364             # run on clang with no real loss.
    365             command = litConfig.valgrindArgs + command
    366 
    367     return executeCommand(command, cwd=cwd, env=test.config.environment)
    368 
    369 def isExpectedFail(xfails, xtargets, target_triple):
    370     # Check if any xfail matches this target.
    371     for item in xfails:
    372         if item == '*' or item in target_triple:
    373             break
    374     else:
    375         return False
    376 
    377     # If so, see if it is expected to pass on this target.
    378     #
    379     # FIXME: Rename XTARGET to something that makes sense, like XPASS.
    380     for item in xtargets:
    381         if item == '*' or item in target_triple:
    382             return False
    383 
    384     return True
    385 
    386 def parseIntegratedTestScript(test, normalize_slashes=False):
    387     """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
    388     script and extract the lines to 'RUN' as well as 'XFAIL' and 'XTARGET'
    389     information. The RUN lines also will have variable substitution performed.
    390     """
    391 
    392     # Get the temporary location, this is always relative to the test suite
    393     # root, not test source root.
    394     #
    395     # FIXME: This should not be here?
    396     sourcepath = test.getSourcePath()
    397     sourcedir = os.path.dirname(sourcepath)
    398     execpath = test.getExecPath()
    399     execdir,execbase = os.path.split(execpath)
    400     tmpDir = os.path.join(execdir, 'Output')
    401     tmpBase = os.path.join(tmpDir, execbase)
    402     if test.index is not None:
    403         tmpBase += '_%d' % test.index
    404 
    405     # Normalize slashes, if requested.
    406     if normalize_slashes:
    407         sourcepath = sourcepath.replace('\\', '/')
    408         sourcedir = sourcedir.replace('\\', '/')
    409         tmpDir = tmpDir.replace('\\', '/')
    410         tmpBase = tmpBase.replace('\\', '/')
    411 
    412     # We use #_MARKER_# to hide %% while we do the other substitutions.
    413     substitutions = [('%%', '#_MARKER_#')]
    414     substitutions.extend(test.config.substitutions)
    415     substitutions.extend([('%s', sourcepath),
    416                           ('%S', sourcedir),
    417                           ('%p', sourcedir),
    418                           ('%t', tmpBase + '.tmp'),
    419                           ('%T', tmpDir),
    420                           # FIXME: Remove this once we kill DejaGNU.
    421                           ('%abs_tmp', tmpBase + '.tmp'),
    422                           ('#_MARKER_#', '%')])
    423 
    424     # Collect the test lines from the script.
    425     script = []
    426     xfails = []
    427     xtargets = []
    428     requires = []
    429     for ln in open(sourcepath):
    430         if 'RUN:' in ln:
    431             # Isolate the command to run.
    432             index = ln.index('RUN:')
    433             ln = ln[index+4:]
    434 
    435             # Trim trailing whitespace.
    436             ln = ln.rstrip()
    437 
    438             # Collapse lines with trailing '\\'.
    439             if script and script[-1][-1] == '\\':
    440                 script[-1] = script[-1][:-1] + ln
    441             else:
    442                 script.append(ln)
    443         elif 'XFAIL:' in ln:
    444             items = ln[ln.index('XFAIL:') + 6:].split(',')
    445             xfails.extend([s.strip() for s in items])
    446         elif 'XTARGET:' in ln:
    447             items = ln[ln.index('XTARGET:') + 8:].split(',')
    448             xtargets.extend([s.strip() for s in items])
    449         elif 'REQUIRES:' in ln:
    450             items = ln[ln.index('REQUIRES:') + 9:].split(',')
    451             requires.extend([s.strip() for s in items])
    452         elif 'END.' in ln:
    453             # Check for END. lines.
    454             if ln[ln.index('END.'):].strip() == 'END.':
    455                 break
    456 
    457     # Apply substitutions to the script.  Allow full regular
    458     # expression syntax.  Replace each matching occurrence of regular
    459     # expression pattern a with substitution b in line ln.
    460     def processLine(ln):
    461         # Apply substitutions
    462         for a,b in substitutions:
    463             if kIsWindows:
    464                 b = b.replace("\\","\\\\")
    465             ln = re.sub(a, b, ln)
    466 
    467         # Strip the trailing newline and any extra whitespace.
    468         return ln.strip()
    469     script = map(processLine, script)
    470 
    471     # Verify the script contains a run line.
    472     if not script:
    473         return (Test.UNRESOLVED, "Test has no run line!")
    474 
    475     # Check for unterminated run lines.
    476     if script[-1][-1] == '\\':
    477         return (Test.UNRESOLVED, "Test has unterminated run lines (with '\\')")
    478 
    479     # Check that we have the required features:
    480     missing_required_features = [f for f in requires
    481                                  if f not in test.config.available_features]
    482     if missing_required_features:
    483         msg = ', '.join(missing_required_features)
    484         return (Test.UNSUPPORTED,
    485                 "Test requires the following features: %s" % msg)
    486 
    487     isXFail = isExpectedFail(xfails, xtargets, test.suite.config.target_triple)
    488     return script,isXFail,tmpBase,execdir
    489 
    490 def formatTestOutput(status, out, err, exitCode, failDueToStderr, script):
    491     output = StringIO.StringIO()
    492     print >>output, "Script:"
    493     print >>output, "--"
    494     print >>output, '\n'.join(script)
    495     print >>output, "--"
    496     print >>output, "Exit Code: %r" % exitCode,
    497     if failDueToStderr:
    498         print >>output, "(but there was output on stderr)"
    499     else:
    500         print >>output
    501     if out:
    502         print >>output, "Command Output (stdout):"
    503         print >>output, "--"
    504         output.write(out)
    505         print >>output, "--"
    506     if err:
    507         print >>output, "Command Output (stderr):"
    508         print >>output, "--"
    509         output.write(err)
    510         print >>output, "--"
    511     return (status, output.getvalue())
    512 
    513 def executeTclTest(test, litConfig):
    514     if test.config.unsupported:
    515         return (Test.UNSUPPORTED, 'Test is unsupported')
    516 
    517     # Parse the test script, normalizing slashes in substitutions on Windows
    518     # (since otherwise Tcl style lexing will treat them as escapes).
    519     res = parseIntegratedTestScript(test, normalize_slashes=kIsWindows)
    520     if len(res) == 2:
    521         return res
    522 
    523     script, isXFail, tmpBase, execdir = res
    524 
    525     if litConfig.noExecute:
    526         return (Test.PASS, '')
    527 
    528     # Create the output directory if it does not already exist.
    529     Util.mkdir_p(os.path.dirname(tmpBase))
    530 
    531     res = executeTclScriptInternal(test, litConfig, tmpBase, script, execdir)
    532     if len(res) == 2:
    533         return res
    534 
    535     # Test for failure. In addition to the exit code, Tcl commands are
    536     # considered to fail if there is any standard error output.
    537     out,err,exitCode = res
    538     if isXFail:
    539         ok = exitCode != 0 or err and not litConfig.ignoreStdErr
    540         if ok:
    541             status = Test.XFAIL
    542         else:
    543             status = Test.XPASS
    544     else:
    545         ok = exitCode == 0 and (not err or litConfig.ignoreStdErr)
    546         if ok:
    547             status = Test.PASS
    548         else:
    549             status = Test.FAIL
    550 
    551     if ok:
    552         return (status,'')
    553 
    554     # Set a flag for formatTestOutput so it can explain why the test was
    555     # considered to have failed, despite having an exit code of 0.
    556     failDueToStderr = exitCode == 0 and err and not litConfig.ignoreStdErr
    557 
    558     return formatTestOutput(status, out, err, exitCode, failDueToStderr, script)
    559 
    560 def executeShTest(test, litConfig, useExternalSh):
    561     if test.config.unsupported:
    562         return (Test.UNSUPPORTED, 'Test is unsupported')
    563 
    564     res = parseIntegratedTestScript(test, useExternalSh)
    565     if len(res) == 2:
    566         return res
    567 
    568     script, isXFail, tmpBase, execdir = res
    569 
    570     if litConfig.noExecute:
    571         return (Test.PASS, '')
    572 
    573     # Create the output directory if it does not already exist.
    574     Util.mkdir_p(os.path.dirname(tmpBase))
    575 
    576     if useExternalSh:
    577         res = executeScript(test, litConfig, tmpBase, script, execdir)
    578     else:
    579         res = executeScriptInternal(test, litConfig, tmpBase, script, execdir)
    580     if len(res) == 2:
    581         return res
    582 
    583     out,err,exitCode = res
    584     if isXFail:
    585         ok = exitCode != 0
    586         if ok:
    587             status = Test.XFAIL
    588         else:
    589             status = Test.XPASS
    590     else:
    591         ok = exitCode == 0
    592         if ok:
    593             status = Test.PASS
    594         else:
    595             status = Test.FAIL
    596 
    597     if ok:
    598         return (status,'')
    599 
    600     # Sh tests are not considered to fail just from stderr output.
    601     failDueToStderr = False
    602 
    603     return formatTestOutput(status, out, err, exitCode, failDueToStderr, script)
    604