Home | History | Annotate | Download | only in lit
      1 from __future__ import absolute_import
      2 import os, signal, subprocess, sys
      3 import re
      4 import platform
      5 import tempfile
      6 
      7 import lit.ShUtil as ShUtil
      8 import lit.Test as Test
      9 import lit.util
     10 from lit.util import to_bytes, to_string
     11 
     12 class InternalShellError(Exception):
     13     def __init__(self, command, message):
     14         self.command = command
     15         self.message = message
     16 
     17 kIsWindows = platform.system() == 'Windows'
     18 
     19 # Don't use close_fds on Windows.
     20 kUseCloseFDs = not kIsWindows
     21 
     22 # Use temporary files to replace /dev/null on Windows.
     23 kAvoidDevNull = kIsWindows
     24 
     25 class ShellEnvironment(object):
     26 
     27     """Mutable shell environment containing things like CWD and env vars.
     28 
     29     Environment variables are not implemented, but cwd tracking is.
     30     """
     31 
     32     def __init__(self, cwd, env):
     33         self.cwd = cwd
     34         self.env = env
     35 
     36 def executeShCmd(cmd, shenv, results):
     37     if isinstance(cmd, ShUtil.Seq):
     38         if cmd.op == ';':
     39             res = executeShCmd(cmd.lhs, shenv, results)
     40             return executeShCmd(cmd.rhs, shenv, results)
     41 
     42         if cmd.op == '&':
     43             raise InternalShellError(cmd,"unsupported shell operator: '&'")
     44 
     45         if cmd.op == '||':
     46             res = executeShCmd(cmd.lhs, shenv, results)
     47             if res != 0:
     48                 res = executeShCmd(cmd.rhs, shenv, results)
     49             return res
     50 
     51         if cmd.op == '&&':
     52             res = executeShCmd(cmd.lhs, shenv, results)
     53             if res is None:
     54                 return res
     55 
     56             if res == 0:
     57                 res = executeShCmd(cmd.rhs, shenv, results)
     58             return res
     59 
     60         raise ValueError('Unknown shell command: %r' % cmd.op)
     61     assert isinstance(cmd, ShUtil.Pipeline)
     62 
     63     # Handle shell builtins first.
     64     if cmd.commands[0].args[0] == 'cd':
     65         # Update the cwd in the environment.
     66         if len(cmd.commands[0].args) != 2:
     67             raise ValueError('cd supports only one argument')
     68         newdir = cmd.commands[0].args[1]
     69         if os.path.isabs(newdir):
     70             shenv.cwd = newdir
     71         else:
     72             shenv.cwd = os.path.join(shenv.cwd, newdir)
     73         return 0
     74 
     75     procs = []
     76     input = subprocess.PIPE
     77     stderrTempFiles = []
     78     opened_files = []
     79     named_temp_files = []
     80     # To avoid deadlock, we use a single stderr stream for piped
     81     # output. This is null until we have seen some output using
     82     # stderr.
     83     for i,j in enumerate(cmd.commands):
     84         # Apply the redirections, we use (N,) as a sentinel to indicate stdin,
     85         # stdout, stderr for N equal to 0, 1, or 2 respectively. Redirects to or
     86         # from a file are represented with a list [file, mode, file-object]
     87         # where file-object is initially None.
     88         redirects = [(0,), (1,), (2,)]
     89         for r in j.redirects:
     90             if r[0] == ('>',2):
     91                 redirects[2] = [r[1], 'w', None]
     92             elif r[0] == ('>>',2):
     93                 redirects[2] = [r[1], 'a', None]
     94             elif r[0] == ('>&',2) and r[1] in '012':
     95                 redirects[2] = redirects[int(r[1])]
     96             elif r[0] == ('>&',) or r[0] == ('&>',):
     97                 redirects[1] = redirects[2] = [r[1], 'w', None]
     98             elif r[0] == ('>',):
     99                 redirects[1] = [r[1], 'w', None]
    100             elif r[0] == ('>>',):
    101                 redirects[1] = [r[1], 'a', None]
    102             elif r[0] == ('<',):
    103                 redirects[0] = [r[1], 'r', None]
    104             else:
    105                 raise InternalShellError(j,"Unsupported redirect: %r" % (r,))
    106 
    107         # Map from the final redirections to something subprocess can handle.
    108         final_redirects = []
    109         for index,r in enumerate(redirects):
    110             if r == (0,):
    111                 result = input
    112             elif r == (1,):
    113                 if index == 0:
    114                     raise InternalShellError(j,"Unsupported redirect for stdin")
    115                 elif index == 1:
    116                     result = subprocess.PIPE
    117                 else:
    118                     result = subprocess.STDOUT
    119             elif r == (2,):
    120                 if index != 2:
    121                     raise InternalShellError(j,"Unsupported redirect on stdout")
    122                 result = subprocess.PIPE
    123             else:
    124                 if r[2] is None:
    125                     if kAvoidDevNull and r[0] == '/dev/null':
    126                         r[2] = tempfile.TemporaryFile(mode=r[1])
    127                     else:
    128                         # Make sure relative paths are relative to the cwd.
    129                         redir_filename = os.path.join(shenv.cwd, r[0])
    130                         r[2] = open(redir_filename, r[1])
    131                     # Workaround a Win32 and/or subprocess bug when appending.
    132                     #
    133                     # FIXME: Actually, this is probably an instance of PR6753.
    134                     if r[1] == 'a':
    135                         r[2].seek(0, 2)
    136                     opened_files.append(r[2])
    137                 result = r[2]
    138             final_redirects.append(result)
    139 
    140         stdin, stdout, stderr = final_redirects
    141 
    142         # If stderr wants to come from stdout, but stdout isn't a pipe, then put
    143         # stderr on a pipe and treat it as stdout.
    144         if (stderr == subprocess.STDOUT and stdout != subprocess.PIPE):
    145             stderr = subprocess.PIPE
    146             stderrIsStdout = True
    147         else:
    148             stderrIsStdout = False
    149 
    150             # Don't allow stderr on a PIPE except for the last
    151             # process, this could deadlock.
    152             #
    153             # FIXME: This is slow, but so is deadlock.
    154             if stderr == subprocess.PIPE and j != cmd.commands[-1]:
    155                 stderr = tempfile.TemporaryFile(mode='w+b')
    156                 stderrTempFiles.append((i, stderr))
    157 
    158         # Resolve the executable path ourselves.
    159         args = list(j.args)
    160         executable = lit.util.which(args[0], shenv.env['PATH'])
    161         if not executable:
    162             raise InternalShellError(j, '%r: command not found' % j.args[0])
    163 
    164         # Replace uses of /dev/null with temporary files.
    165         if kAvoidDevNull:
    166             for i,arg in enumerate(args):
    167                 if arg == "/dev/null":
    168                     f = tempfile.NamedTemporaryFile(delete=False)
    169                     f.close()
    170                     named_temp_files.append(f.name)
    171                     args[i] = f.name
    172 
    173         try:
    174             procs.append(subprocess.Popen(args, cwd=shenv.cwd,
    175                                           executable = executable,
    176                                           stdin = stdin,
    177                                           stdout = stdout,
    178                                           stderr = stderr,
    179                                           env = shenv.env,
    180                                           close_fds = kUseCloseFDs))
    181         except OSError as e:
    182             raise InternalShellError(j, 'Could not create process due to {}'.format(e))
    183 
    184         # Immediately close stdin for any process taking stdin from us.
    185         if stdin == subprocess.PIPE:
    186             procs[-1].stdin.close()
    187             procs[-1].stdin = None
    188 
    189         # Update the current stdin source.
    190         if stdout == subprocess.PIPE:
    191             input = procs[-1].stdout
    192         elif stderrIsStdout:
    193             input = procs[-1].stderr
    194         else:
    195             input = subprocess.PIPE
    196 
    197     # Explicitly close any redirected files. We need to do this now because we
    198     # need to release any handles we may have on the temporary files (important
    199     # on Win32, for example). Since we have already spawned the subprocess, our
    200     # handles have already been transferred so we do not need them anymore.
    201     for f in opened_files:
    202         f.close()
    203 
    204     # FIXME: There is probably still deadlock potential here. Yawn.
    205     procData = [None] * len(procs)
    206     procData[-1] = procs[-1].communicate()
    207 
    208     for i in range(len(procs) - 1):
    209         if procs[i].stdout is not None:
    210             out = procs[i].stdout.read()
    211         else:
    212             out = ''
    213         if procs[i].stderr is not None:
    214             err = procs[i].stderr.read()
    215         else:
    216             err = ''
    217         procData[i] = (out,err)
    218 
    219     # Read stderr out of the temp files.
    220     for i,f in stderrTempFiles:
    221         f.seek(0, 0)
    222         procData[i] = (procData[i][0], f.read())
    223 
    224     def to_string(bytes):
    225         if isinstance(bytes, str):
    226             return bytes
    227         return bytes.encode('utf-8')
    228 
    229     exitCode = None
    230     for i,(out,err) in enumerate(procData):
    231         res = procs[i].wait()
    232         # Detect Ctrl-C in subprocess.
    233         if res == -signal.SIGINT:
    234             raise KeyboardInterrupt
    235 
    236         # Ensure the resulting output is always of string type.
    237         try:
    238             out = to_string(out.decode('utf-8'))
    239         except:
    240             out = str(out)
    241         try:
    242             err = to_string(err.decode('utf-8'))
    243         except:
    244             err = str(err)
    245 
    246         results.append((cmd.commands[i], out, err, res))
    247         if cmd.pipe_err:
    248             # Python treats the exit code as a signed char.
    249             if exitCode is None:
    250                 exitCode = res
    251             elif res < 0:
    252                 exitCode = min(exitCode, res)
    253             else:
    254                 exitCode = max(exitCode, res)
    255         else:
    256             exitCode = res
    257 
    258     # Remove any named temporary files we created.
    259     for f in named_temp_files:
    260         try:
    261             os.remove(f)
    262         except OSError:
    263             pass
    264 
    265     if cmd.negate:
    266         exitCode = not exitCode
    267 
    268     return exitCode
    269 
    270 def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
    271     cmds = []
    272     for ln in commands:
    273         try:
    274             cmds.append(ShUtil.ShParser(ln, litConfig.isWindows,
    275                                         test.config.pipefail).parse())
    276         except:
    277             return lit.Test.Result(Test.FAIL, "shell parser error on: %r" % ln)
    278 
    279     cmd = cmds[0]
    280     for c in cmds[1:]:
    281         cmd = ShUtil.Seq(cmd, '&&', c)
    282 
    283     results = []
    284     try:
    285         shenv = ShellEnvironment(cwd, test.config.environment)
    286         exitCode = executeShCmd(cmd, shenv, results)
    287     except InternalShellError:
    288         e = sys.exc_info()[1]
    289         exitCode = 127
    290         results.append((e.command, '', e.message, exitCode))
    291 
    292     out = err = ''
    293     for i,(cmd, cmd_out,cmd_err,res) in enumerate(results):
    294         out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
    295         out += 'Command %d Result: %r\n' % (i, res)
    296         out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
    297         out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
    298 
    299     return out, err, exitCode
    300 
    301 def executeScript(test, litConfig, tmpBase, commands, cwd):
    302     bashPath = litConfig.getBashPath();
    303     isWin32CMDEXE = (litConfig.isWindows and not bashPath)
    304     script = tmpBase + '.script'
    305     if isWin32CMDEXE:
    306         script += '.bat'
    307 
    308     # Write script file
    309     mode = 'w'
    310     if litConfig.isWindows and not isWin32CMDEXE:
    311       mode += 'b'  # Avoid CRLFs when writing bash scripts.
    312     f = open(script, mode)
    313     if isWin32CMDEXE:
    314         f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
    315     else:
    316         if test.config.pipefail:
    317             f.write('set -o pipefail;')
    318         f.write('{ ' + '; } &&\n{ '.join(commands) + '; }')
    319     f.write('\n')
    320     f.close()
    321 
    322     if isWin32CMDEXE:
    323         command = ['cmd','/c', script]
    324     else:
    325         if bashPath:
    326             command = [bashPath, script]
    327         else:
    328             command = ['/bin/sh', script]
    329         if litConfig.useValgrind:
    330             # FIXME: Running valgrind on sh is overkill. We probably could just
    331             # run on clang with no real loss.
    332             command = litConfig.valgrindArgs + command
    333 
    334     return lit.util.executeCommand(command, cwd=cwd,
    335                                    env=test.config.environment)
    336 
    337 def parseIntegratedTestScriptCommands(source_path):
    338     """
    339     parseIntegratedTestScriptCommands(source_path) -> commands
    340 
    341     Parse the commands in an integrated test script file into a list of
    342     (line_number, command_type, line).
    343     """
    344 
    345     # This code is carefully written to be dual compatible with Python 2.5+ and
    346     # Python 3 without requiring input files to always have valid codings. The
    347     # trick we use is to open the file in binary mode and use the regular
    348     # expression library to find the commands, with it scanning strings in
    349     # Python2 and bytes in Python3.
    350     #
    351     # Once we find a match, we do require each script line to be decodable to
    352     # UTF-8, so we convert the outputs to UTF-8 before returning. This way the
    353     # remaining code can work with "strings" agnostic of the executing Python
    354     # version.
    355 
    356     keywords = ['RUN:', 'XFAIL:', 'REQUIRES:', 'UNSUPPORTED:', 'END.']
    357     keywords_re = re.compile(
    358         to_bytes("(%s)(.*)\n" % ("|".join(k for k in keywords),)))
    359 
    360     f = open(source_path, 'rb')
    361     try:
    362         # Read the entire file contents.
    363         data = f.read()
    364 
    365         # Ensure the data ends with a newline.
    366         if not data.endswith(to_bytes('\n')):
    367             data = data + to_bytes('\n')
    368 
    369         # Iterate over the matches.
    370         line_number = 1
    371         last_match_position = 0
    372         for match in keywords_re.finditer(data):
    373             # Compute the updated line number by counting the intervening
    374             # newlines.
    375             match_position = match.start()
    376             line_number += data.count(to_bytes('\n'), last_match_position,
    377                                       match_position)
    378             last_match_position = match_position
    379 
    380             # Convert the keyword and line to UTF-8 strings and yield the
    381             # command. Note that we take care to return regular strings in
    382             # Python 2, to avoid other code having to differentiate between the
    383             # str and unicode types.
    384             keyword,ln = match.groups()
    385             yield (line_number, to_string(keyword[:-1].decode('utf-8')),
    386                    to_string(ln.decode('utf-8')))
    387     finally:
    388         f.close()
    389 
    390 
    391 def parseIntegratedTestScript(test, normalize_slashes=False,
    392                               extra_substitutions=[], require_script=True):
    393     """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
    394     script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES'
    395     and 'UNSUPPORTED' information. The RUN lines also will have variable
    396     substitution performed. If 'require_script' is False an empty script may be
    397     returned. This can be used for test formats where the actual script is
    398     optional or ignored.
    399     """
    400 
    401     # Get the temporary location, this is always relative to the test suite
    402     # root, not test source root.
    403     #
    404     # FIXME: This should not be here?
    405     sourcepath = test.getSourcePath()
    406     sourcedir = os.path.dirname(sourcepath)
    407     execpath = test.getExecPath()
    408     execdir,execbase = os.path.split(execpath)
    409     tmpDir = os.path.join(execdir, 'Output')
    410     tmpBase = os.path.join(tmpDir, execbase)
    411 
    412     # Normalize slashes, if requested.
    413     if normalize_slashes:
    414         sourcepath = sourcepath.replace('\\', '/')
    415         sourcedir = sourcedir.replace('\\', '/')
    416         tmpDir = tmpDir.replace('\\', '/')
    417         tmpBase = tmpBase.replace('\\', '/')
    418 
    419     # We use #_MARKER_# to hide %% while we do the other substitutions.
    420     substitutions = list(extra_substitutions)
    421     substitutions.extend([('%%', '#_MARKER_#')])
    422     substitutions.extend(test.config.substitutions)
    423     substitutions.extend([('%s', sourcepath),
    424                           ('%S', sourcedir),
    425                           ('%p', sourcedir),
    426                           ('%{pathsep}', os.pathsep),
    427                           ('%t', tmpBase + '.tmp'),
    428                           ('%T', tmpDir),
    429                           ('#_MARKER_#', '%')])
    430 
    431     # "%/[STpst]" should be normalized.
    432     substitutions.extend([
    433             ('%/s', sourcepath.replace('\\', '/')),
    434             ('%/S', sourcedir.replace('\\', '/')),
    435             ('%/p', sourcedir.replace('\\', '/')),
    436             ('%/t', tmpBase.replace('\\', '/') + '.tmp'),
    437             ('%/T', tmpDir.replace('\\', '/')),
    438             ])
    439 
    440     # Collect the test lines from the script.
    441     script = []
    442     requires = []
    443     unsupported = []
    444     for line_number, command_type, ln in \
    445             parseIntegratedTestScriptCommands(sourcepath):
    446         if command_type == 'RUN':
    447             # Trim trailing whitespace.
    448             ln = ln.rstrip()
    449 
    450             # Substitute line number expressions
    451             ln = re.sub('%\(line\)', str(line_number), ln)
    452             def replace_line_number(match):
    453                 if match.group(1) == '+':
    454                     return str(line_number + int(match.group(2)))
    455                 if match.group(1) == '-':
    456                     return str(line_number - int(match.group(2)))
    457             ln = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, ln)
    458 
    459             # Collapse lines with trailing '\\'.
    460             if script and script[-1][-1] == '\\':
    461                 script[-1] = script[-1][:-1] + ln
    462             else:
    463                 script.append(ln)
    464         elif command_type == 'XFAIL':
    465             test.xfails.extend([s.strip() for s in ln.split(',')])
    466         elif command_type == 'REQUIRES':
    467             requires.extend([s.strip() for s in ln.split(',')])
    468         elif command_type == 'UNSUPPORTED':
    469             unsupported.extend([s.strip() for s in ln.split(',')])
    470         elif command_type == 'END':
    471             # END commands are only honored if the rest of the line is empty.
    472             if not ln.strip():
    473                 break
    474         else:
    475             raise ValueError("unknown script command type: %r" % (
    476                     command_type,))
    477 
    478     # Apply substitutions to the script.  Allow full regular
    479     # expression syntax.  Replace each matching occurrence of regular
    480     # expression pattern a with substitution b in line ln.
    481     def processLine(ln):
    482         # Apply substitutions
    483         for a,b in substitutions:
    484             if kIsWindows:
    485                 b = b.replace("\\","\\\\")
    486             ln = re.sub(a, b, ln)
    487 
    488         # Strip the trailing newline and any extra whitespace.
    489         return ln.strip()
    490     script = [processLine(ln)
    491               for ln in script]
    492 
    493     # Verify the script contains a run line.
    494     if require_script and not script:
    495         return lit.Test.Result(Test.UNRESOLVED, "Test has no run line!")
    496 
    497     # Check for unterminated run lines.
    498     if script and script[-1][-1] == '\\':
    499         return lit.Test.Result(Test.UNRESOLVED,
    500                                "Test has unterminated run lines (with '\\')")
    501 
    502     # Check that we have the required features:
    503     missing_required_features = [f for f in requires
    504                                  if f not in test.config.available_features]
    505     if missing_required_features:
    506         msg = ', '.join(missing_required_features)
    507         return lit.Test.Result(Test.UNSUPPORTED,
    508                                "Test requires the following features: %s" % msg)
    509     unsupported_features = [f for f in unsupported
    510                             if f in test.config.available_features]
    511     if unsupported_features:
    512         msg = ', '.join(unsupported_features)
    513         return lit.Test.Result(Test.UNSUPPORTED,
    514                     "Test is unsupported with the following features: %s" % msg)
    515 
    516     return script,tmpBase,execdir
    517 
    518 def _runShTest(test, litConfig, useExternalSh,
    519                    script, tmpBase, execdir):
    520     # Create the output directory if it does not already exist.
    521     lit.util.mkdir_p(os.path.dirname(tmpBase))
    522 
    523     if useExternalSh:
    524         res = executeScript(test, litConfig, tmpBase, script, execdir)
    525     else:
    526         res = executeScriptInternal(test, litConfig, tmpBase, script, execdir)
    527     if isinstance(res, lit.Test.Result):
    528         return res
    529 
    530     out,err,exitCode = res
    531     if exitCode == 0:
    532         status = Test.PASS
    533     else:
    534         status = Test.FAIL
    535 
    536     # Form the output log.
    537     output = """Script:\n--\n%s\n--\nExit Code: %d\n\n""" % (
    538         '\n'.join(script), exitCode)
    539 
    540     # Append the outputs, if present.
    541     if out:
    542         output += """Command Output (stdout):\n--\n%s\n--\n""" % (out,)
    543     if err:
    544         output += """Command Output (stderr):\n--\n%s\n--\n""" % (err,)
    545 
    546     return lit.Test.Result(status, output)
    547 
    548 
    549 def executeShTest(test, litConfig, useExternalSh,
    550                   extra_substitutions=[]):
    551     if test.config.unsupported:
    552         return (Test.UNSUPPORTED, 'Test is unsupported')
    553 
    554     res = parseIntegratedTestScript(test, useExternalSh, extra_substitutions)
    555     if isinstance(res, lit.Test.Result):
    556         return res
    557     if litConfig.noExecute:
    558         return lit.Test.Result(Test.PASS)
    559 
    560     script, tmpBase, execdir = res
    561     return _runShTest(test, litConfig, useExternalSh, script, tmpBase, execdir)
    562 
    563