TestRunner.py revision b35a1733607b07f36c3617932b67debb375a15c5
1from __future__ import absolute_import
2import os, signal, subprocess, sys
3import StringIO
4
5import platform
6import tempfile
7
8import re
9import lit.ShUtil as ShUtil
10import lit.Test as Test
11import lit.Util as Util
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
26def executeCommand(command, cwd=None, env=None):
27    # Close extra file handles on UNIX (on Windows this cannot be done while
28    # also redirecting input).
29    close_fds = not kIsWindows
30
31    p = subprocess.Popen(command, cwd=cwd,
32                         stdin=subprocess.PIPE,
33                         stdout=subprocess.PIPE,
34                         stderr=subprocess.PIPE,
35                         env=env, close_fds=close_fds)
36    out,err = p.communicate()
37    exitCode = p.wait()
38
39    # Detect Ctrl-C in subprocess.
40    if exitCode == -signal.SIGINT:
41        raise KeyboardInterrupt
42
43    return out, err, exitCode
44
45def executeShCmd(cmd, cfg, cwd, results):
46    if isinstance(cmd, ShUtil.Seq):
47        if cmd.op == ';':
48            res = executeShCmd(cmd.lhs, cfg, cwd, results)
49            return executeShCmd(cmd.rhs, cfg, cwd, results)
50
51        if cmd.op == '&':
52            raise InternalShellError(cmd,"unsupported shell operator: '&'")
53
54        if cmd.op == '||':
55            res = executeShCmd(cmd.lhs, cfg, cwd, results)
56            if res != 0:
57                res = executeShCmd(cmd.rhs, cfg, cwd, results)
58            return res
59
60        if cmd.op == '&&':
61            res = executeShCmd(cmd.lhs, cfg, cwd, results)
62            if res is None:
63                return res
64
65            if res == 0:
66                res = executeShCmd(cmd.rhs, cfg, cwd, results)
67            return res
68
69        raise ValueError('Unknown shell command: %r' % cmd.op)
70
71    assert isinstance(cmd, ShUtil.Pipeline)
72    procs = []
73    input = subprocess.PIPE
74    stderrTempFiles = []
75    opened_files = []
76    named_temp_files = []
77    # To avoid deadlock, we use a single stderr stream for piped
78    # output. This is null until we have seen some output using
79    # stderr.
80    for i,j in enumerate(cmd.commands):
81        # Apply the redirections, we use (N,) as a sentinel to indicate stdin,
82        # stdout, stderr for N equal to 0, 1, or 2 respectively. Redirects to or
83        # from a file are represented with a list [file, mode, file-object]
84        # where file-object is initially None.
85        redirects = [(0,), (1,), (2,)]
86        for r in j.redirects:
87            if r[0] == ('>',2):
88                redirects[2] = [r[1], 'w', None]
89            elif r[0] == ('>>',2):
90                redirects[2] = [r[1], 'a', None]
91            elif r[0] == ('>&',2) and r[1] in '012':
92                redirects[2] = redirects[int(r[1])]
93            elif r[0] == ('>&',) or r[0] == ('&>',):
94                redirects[1] = redirects[2] = [r[1], 'w', None]
95            elif r[0] == ('>',):
96                redirects[1] = [r[1], 'w', None]
97            elif r[0] == ('>>',):
98                redirects[1] = [r[1], 'a', None]
99            elif r[0] == ('<',):
100                redirects[0] = [r[1], 'r', None]
101            else:
102                raise InternalShellError(j,"Unsupported redirect: %r" % (r,))
103
104        # Map from the final redirections to something subprocess can handle.
105        final_redirects = []
106        for index,r in enumerate(redirects):
107            if r == (0,):
108                result = input
109            elif r == (1,):
110                if index == 0:
111                    raise InternalShellError(j,"Unsupported redirect for stdin")
112                elif index == 1:
113                    result = subprocess.PIPE
114                else:
115                    result = subprocess.STDOUT
116            elif r == (2,):
117                if index != 2:
118                    raise InternalShellError(j,"Unsupported redirect on stdout")
119                result = subprocess.PIPE
120            else:
121                if r[2] is None:
122                    if kAvoidDevNull and r[0] == '/dev/null':
123                        r[2] = tempfile.TemporaryFile(mode=r[1])
124                    else:
125                        r[2] = open(r[0], r[1])
126                    # Workaround a Win32 and/or subprocess bug when appending.
127                    #
128                    # FIXME: Actually, this is probably an instance of PR6753.
129                    if r[1] == 'a':
130                        r[2].seek(0, 2)
131                    opened_files.append(r[2])
132                result = r[2]
133            final_redirects.append(result)
134
135        stdin, stdout, stderr = final_redirects
136
137        # If stderr wants to come from stdout, but stdout isn't a pipe, then put
138        # stderr on a pipe and treat it as stdout.
139        if (stderr == subprocess.STDOUT and stdout != subprocess.PIPE):
140            stderr = subprocess.PIPE
141            stderrIsStdout = True
142        else:
143            stderrIsStdout = False
144
145            # Don't allow stderr on a PIPE except for the last
146            # process, this could deadlock.
147            #
148            # FIXME: This is slow, but so is deadlock.
149            if stderr == subprocess.PIPE and j != cmd.commands[-1]:
150                stderr = tempfile.TemporaryFile(mode='w+b')
151                stderrTempFiles.append((i, stderr))
152
153        # Resolve the executable path ourselves.
154        args = list(j.args)
155        args[0] = Util.which(args[0], cfg.environment['PATH'])
156        if not args[0]:
157            raise InternalShellError(j, '%r: command not found' % j.args[0])
158
159        # Replace uses of /dev/null with temporary files.
160        if kAvoidDevNull:
161            for i,arg in enumerate(args):
162                if arg == "/dev/null":
163                    f = tempfile.NamedTemporaryFile(delete=False)
164                    f.close()
165                    named_temp_files.append(f.name)
166                    args[i] = f.name
167
168        procs.append(subprocess.Popen(args, cwd=cwd,
169                                      stdin = stdin,
170                                      stdout = stdout,
171                                      stderr = stderr,
172                                      env = cfg.environment,
173                                      close_fds = kUseCloseFDs))
174
175        # Immediately close stdin for any process taking stdin from us.
176        if stdin == subprocess.PIPE:
177            procs[-1].stdin.close()
178            procs[-1].stdin = None
179
180        # Update the current stdin source.
181        if stdout == subprocess.PIPE:
182            input = procs[-1].stdout
183        elif stderrIsStdout:
184            input = procs[-1].stderr
185        else:
186            input = subprocess.PIPE
187
188    # Explicitly close any redirected files. We need to do this now because we
189    # need to release any handles we may have on the temporary files (important
190    # on Win32, for example). Since we have already spawned the subprocess, our
191    # handles have already been transferred so we do not need them anymore.
192    for f in opened_files:
193        f.close()
194
195    # FIXME: There is probably still deadlock potential here. Yawn.
196    procData = [None] * len(procs)
197    procData[-1] = procs[-1].communicate()
198
199    for i in range(len(procs) - 1):
200        if procs[i].stdout is not None:
201            out = procs[i].stdout.read()
202        else:
203            out = ''
204        if procs[i].stderr is not None:
205            err = procs[i].stderr.read()
206        else:
207            err = ''
208        procData[i] = (out,err)
209
210    # Read stderr out of the temp files.
211    for i,f in stderrTempFiles:
212        f.seek(0, 0)
213        procData[i] = (procData[i][0], f.read())
214
215    exitCode = None
216    for i,(out,err) in enumerate(procData):
217        res = procs[i].wait()
218        # Detect Ctrl-C in subprocess.
219        if res == -signal.SIGINT:
220            raise KeyboardInterrupt
221
222        results.append((cmd.commands[i], out, err, res))
223        if cmd.pipe_err:
224            # Python treats the exit code as a signed char.
225            if res < 0:
226                exitCode = min(exitCode, res)
227            else:
228                exitCode = max(exitCode, res)
229        else:
230            exitCode = res
231
232    # Remove any named temporary files we created.
233    for f in named_temp_files:
234        try:
235            os.remove(f)
236        except OSError:
237            pass
238
239    if cmd.negate:
240        exitCode = not exitCode
241
242    return exitCode
243
244def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
245    cmds = []
246    for ln in commands:
247        try:
248            cmds.append(ShUtil.ShParser(ln, litConfig.isWindows,
249                                        test.config.pipefail).parse())
250        except:
251            return (Test.FAIL, "shell parser error on: %r" % ln)
252
253    cmd = cmds[0]
254    for c in cmds[1:]:
255        cmd = ShUtil.Seq(cmd, '&&', c)
256
257    results = []
258    try:
259        exitCode = executeShCmd(cmd, test.config, cwd, results)
260    except InternalShellError:
261        e = sys.exc_info()[1]
262        exitCode = 127
263        results.append((e.command, '', e.message, exitCode))
264
265    out = err = ''
266    for i,(cmd, cmd_out,cmd_err,res) in enumerate(results):
267        out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
268        out += 'Command %d Result: %r\n' % (i, res)
269        out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
270        out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
271
272    return out, err, exitCode
273
274def executeScript(test, litConfig, tmpBase, commands, cwd):
275    bashPath = litConfig.getBashPath();
276    isWin32CMDEXE = (litConfig.isWindows and not bashPath)
277    script = tmpBase + '.script'
278    if isWin32CMDEXE:
279        script += '.bat'
280
281    # Write script file
282    mode = 'w'
283    if litConfig.isWindows and not isWin32CMDEXE:
284      mode += 'b'  # Avoid CRLFs when writing bash scripts.
285    f = open(script, mode)
286    if isWin32CMDEXE:
287        f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
288    else:
289        if test.config.pipefail:
290            f.write('set -o pipefail;')
291        f.write('{ ' + '; } &&\n{ '.join(commands) + '; }')
292    f.write('\n')
293    f.close()
294
295    if isWin32CMDEXE:
296        command = ['cmd','/c', script]
297    else:
298        if bashPath:
299            command = [bashPath, script]
300        else:
301            command = ['/bin/sh', script]
302        if litConfig.useValgrind:
303            # FIXME: Running valgrind on sh is overkill. We probably could just
304            # run on clang with no real loss.
305            command = litConfig.valgrindArgs + command
306
307    return executeCommand(command, cwd=cwd, env=test.config.environment)
308
309def isExpectedFail(test, xfails):
310    # Check if any of the xfails match an available feature or the target.
311    for item in xfails:
312        # If this is the wildcard, it always fails.
313        if item == '*':
314            return True
315
316        # If this is an exact match for one of the features, it fails.
317        if item in test.config.available_features:
318            return True
319
320        # If this is a part of the target triple, it fails.
321        if item in test.suite.config.target_triple:
322            return True
323
324    return False
325
326def parseIntegratedTestScript(test, normalize_slashes=False,
327                              extra_substitutions=[]):
328    """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
329    script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES'
330    information. The RUN lines also will have variable substitution performed.
331    """
332
333    # Get the temporary location, this is always relative to the test suite
334    # root, not test source root.
335    #
336    # FIXME: This should not be here?
337    sourcepath = test.getSourcePath()
338    sourcedir = os.path.dirname(sourcepath)
339    execpath = test.getExecPath()
340    execdir,execbase = os.path.split(execpath)
341    tmpDir = os.path.join(execdir, 'Output')
342    tmpBase = os.path.join(tmpDir, execbase)
343    if test.index is not None:
344        tmpBase += '_%d' % test.index
345
346    # Normalize slashes, if requested.
347    if normalize_slashes:
348        sourcepath = sourcepath.replace('\\', '/')
349        sourcedir = sourcedir.replace('\\', '/')
350        tmpDir = tmpDir.replace('\\', '/')
351        tmpBase = tmpBase.replace('\\', '/')
352
353    # We use #_MARKER_# to hide %% while we do the other substitutions.
354    substitutions = list(extra_substitutions)
355    substitutions.extend([('%%', '#_MARKER_#')])
356    substitutions.extend(test.config.substitutions)
357    substitutions.extend([('%s', sourcepath),
358                          ('%S', sourcedir),
359                          ('%p', sourcedir),
360                          ('%{pathsep}', os.pathsep),
361                          ('%t', tmpBase + '.tmp'),
362                          ('%T', tmpDir),
363                          ('#_MARKER_#', '%')])
364
365    # Collect the test lines from the script.
366    script = []
367    xfails = []
368    requires = []
369    line_number = 0
370    for ln in open(sourcepath):
371        line_number += 1
372        if 'RUN:' in ln:
373            # Isolate the command to run.
374            index = ln.index('RUN:')
375            ln = ln[index+4:]
376
377            # Trim trailing whitespace.
378            ln = ln.rstrip()
379
380            # Substitute line number expressions
381            ln = re.sub('%\(line\)', str(line_number), ln)
382            def replace_line_number(match):
383                if match.group(1) == '+':
384                    return str(line_number + int(match.group(2)))
385                if match.group(1) == '-':
386                    return str(line_number - int(match.group(2)))
387            ln = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, ln)
388
389            # Collapse lines with trailing '\\'.
390            if script and script[-1][-1] == '\\':
391                script[-1] = script[-1][:-1] + ln
392            else:
393                script.append(ln)
394        elif 'XFAIL:' in ln:
395            items = ln[ln.index('XFAIL:') + 6:].split(',')
396            xfails.extend([s.strip() for s in items])
397        elif 'REQUIRES:' in ln:
398            items = ln[ln.index('REQUIRES:') + 9:].split(',')
399            requires.extend([s.strip() for s in items])
400        elif 'END.' in ln:
401            # Check for END. lines.
402            if ln[ln.index('END.'):].strip() == 'END.':
403                break
404
405    # Apply substitutions to the script.  Allow full regular
406    # expression syntax.  Replace each matching occurrence of regular
407    # expression pattern a with substitution b in line ln.
408    def processLine(ln):
409        # Apply substitutions
410        for a,b in substitutions:
411            if kIsWindows:
412                b = b.replace("\\","\\\\")
413            ln = re.sub(a, b, ln)
414
415        # Strip the trailing newline and any extra whitespace.
416        return ln.strip()
417    script = map(processLine, script)
418
419    # Verify the script contains a run line.
420    if not script:
421        return (Test.UNRESOLVED, "Test has no run line!")
422
423    # Check for unterminated run lines.
424    if script[-1][-1] == '\\':
425        return (Test.UNRESOLVED, "Test has unterminated run lines (with '\\')")
426
427    # Check that we have the required features:
428    missing_required_features = [f for f in requires
429                                 if f not in test.config.available_features]
430    if missing_required_features:
431        msg = ', '.join(missing_required_features)
432        return (Test.UNSUPPORTED,
433                "Test requires the following features: %s" % msg)
434
435    isXFail = isExpectedFail(test, xfails)
436    return script,isXFail,tmpBase,execdir
437
438def formatTestOutput(status, out, err, exitCode, script):
439    output = StringIO.StringIO()
440    output.write("Script:\n")
441    output.write("--\n")
442    output.write('\n'.join(script))
443    output.write("\n--\n")
444    output.write("Exit Code: %r\n\n" % exitCode)
445    if out:
446        output.write("Command Output (stdout):\n")
447        output.write("--\n")
448        output.write(out)
449        output.write("--\n")
450    if err:
451        output.write("Command Output (stderr):\n")
452        output.write("--\n")
453        output.write(err)
454        output.write("--\n")
455    return (status, output.getvalue())
456
457def executeShTest(test, litConfig, useExternalSh,
458                  extra_substitutions=[]):
459    if test.config.unsupported:
460        return (Test.UNSUPPORTED, 'Test is unsupported')
461
462    res = parseIntegratedTestScript(test, useExternalSh, extra_substitutions)
463    if len(res) == 2:
464        return res
465
466    script, isXFail, tmpBase, execdir = res
467
468    # Create the output directory if it does not already exist.
469    Util.mkdir_p(os.path.dirname(tmpBase))
470
471    if useExternalSh:
472        res = executeScript(test, litConfig, tmpBase, script, execdir)
473    else:
474        res = executeScriptInternal(test, litConfig, tmpBase, script, execdir)
475    if len(res) == 2:
476        return res
477
478    out,err,exitCode = res
479    if isXFail:
480        ok = exitCode != 0
481        if ok:
482            status = Test.XFAIL
483        else:
484            status = Test.XPASS
485    else:
486        ok = exitCode == 0
487        if ok:
488            status = Test.PASS
489        else:
490            status = Test.FAIL
491
492    if ok:
493        return (status,'')
494
495    return formatTestOutput(status, out, err, exitCode, script)
496