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 import threading
      7 
      8 import lit.ShUtil as ShUtil
      9 import lit.Test as Test
     10 import lit.util
     11 from lit.util import to_bytes, to_string
     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 class ShellEnvironment(object):
     27 
     28     """Mutable shell environment containing things like CWD and env vars.
     29 
     30     Environment variables are not implemented, but cwd tracking is.
     31     """
     32 
     33     def __init__(self, cwd, env):
     34         self.cwd = cwd
     35         self.env = dict(env)
     36 
     37 class TimeoutHelper(object):
     38     """
     39         Object used to helper manage enforcing a timeout in
     40         _executeShCmd(). It is passed through recursive calls
     41         to collect processes that have been executed so that when
     42         the timeout happens they can be killed.
     43     """
     44     def __init__(self, timeout):
     45         self.timeout = timeout
     46         self._procs = []
     47         self._timeoutReached = False
     48         self._doneKillPass = False
     49         # This lock will be used to protect concurrent access
     50         # to _procs and _doneKillPass
     51         self._lock = None
     52         self._timer = None
     53 
     54     def cancel(self):
     55         if not self.active():
     56             return
     57         self._timer.cancel()
     58 
     59     def active(self):
     60         return self.timeout > 0
     61 
     62     def addProcess(self, proc):
     63         if not self.active():
     64             return
     65         needToRunKill = False
     66         with self._lock:
     67             self._procs.append(proc)
     68             # Avoid re-entering the lock by finding out if kill needs to be run
     69             # again here but call it if necessary once we have left the lock.
     70             # We could use a reentrant lock here instead but this code seems
     71             # clearer to me.
     72             needToRunKill = self._doneKillPass
     73 
     74         # The initial call to _kill() from the timer thread already happened so
     75         # we need to call it again from this thread, otherwise this process
     76         # will be left to run even though the timeout was already hit
     77         if needToRunKill:
     78             assert self.timeoutReached()
     79             self._kill()
     80 
     81     def startTimer(self):
     82         if not self.active():
     83             return
     84 
     85         # Do some late initialisation that's only needed
     86         # if there is a timeout set
     87         self._lock = threading.Lock()
     88         self._timer = threading.Timer(self.timeout, self._handleTimeoutReached)
     89         self._timer.start()
     90 
     91     def _handleTimeoutReached(self):
     92         self._timeoutReached = True
     93         self._kill()
     94 
     95     def timeoutReached(self):
     96         return self._timeoutReached
     97 
     98     def _kill(self):
     99         """
    100             This method may be called multiple times as we might get unlucky
    101             and be in the middle of creating a new process in _executeShCmd()
    102             which won't yet be in ``self._procs``. By locking here and in
    103             addProcess() we should be able to kill processes launched after
    104             the initial call to _kill()
    105         """
    106         with self._lock:
    107             for p in self._procs:
    108                 lit.util.killProcessAndChildren(p.pid)
    109             # Empty the list and note that we've done a pass over the list
    110             self._procs = [] # Python2 doesn't have list.clear()
    111             self._doneKillPass = True
    112 
    113 class ShellCommandResult(object):
    114     """Captures the result of an individual command."""
    115 
    116     def __init__(self, command, stdout, stderr, exitCode, timeoutReached,
    117                  outputFiles = []):
    118         self.command = command
    119         self.stdout = stdout
    120         self.stderr = stderr
    121         self.exitCode = exitCode
    122         self.timeoutReached = timeoutReached
    123         self.outputFiles = list(outputFiles)
    124                
    125 def executeShCmd(cmd, shenv, results, timeout=0):
    126     """
    127         Wrapper around _executeShCmd that handles
    128         timeout
    129     """
    130     # Use the helper even when no timeout is required to make
    131     # other code simpler (i.e. avoid bunch of ``!= None`` checks)
    132     timeoutHelper = TimeoutHelper(timeout)
    133     if timeout > 0:
    134         timeoutHelper.startTimer()
    135     finalExitCode = _executeShCmd(cmd, shenv, results, timeoutHelper)
    136     timeoutHelper.cancel()
    137     timeoutInfo = None
    138     if timeoutHelper.timeoutReached():
    139         timeoutInfo = 'Reached timeout of {} seconds'.format(timeout)
    140 
    141     return (finalExitCode, timeoutInfo)
    142 
    143 def _executeShCmd(cmd, shenv, results, timeoutHelper):
    144     if timeoutHelper.timeoutReached():
    145         # Prevent further recursion if the timeout has been hit
    146         # as we should try avoid launching more processes.
    147         return None
    148 
    149     if isinstance(cmd, ShUtil.Seq):
    150         if cmd.op == ';':
    151             res = _executeShCmd(cmd.lhs, shenv, results, timeoutHelper)
    152             return _executeShCmd(cmd.rhs, shenv, results, timeoutHelper)
    153 
    154         if cmd.op == '&':
    155             raise InternalShellError(cmd,"unsupported shell operator: '&'")
    156 
    157         if cmd.op == '||':
    158             res = _executeShCmd(cmd.lhs, shenv, results, timeoutHelper)
    159             if res != 0:
    160                 res = _executeShCmd(cmd.rhs, shenv, results, timeoutHelper)
    161             return res
    162 
    163         if cmd.op == '&&':
    164             res = _executeShCmd(cmd.lhs, shenv, results, timeoutHelper)
    165             if res is None:
    166                 return res
    167 
    168             if res == 0:
    169                 res = _executeShCmd(cmd.rhs, shenv, results, timeoutHelper)
    170             return res
    171 
    172         raise ValueError('Unknown shell command: %r' % cmd.op)
    173     assert isinstance(cmd, ShUtil.Pipeline)
    174 
    175     # Handle shell builtins first.
    176     if cmd.commands[0].args[0] == 'cd':
    177         if len(cmd.commands) != 1:
    178             raise ValueError("'cd' cannot be part of a pipeline")
    179         if len(cmd.commands[0].args) != 2:
    180             raise ValueError("'cd' supports only one argument")
    181         newdir = cmd.commands[0].args[1]
    182         # Update the cwd in the parent environment.
    183         if os.path.isabs(newdir):
    184             shenv.cwd = newdir
    185         else:
    186             shenv.cwd = os.path.join(shenv.cwd, newdir)
    187         # The cd builtin always succeeds. If the directory does not exist, the
    188         # following Popen calls will fail instead.
    189         return 0
    190 
    191     procs = []
    192     input = subprocess.PIPE
    193     stderrTempFiles = []
    194     opened_files = []
    195     named_temp_files = []
    196     # To avoid deadlock, we use a single stderr stream for piped
    197     # output. This is null until we have seen some output using
    198     # stderr.
    199     for i,j in enumerate(cmd.commands):
    200         # Reference the global environment by default.
    201         cmd_shenv = shenv
    202         if j.args[0] == 'env':
    203             # Create a copy of the global environment and modify it for this one
    204             # command. There might be multiple envs in a pipeline:
    205             #   env FOO=1 llc < %s | env BAR=2 llvm-mc | FileCheck %s
    206             cmd_shenv = ShellEnvironment(shenv.cwd, shenv.env)
    207             arg_idx = 1
    208             for arg_idx, arg in enumerate(j.args[1:]):
    209                 # Partition the string into KEY=VALUE.
    210                 key, eq, val = arg.partition('=')
    211                 # Stop if there was no equals.
    212                 if eq == '':
    213                     break
    214                 cmd_shenv.env[key] = val
    215             j.args = j.args[arg_idx+1:]
    216 
    217         # Apply the redirections, we use (N,) as a sentinel to indicate stdin,
    218         # stdout, stderr for N equal to 0, 1, or 2 respectively. Redirects to or
    219         # from a file are represented with a list [file, mode, file-object]
    220         # where file-object is initially None.
    221         redirects = [(0,), (1,), (2,)]
    222         for r in j.redirects:
    223             if r[0] == ('>',2):
    224                 redirects[2] = [r[1], 'w', None]
    225             elif r[0] == ('>>',2):
    226                 redirects[2] = [r[1], 'a', None]
    227             elif r[0] == ('>&',2) and r[1] in '012':
    228                 redirects[2] = redirects[int(r[1])]
    229             elif r[0] == ('>&',) or r[0] == ('&>',):
    230                 redirects[1] = redirects[2] = [r[1], 'w', None]
    231             elif r[0] == ('>',):
    232                 redirects[1] = [r[1], 'w', None]
    233             elif r[0] == ('>>',):
    234                 redirects[1] = [r[1], 'a', None]
    235             elif r[0] == ('<',):
    236                 redirects[0] = [r[1], 'r', None]
    237             else:
    238                 raise InternalShellError(j,"Unsupported redirect: %r" % (r,))
    239 
    240         # Map from the final redirections to something subprocess can handle.
    241         final_redirects = []
    242         for index,r in enumerate(redirects):
    243             if r == (0,):
    244                 result = input
    245             elif r == (1,):
    246                 if index == 0:
    247                     raise InternalShellError(j,"Unsupported redirect for stdin")
    248                 elif index == 1:
    249                     result = subprocess.PIPE
    250                 else:
    251                     result = subprocess.STDOUT
    252             elif r == (2,):
    253                 if index != 2:
    254                     raise InternalShellError(j,"Unsupported redirect on stdout")
    255                 result = subprocess.PIPE
    256             else:
    257                 if r[2] is None:
    258                     redir_filename = None
    259                     if kAvoidDevNull and r[0] == '/dev/null':
    260                         r[2] = tempfile.TemporaryFile(mode=r[1])
    261                     elif kIsWindows and r[0] == '/dev/tty':
    262                         # Simulate /dev/tty on Windows.
    263                         # "CON" is a special filename for the console.
    264                         r[2] = open("CON", r[1])
    265                     else:
    266                         # Make sure relative paths are relative to the cwd.
    267                         redir_filename = os.path.join(cmd_shenv.cwd, r[0])
    268                         r[2] = open(redir_filename, r[1])
    269                     # Workaround a Win32 and/or subprocess bug when appending.
    270                     #
    271                     # FIXME: Actually, this is probably an instance of PR6753.
    272                     if r[1] == 'a':
    273                         r[2].seek(0, 2)
    274                     opened_files.append(tuple(r) + (redir_filename,))
    275                 result = r[2]
    276             final_redirects.append(result)
    277 
    278         stdin, stdout, stderr = final_redirects
    279 
    280         # If stderr wants to come from stdout, but stdout isn't a pipe, then put
    281         # stderr on a pipe and treat it as stdout.
    282         if (stderr == subprocess.STDOUT and stdout != subprocess.PIPE):
    283             stderr = subprocess.PIPE
    284             stderrIsStdout = True
    285         else:
    286             stderrIsStdout = False
    287 
    288             # Don't allow stderr on a PIPE except for the last
    289             # process, this could deadlock.
    290             #
    291             # FIXME: This is slow, but so is deadlock.
    292             if stderr == subprocess.PIPE and j != cmd.commands[-1]:
    293                 stderr = tempfile.TemporaryFile(mode='w+b')
    294                 stderrTempFiles.append((i, stderr))
    295 
    296         # Resolve the executable path ourselves.
    297         args = list(j.args)
    298         executable = None
    299         # For paths relative to cwd, use the cwd of the shell environment.
    300         if args[0].startswith('.'):
    301             exe_in_cwd = os.path.join(cmd_shenv.cwd, args[0])
    302             if os.path.isfile(exe_in_cwd):
    303                 executable = exe_in_cwd
    304         if not executable:
    305             executable = lit.util.which(args[0], cmd_shenv.env['PATH'])
    306         if not executable:
    307             raise InternalShellError(j, '%r: command not found' % j.args[0])
    308 
    309         # Replace uses of /dev/null with temporary files.
    310         if kAvoidDevNull:
    311             for i,arg in enumerate(args):
    312                 if arg == "/dev/null":
    313                     f = tempfile.NamedTemporaryFile(delete=False)
    314                     f.close()
    315                     named_temp_files.append(f.name)
    316                     args[i] = f.name
    317 
    318         try:
    319             procs.append(subprocess.Popen(args, cwd=cmd_shenv.cwd,
    320                                           executable = executable,
    321                                           stdin = stdin,
    322                                           stdout = stdout,
    323                                           stderr = stderr,
    324                                           env = cmd_shenv.env,
    325                                           close_fds = kUseCloseFDs))
    326             # Let the helper know about this process
    327             timeoutHelper.addProcess(procs[-1])
    328         except OSError as e:
    329             raise InternalShellError(j, 'Could not create process ({}) due to {}'.format(executable, e))
    330 
    331         # Immediately close stdin for any process taking stdin from us.
    332         if stdin == subprocess.PIPE:
    333             procs[-1].stdin.close()
    334             procs[-1].stdin = None
    335 
    336         # Update the current stdin source.
    337         if stdout == subprocess.PIPE:
    338             input = procs[-1].stdout
    339         elif stderrIsStdout:
    340             input = procs[-1].stderr
    341         else:
    342             input = subprocess.PIPE
    343 
    344     # Explicitly close any redirected files. We need to do this now because we
    345     # need to release any handles we may have on the temporary files (important
    346     # on Win32, for example). Since we have already spawned the subprocess, our
    347     # handles have already been transferred so we do not need them anymore.
    348     for (name, mode, f, path) in opened_files:
    349         f.close()
    350 
    351     # FIXME: There is probably still deadlock potential here. Yawn.
    352     procData = [None] * len(procs)
    353     procData[-1] = procs[-1].communicate()
    354 
    355     for i in range(len(procs) - 1):
    356         if procs[i].stdout is not None:
    357             out = procs[i].stdout.read()
    358         else:
    359             out = ''
    360         if procs[i].stderr is not None:
    361             err = procs[i].stderr.read()
    362         else:
    363             err = ''
    364         procData[i] = (out,err)
    365 
    366     # Read stderr out of the temp files.
    367     for i,f in stderrTempFiles:
    368         f.seek(0, 0)
    369         procData[i] = (procData[i][0], f.read())
    370 
    371     def to_string(bytes):
    372         if isinstance(bytes, str):
    373             return bytes
    374         return bytes.encode('utf-8')
    375 
    376     exitCode = None
    377     for i,(out,err) in enumerate(procData):
    378         res = procs[i].wait()
    379         # Detect Ctrl-C in subprocess.
    380         if res == -signal.SIGINT:
    381             raise KeyboardInterrupt
    382 
    383         # Ensure the resulting output is always of string type.
    384         try:
    385             if out is None:
    386                 out = ''
    387             else:
    388                 out = to_string(out.decode('utf-8', errors='replace'))
    389         except:
    390             out = str(out)
    391         try:
    392             if err is None:
    393                 err = ''
    394             else:
    395                 err = to_string(err.decode('utf-8', errors='replace'))
    396         except:
    397             err = str(err)
    398 
    399         # Gather the redirected output files for failed commands.
    400         output_files = []
    401         if res != 0:
    402             for (name, mode, f, path) in sorted(opened_files):
    403                 if path is not None and mode in ('w', 'a'):
    404                     try:
    405                         with open(path, 'rb') as f:
    406                             data = f.read()
    407                     except:
    408                         data = None
    409                     if data != None:
    410                         output_files.append((name, path, data))
    411             
    412         results.append(ShellCommandResult(
    413             cmd.commands[i], out, err, res, timeoutHelper.timeoutReached(),
    414             output_files))
    415         if cmd.pipe_err:
    416             # Python treats the exit code as a signed char.
    417             if exitCode is None:
    418                 exitCode = res
    419             elif res < 0:
    420                 exitCode = min(exitCode, res)
    421             else:
    422                 exitCode = max(exitCode, res)
    423         else:
    424             exitCode = res
    425 
    426     # Remove any named temporary files we created.
    427     for f in named_temp_files:
    428         try:
    429             os.remove(f)
    430         except OSError:
    431             pass
    432 
    433     if cmd.negate:
    434         exitCode = not exitCode
    435 
    436     return exitCode
    437 
    438 def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
    439     cmds = []
    440     for ln in commands:
    441         try:
    442             cmds.append(ShUtil.ShParser(ln, litConfig.isWindows,
    443                                         test.config.pipefail).parse())
    444         except:
    445             return lit.Test.Result(Test.FAIL, "shell parser error on: %r" % ln)
    446 
    447     cmd = cmds[0]
    448     for c in cmds[1:]:
    449         cmd = ShUtil.Seq(cmd, '&&', c)
    450 
    451     results = []
    452     timeoutInfo = None
    453     try:
    454         shenv = ShellEnvironment(cwd, test.config.environment)
    455         exitCode, timeoutInfo = executeShCmd(cmd, shenv, results, timeout=litConfig.maxIndividualTestTime)
    456     except InternalShellError:
    457         e = sys.exc_info()[1]
    458         exitCode = 127
    459         results.append(
    460             ShellCommandResult(e.command, '', e.message, exitCode, False))
    461 
    462     out = err = ''
    463     for i,result in enumerate(results):
    464         # Write the command line run.
    465         out += '$ %s\n' % (' '.join('"%s"' % s
    466                                     for s in result.command.args),)
    467 
    468         # If nothing interesting happened, move on.
    469         if litConfig.maxIndividualTestTime == 0 and \
    470                result.exitCode == 0 and \
    471                not result.stdout.strip() and not result.stderr.strip():
    472             continue
    473 
    474         # Otherwise, something failed or was printed, show it.
    475 
    476         # Add the command output, if redirected.
    477         for (name, path, data) in result.outputFiles:
    478             if data.strip():
    479                 out += "# redirected output from %r:\n" % (name,)
    480                 data = to_string(data.decode('utf-8', errors='replace'))
    481                 if len(data) > 1024:
    482                     out += data[:1024] + "\n...\n"
    483                     out += "note: data was truncated\n"
    484                 else:
    485                     out += data
    486                 out += "\n"
    487                     
    488         if result.stdout.strip():
    489             out += '# command output:\n%s\n' % (result.stdout,)
    490         if result.stderr.strip():
    491             out += '# command stderr:\n%s\n' % (result.stderr,)
    492         if not result.stdout.strip() and not result.stderr.strip():
    493             out += "note: command had no output on stdout or stderr\n"
    494 
    495         # Show the error conditions:
    496         if result.exitCode != 0:
    497             out += "error: command failed with exit status: %d\n" % (
    498                 result.exitCode,)
    499         if litConfig.maxIndividualTestTime > 0:
    500             out += 'error: command reached timeout: %s\n' % (
    501                 i, str(result.timeoutReached))
    502 
    503     return out, err, exitCode, timeoutInfo
    504 
    505 def executeScript(test, litConfig, tmpBase, commands, cwd):
    506     bashPath = litConfig.getBashPath();
    507     isWin32CMDEXE = (litConfig.isWindows and not bashPath)
    508     script = tmpBase + '.script'
    509     if isWin32CMDEXE:
    510         script += '.bat'
    511 
    512     # Write script file
    513     mode = 'w'
    514     if litConfig.isWindows and not isWin32CMDEXE:
    515       mode += 'b'  # Avoid CRLFs when writing bash scripts.
    516     f = open(script, mode)
    517     if isWin32CMDEXE:
    518         f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
    519     else:
    520         if test.config.pipefail:
    521             f.write('set -o pipefail;')
    522         f.write('{ ' + '; } &&\n{ '.join(commands) + '; }')
    523     f.write('\n')
    524     f.close()
    525 
    526     if isWin32CMDEXE:
    527         command = ['cmd','/c', script]
    528     else:
    529         if bashPath:
    530             command = [bashPath, script]
    531         else:
    532             command = ['/bin/sh', script]
    533         if litConfig.useValgrind:
    534             # FIXME: Running valgrind on sh is overkill. We probably could just
    535             # run on clang with no real loss.
    536             command = litConfig.valgrindArgs + command
    537 
    538     try:
    539         out, err, exitCode = lit.util.executeCommand(command, cwd=cwd,
    540                                        env=test.config.environment,
    541                                        timeout=litConfig.maxIndividualTestTime)
    542         return (out, err, exitCode, None)
    543     except lit.util.ExecuteCommandTimeoutException as e:
    544         return (e.out, e.err, e.exitCode, e.msg)
    545 
    546 def parseIntegratedTestScriptCommands(source_path, keywords):
    547     """
    548     parseIntegratedTestScriptCommands(source_path) -> commands
    549 
    550     Parse the commands in an integrated test script file into a list of
    551     (line_number, command_type, line).
    552     """
    553 
    554     # This code is carefully written to be dual compatible with Python 2.5+ and
    555     # Python 3 without requiring input files to always have valid codings. The
    556     # trick we use is to open the file in binary mode and use the regular
    557     # expression library to find the commands, with it scanning strings in
    558     # Python2 and bytes in Python3.
    559     #
    560     # Once we find a match, we do require each script line to be decodable to
    561     # UTF-8, so we convert the outputs to UTF-8 before returning. This way the
    562     # remaining code can work with "strings" agnostic of the executing Python
    563     # version.
    564 
    565     keywords_re = re.compile(
    566         to_bytes("(%s)(.*)\n" % ("|".join(re.escape(k) for k in keywords),)))
    567 
    568     f = open(source_path, 'rb')
    569     try:
    570         # Read the entire file contents.
    571         data = f.read()
    572 
    573         # Ensure the data ends with a newline.
    574         if not data.endswith(to_bytes('\n')):
    575             data = data + to_bytes('\n')
    576 
    577         # Iterate over the matches.
    578         line_number = 1
    579         last_match_position = 0
    580         for match in keywords_re.finditer(data):
    581             # Compute the updated line number by counting the intervening
    582             # newlines.
    583             match_position = match.start()
    584             line_number += data.count(to_bytes('\n'), last_match_position,
    585                                       match_position)
    586             last_match_position = match_position
    587 
    588             # Convert the keyword and line to UTF-8 strings and yield the
    589             # command. Note that we take care to return regular strings in
    590             # Python 2, to avoid other code having to differentiate between the
    591             # str and unicode types.
    592             keyword,ln = match.groups()
    593             yield (line_number, to_string(keyword.decode('utf-8')),
    594                    to_string(ln.decode('utf-8')))
    595     finally:
    596         f.close()
    597 
    598 def getTempPaths(test):
    599     """Get the temporary location, this is always relative to the test suite
    600     root, not test source root."""
    601     execpath = test.getExecPath()
    602     execdir,execbase = os.path.split(execpath)
    603     tmpDir = os.path.join(execdir, 'Output')
    604     tmpBase = os.path.join(tmpDir, execbase)
    605     return tmpDir, tmpBase
    606 
    607 def getDefaultSubstitutions(test, tmpDir, tmpBase, normalize_slashes=False):
    608     sourcepath = test.getSourcePath()
    609     sourcedir = os.path.dirname(sourcepath)
    610 
    611     # Normalize slashes, if requested.
    612     if normalize_slashes:
    613         sourcepath = sourcepath.replace('\\', '/')
    614         sourcedir = sourcedir.replace('\\', '/')
    615         tmpDir = tmpDir.replace('\\', '/')
    616         tmpBase = tmpBase.replace('\\', '/')
    617 
    618     # We use #_MARKER_# to hide %% while we do the other substitutions.
    619     substitutions = []
    620     substitutions.extend([('%%', '#_MARKER_#')])
    621     substitutions.extend(test.config.substitutions)
    622     substitutions.extend([('%s', sourcepath),
    623                           ('%S', sourcedir),
    624                           ('%p', sourcedir),
    625                           ('%{pathsep}', os.pathsep),
    626                           ('%t', tmpBase + '.tmp'),
    627                           ('%T', tmpDir),
    628                           ('#_MARKER_#', '%')])
    629 
    630     # "%/[STpst]" should be normalized.
    631     substitutions.extend([
    632             ('%/s', sourcepath.replace('\\', '/')),
    633             ('%/S', sourcedir.replace('\\', '/')),
    634             ('%/p', sourcedir.replace('\\', '/')),
    635             ('%/t', tmpBase.replace('\\', '/') + '.tmp'),
    636             ('%/T', tmpDir.replace('\\', '/')),
    637             ])
    638 
    639     # "%:[STpst]" are paths without colons.
    640     if kIsWindows:
    641         substitutions.extend([
    642                 ('%:s', re.sub(r'^(.):', r'\1', sourcepath)),
    643                 ('%:S', re.sub(r'^(.):', r'\1', sourcedir)),
    644                 ('%:p', re.sub(r'^(.):', r'\1', sourcedir)),
    645                 ('%:t', re.sub(r'^(.):', r'\1', tmpBase) + '.tmp'),
    646                 ('%:T', re.sub(r'^(.):', r'\1', tmpDir)),
    647                 ])
    648     else:
    649         substitutions.extend([
    650                 ('%:s', sourcepath),
    651                 ('%:S', sourcedir),
    652                 ('%:p', sourcedir),
    653                 ('%:t', tmpBase + '.tmp'),
    654                 ('%:T', tmpDir),
    655                 ])
    656     return substitutions
    657 
    658 def applySubstitutions(script, substitutions):
    659     """Apply substitutions to the script.  Allow full regular expression syntax.
    660     Replace each matching occurrence of regular expression pattern a with
    661     substitution b in line ln."""
    662     def processLine(ln):
    663         # Apply substitutions
    664         for a,b in substitutions:
    665             if kIsWindows:
    666                 b = b.replace("\\","\\\\")
    667             ln = re.sub(a, b, ln)
    668 
    669         # Strip the trailing newline and any extra whitespace.
    670         return ln.strip()
    671     # Note Python 3 map() gives an iterator rather than a list so explicitly
    672     # convert to list before returning.
    673     return list(map(processLine, script))
    674 
    675 
    676 class ParserKind(object):
    677     """
    678     An enumeration representing the style of an integrated test keyword or
    679     command.
    680 
    681     TAG: A keyword taking no value. Ex 'END.'
    682     COMMAND: A Keyword taking a list of shell commands. Ex 'RUN:'
    683     LIST: A keyword taking a comma separated list of value. Ex 'XFAIL:'
    684     CUSTOM: A keyword with custom parsing semantics.
    685     """
    686     TAG = 0
    687     COMMAND = 1
    688     LIST = 2
    689     CUSTOM = 3
    690 
    691 
    692 class IntegratedTestKeywordParser(object):
    693     """A parser for LLVM/Clang style integrated test scripts.
    694 
    695     keyword: The keyword to parse for. It must end in either '.' or ':'.
    696     kind: An value of ParserKind.
    697     parser: A custom parser. This value may only be specified with
    698             ParserKind.CUSTOM.
    699     """
    700     def __init__(self, keyword, kind, parser=None, initial_value=None):
    701         if not keyword.endswith('.') and not keyword.endswith(':'):
    702             raise ValueError("keyword '%s' must end with either '.' or ':' "
    703                              % keyword)
    704         if keyword.endswith('.') and kind in \
    705                 [ParserKind.LIST, ParserKind.COMMAND]:
    706             raise ValueError("Keyword '%s' should end in ':'" % keyword)
    707 
    708         elif keyword.endswith(':') and kind in [ParserKind.TAG]:
    709             raise ValueError("Keyword '%s' should end in '.'" % keyword)
    710         if parser is not None and kind != ParserKind.CUSTOM:
    711             raise ValueError("custom parsers can only be specified with "
    712                              "ParserKind.CUSTOM")
    713         self.keyword = keyword
    714         self.kind = kind
    715         self.parsed_lines = []
    716         self.value = initial_value
    717         self.parser = parser
    718 
    719         if kind == ParserKind.COMMAND:
    720             self.parser = self._handleCommand
    721         elif kind == ParserKind.LIST:
    722             self.parser = self._handleList
    723         elif kind == ParserKind.TAG:
    724             if not keyword.endswith('.'):
    725                 raise ValueError("keyword '%s' should end with '.'" % keyword)
    726             self.parser = self._handleTag
    727         elif kind == ParserKind.CUSTOM:
    728             if parser is None:
    729                 raise ValueError("ParserKind.CUSTOM requires a custom parser")
    730             self.parser = parser
    731         else:
    732             raise ValueError("Unknown kind '%s'" % kind)
    733 
    734     def parseLine(self, line_number, line):
    735         self.parsed_lines += [(line_number, line)]
    736         self.value = self.parser(line_number, line, self.value)
    737 
    738     def getValue(self):
    739         return self.value
    740 
    741     @staticmethod
    742     def _handleTag(line_number, line, output):
    743         """A helper for parsing TAG type keywords"""
    744         return (not line.strip() or output)
    745 
    746     @staticmethod
    747     def _handleCommand(line_number, line, output):
    748         """A helper for parsing COMMAND type keywords"""
    749         # Trim trailing whitespace.
    750         line = line.rstrip()
    751         # Substitute line number expressions
    752         line = re.sub('%\(line\)', str(line_number), line)
    753 
    754         def replace_line_number(match):
    755             if match.group(1) == '+':
    756                 return str(line_number + int(match.group(2)))
    757             if match.group(1) == '-':
    758                 return str(line_number - int(match.group(2)))
    759         line = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, line)
    760         # Collapse lines with trailing '\\'.
    761         if output and output[-1][-1] == '\\':
    762             output[-1] = output[-1][:-1] + line
    763         else:
    764             if output is None:
    765                 output = []
    766             output.append(line)
    767         return output
    768 
    769     @staticmethod
    770     def _handleList(line_number, line, output):
    771         """A parser for LIST type keywords"""
    772         if output is None:
    773             output = []
    774         output.extend([s.strip() for s in line.split(',')])
    775         return output
    776 
    777 
    778 def parseIntegratedTestScript(test, additional_parsers=[],
    779                               require_script=True):
    780     """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
    781     script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES'
    782     'REQUIRES-ANY' and 'UNSUPPORTED' information.
    783 
    784     If additional parsers are specified then the test is also scanned for the
    785     keywords they specify and all matches are passed to the custom parser.
    786 
    787     If 'require_script' is False an empty script
    788     may be returned. This can be used for test formats where the actual script
    789     is optional or ignored.
    790     """
    791     # Collect the test lines from the script.
    792     sourcepath = test.getSourcePath()
    793     script = []
    794     requires = []
    795     requires_any = []
    796     unsupported = []
    797     builtin_parsers = [
    798         IntegratedTestKeywordParser('RUN:', ParserKind.COMMAND,
    799                                     initial_value=script),
    800         IntegratedTestKeywordParser('XFAIL:', ParserKind.LIST,
    801                                     initial_value=test.xfails),
    802         IntegratedTestKeywordParser('REQUIRES:', ParserKind.LIST,
    803                                     initial_value=requires),
    804         IntegratedTestKeywordParser('REQUIRES-ANY:', ParserKind.LIST,
    805                                     initial_value=requires_any),
    806         IntegratedTestKeywordParser('UNSUPPORTED:', ParserKind.LIST,
    807                                     initial_value=unsupported),
    808         IntegratedTestKeywordParser('END.', ParserKind.TAG)
    809     ]
    810     keyword_parsers = {p.keyword: p for p in builtin_parsers}
    811     for parser in additional_parsers:
    812         if not isinstance(parser, IntegratedTestKeywordParser):
    813             raise ValueError('additional parser must be an instance of '
    814                              'IntegratedTestKeywordParser')
    815         if parser.keyword in keyword_parsers:
    816             raise ValueError("Parser for keyword '%s' already exists"
    817                              % parser.keyword)
    818         keyword_parsers[parser.keyword] = parser
    819 
    820     for line_number, command_type, ln in \
    821             parseIntegratedTestScriptCommands(sourcepath,
    822                                               keyword_parsers.keys()):
    823         parser = keyword_parsers[command_type]
    824         parser.parseLine(line_number, ln)
    825         if command_type == 'END.' and parser.getValue() is True:
    826             break
    827 
    828     # Verify the script contains a run line.
    829     if require_script and not script:
    830         return lit.Test.Result(Test.UNRESOLVED, "Test has no run line!")
    831 
    832     # Check for unterminated run lines.
    833     if script and script[-1][-1] == '\\':
    834         return lit.Test.Result(Test.UNRESOLVED,
    835                                "Test has unterminated run lines (with '\\')")
    836 
    837     # Check that we have the required features:
    838     missing_required_features = [f for f in requires
    839                                  if f not in test.config.available_features]
    840     if missing_required_features:
    841         msg = ', '.join(missing_required_features)
    842         return lit.Test.Result(Test.UNSUPPORTED,
    843                                "Test requires the following features: %s"
    844                                % msg)
    845     requires_any_features = [f for f in requires_any
    846                              if f in test.config.available_features]
    847     if requires_any and not requires_any_features:
    848         msg = ' ,'.join(requires_any)
    849         return lit.Test.Result(Test.UNSUPPORTED,
    850                                "Test requires any of the following features: "
    851                                "%s" % msg)
    852     unsupported_features = [f for f in unsupported
    853                             if f in test.config.available_features]
    854     if unsupported_features:
    855         msg = ', '.join(unsupported_features)
    856         return lit.Test.Result(
    857             Test.UNSUPPORTED,
    858             "Test is unsupported with the following features: %s" % msg)
    859 
    860     unsupported_targets = [f for f in unsupported
    861                            if f in test.suite.config.target_triple]
    862     if unsupported_targets:
    863         return lit.Test.Result(
    864             Test.UNSUPPORTED,
    865             "Test is unsupported with the following triple: %s" % (
    866              test.suite.config.target_triple,))
    867 
    868     if test.config.limit_to_features:
    869         # Check that we have one of the limit_to_features features in requires.
    870         limit_to_features_tests = [f for f in test.config.limit_to_features
    871                                    if f in requires]
    872         if not limit_to_features_tests:
    873             msg = ', '.join(test.config.limit_to_features)
    874             return lit.Test.Result(
    875                 Test.UNSUPPORTED,
    876                 "Test requires one of the limit_to_features features %s" % msg)
    877     return script
    878 
    879 
    880 def _runShTest(test, litConfig, useExternalSh, script, tmpBase):
    881     # Create the output directory if it does not already exist.
    882     lit.util.mkdir_p(os.path.dirname(tmpBase))
    883 
    884     execdir = os.path.dirname(test.getExecPath())
    885     if useExternalSh:
    886         res = executeScript(test, litConfig, tmpBase, script, execdir)
    887     else:
    888         res = executeScriptInternal(test, litConfig, tmpBase, script, execdir)
    889     if isinstance(res, lit.Test.Result):
    890         return res
    891 
    892     out,err,exitCode,timeoutInfo = res
    893     if exitCode == 0:
    894         status = Test.PASS
    895     else:
    896         if timeoutInfo == None:
    897             status = Test.FAIL
    898         else:
    899             status = Test.TIMEOUT
    900 
    901     # Form the output log.
    902     output = """Script:\n--\n%s\n--\nExit Code: %d\n""" % (
    903         '\n'.join(script), exitCode)
    904 
    905     if timeoutInfo != None:
    906         output += """Timeout: %s\n""" % (timeoutInfo,)
    907     output += "\n"
    908 
    909     # Append the outputs, if present.
    910     if out:
    911         output += """Command Output (stdout):\n--\n%s\n--\n""" % (out,)
    912     if err:
    913         output += """Command Output (stderr):\n--\n%s\n--\n""" % (err,)
    914 
    915     return lit.Test.Result(status, output)
    916 
    917 
    918 def executeShTest(test, litConfig, useExternalSh,
    919                   extra_substitutions=[]):
    920     if test.config.unsupported:
    921         return (Test.UNSUPPORTED, 'Test is unsupported')
    922 
    923     script = parseIntegratedTestScript(test)
    924     if isinstance(script, lit.Test.Result):
    925         return script
    926     if litConfig.noExecute:
    927         return lit.Test.Result(Test.PASS)
    928 
    929     tmpDir, tmpBase = getTempPaths(test)
    930     substitutions = list(extra_substitutions)
    931     substitutions += getDefaultSubstitutions(test, tmpDir, tmpBase,
    932                                              normalize_slashes=useExternalSh)
    933     script = applySubstitutions(script, substitutions)
    934 
    935     # Re-run failed tests up to test_retry_attempts times.
    936     attempts = 1
    937     if hasattr(test.config, 'test_retry_attempts'):
    938         attempts += test.config.test_retry_attempts
    939     for i in range(attempts):
    940         res = _runShTest(test, litConfig, useExternalSh, script, tmpBase)
    941         if res.code != Test.FAIL:
    942             break
    943     # If we had to run the test more than once, count it as a flaky pass. These
    944     # will be printed separately in the test summary.
    945     if i > 0 and res.code == Test.PASS:
    946         res.code = Test.FLAKYPASS
    947     return res
    948