• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from __future__ import absolute_import
2import os, signal, subprocess, sys
3import re
4import platform
5import tempfile
6import threading
7
8import lit.ShUtil as ShUtil
9import lit.Test as Test
10import lit.util
11from lit.util import to_bytes, to_string
12
13class InternalShellError(Exception):
14    def __init__(self, command, message):
15        self.command = command
16        self.message = message
17
18kIsWindows = platform.system() == 'Windows'
19
20# Don't use close_fds on Windows.
21kUseCloseFDs = not kIsWindows
22
23# Use temporary files to replace /dev/null on Windows.
24kAvoidDevNull = kIsWindows
25
26class 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
37class 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
113class 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
125def 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
143def _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
438def 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
505def 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
546def 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
598def 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
607def 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
658def 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
676class 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
692class 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
778def 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
880def _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
918def 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