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