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 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).parse())
249        except:
250            return (Test.FAIL, "shell parser error on: %r" % ln)
251
252    cmd = cmds[0]
253    for c in cmds[1:]:
254        cmd = ShUtil.Seq(cmd, '&&', c)
255
256    results = []
257    try:
258        exitCode = executeShCmd(cmd, test.config, cwd, results)
259    except InternalShellError,e:
260        exitCode = 127
261        results.append((e.command, '', e.message, exitCode))
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