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