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