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
26# Negate if win32file is not found.
27kHaveWin32File = kIsWindows
28
29def RemoveForce(f):
30    try:
31        os.remove(f)
32    except OSError:
33        pass
34
35def WinWaitReleased(f):
36    global kHaveWin32File
37    if not kHaveWin32File:
38        return
39    try:
40        import time
41        import win32file, pywintypes
42        retry_cnt = 256
43        while True:
44            try:
45                h = win32file.CreateFile(
46                    f,
47                    win32file.GENERIC_READ,
48                    0, # Exclusive
49                    None,
50                    win32file.OPEN_EXISTING,
51                    win32file.FILE_ATTRIBUTE_NORMAL,
52                    None)
53                h.close()
54                return
55            except WindowsError, (winerror, strerror):
56                retry_cnt = retry_cnt - 1
57                if retry_cnt <= 0:
58                    raise
59                elif winerror == 32: # ERROR_SHARING_VIOLATION
60                    pass
61                else:
62                    raise
63            except pywintypes.error, e:
64                retry_cnt = retry_cnt - 1
65                if retry_cnt <= 0:
66                    raise
67                elif e[0]== 32: # ERROR_SHARING_VIOLATION
68                    pass
69                else:
70                    raise
71            time.sleep(0.01)
72    except ImportError, e:
73        kHaveWin32File = False
74        return
75
76def executeCommand(command, cwd=None, env=None):
77    p = subprocess.Popen(command, cwd=cwd,
78                         stdin=subprocess.PIPE,
79                         stdout=subprocess.PIPE,
80                         stderr=subprocess.PIPE,
81                         env=env)
82    out,err = p.communicate()
83    exitCode = p.wait()
84
85    # Detect Ctrl-C in subprocess.
86    if exitCode == -signal.SIGINT:
87        raise KeyboardInterrupt
88
89    return out, err, exitCode
90
91def executeShCmd(cmd, cfg, cwd, results):
92    if isinstance(cmd, ShUtil.Seq):
93        if cmd.op == ';':
94            res = executeShCmd(cmd.lhs, cfg, cwd, results)
95            return executeShCmd(cmd.rhs, cfg, cwd, results)
96
97        if cmd.op == '&':
98            raise NotImplementedError,"unsupported test command: '&'"
99
100        if cmd.op == '||':
101            res = executeShCmd(cmd.lhs, cfg, cwd, results)
102            if res != 0:
103                res = executeShCmd(cmd.rhs, cfg, cwd, results)
104            return res
105        if cmd.op == '&&':
106            res = executeShCmd(cmd.lhs, cfg, cwd, results)
107            if res is None:
108                return res
109
110            if res == 0:
111                res = executeShCmd(cmd.rhs, cfg, cwd, results)
112            return res
113
114        raise ValueError,'Unknown shell command: %r' % cmd.op
115
116    assert isinstance(cmd, ShUtil.Pipeline)
117    procs = []
118    input = subprocess.PIPE
119    stderrTempFiles = []
120    opened_files = []
121    named_temp_files = []
122    # To avoid deadlock, we use a single stderr stream for piped
123    # output. This is null until we have seen some output using
124    # stderr.
125    for i,j in enumerate(cmd.commands):
126        # Apply the redirections, we use (N,) as a sentinal to indicate stdin,
127        # stdout, stderr for N equal to 0, 1, or 2 respectively. Redirects to or
128        # from a file are represented with a list [file, mode, file-object]
129        # where file-object is initially None.
130        redirects = [(0,), (1,), (2,)]
131        for r in j.redirects:
132            if r[0] == ('>',2):
133                redirects[2] = [r[1], 'w', None]
134            elif r[0] == ('>>',2):
135                redirects[2] = [r[1], 'a', None]
136            elif r[0] == ('>&',2) and r[1] in '012':
137                redirects[2] = redirects[int(r[1])]
138            elif r[0] == ('>&',) or r[0] == ('&>',):
139                redirects[1] = redirects[2] = [r[1], 'w', None]
140            elif r[0] == ('>',):
141                redirects[1] = [r[1], 'w', None]
142            elif r[0] == ('>>',):
143                redirects[1] = [r[1], 'a', None]
144            elif r[0] == ('<',):
145                redirects[0] = [r[1], 'r', None]
146            else:
147                raise NotImplementedError,"Unsupported redirect: %r" % (r,)
148
149        # Map from the final redirections to something subprocess can handle.
150        final_redirects = []
151        for index,r in enumerate(redirects):
152            if r == (0,):
153                result = input
154            elif r == (1,):
155                if index == 0:
156                    raise NotImplementedError,"Unsupported redirect for stdin"
157                elif index == 1:
158                    result = subprocess.PIPE
159                else:
160                    result = subprocess.STDOUT
161            elif r == (2,):
162                if index != 2:
163                    raise NotImplementedError,"Unsupported redirect on stdout"
164                result = subprocess.PIPE
165            else:
166                if r[2] is None:
167                    if kAvoidDevNull and r[0] == '/dev/null':
168                        r[0] = None
169                        r[2] = tempfile.TemporaryFile(mode=r[1])
170                    else:
171                        r[2] = open(r[0], r[1])
172                    # Workaround a Win32 and/or subprocess bug when appending.
173                    #
174                    # FIXME: Actually, this is probably an instance of PR6753.
175                    if r[1] == 'a':
176                        r[2].seek(0, 2)
177                    opened_files.append(r)
178                result = r[2]
179            final_redirects.append(result)
180
181        stdin, stdout, stderr = final_redirects
182
183        # If stderr wants to come from stdout, but stdout isn't a pipe, then put
184        # stderr on a pipe and treat it as stdout.
185        if (stderr == subprocess.STDOUT and stdout != subprocess.PIPE):
186            stderr = subprocess.PIPE
187            stderrIsStdout = True
188        else:
189            stderrIsStdout = False
190
191            # Don't allow stderr on a PIPE except for the last
192            # process, this could deadlock.
193            #
194            # FIXME: This is slow, but so is deadlock.
195            if stderr == subprocess.PIPE and j != cmd.commands[-1]:
196                stderr = tempfile.TemporaryFile(mode='w+b')
197                stderrTempFiles.append((i, stderr))
198
199        # Resolve the executable path ourselves.
200        args = list(j.args)
201        args[0] = Util.which(args[0], cfg.environment['PATH'])
202        if not args[0]:
203            raise InternalShellError(j, '%r: command not found' % j.args[0])
204
205        # Replace uses of /dev/null with temporary files.
206        if kAvoidDevNull:
207            for i,arg in enumerate(args):
208                if arg == "/dev/null":
209                    f = tempfile.NamedTemporaryFile(delete=False)
210                    f.close()
211                    named_temp_files.append(f.name)
212                    args[i] = f.name
213
214        procs.append(subprocess.Popen(args, cwd=cwd,
215                                      stdin = stdin,
216                                      stdout = stdout,
217                                      stderr = stderr,
218                                      env = cfg.environment,
219                                      close_fds = kUseCloseFDs))
220
221        # Immediately close stdin for any process taking stdin from us.
222        if stdin == subprocess.PIPE:
223            procs[-1].stdin.close()
224            procs[-1].stdin = None
225
226        # Update the current stdin source.
227        if stdout == subprocess.PIPE:
228            input = procs[-1].stdout
229        elif stderrIsStdout:
230            input = procs[-1].stderr
231        else:
232            input = subprocess.PIPE
233
234    # Explicitly close any redirected files. We need to do this now because we
235    # need to release any handles we may have on the temporary files (important
236    # on Win32, for example). Since we have already spawned the subprocess, our
237    # handles have already been transferred so we do not need them anymore.
238    for f in opened_files:
239        f[2].close()
240
241    # FIXME: There is probably still deadlock potential here. Yawn.
242    procData = [None] * len(procs)
243    procData[-1] = procs[-1].communicate()
244
245    for i in range(len(procs) - 1):
246        if procs[i].stdout is not None:
247            out = procs[i].stdout.read()
248        else:
249            out = ''
250        if procs[i].stderr is not None:
251            err = procs[i].stderr.read()
252        else:
253            err = ''
254        procData[i] = (out,err)
255
256    # Read stderr out of the temp files.
257    for i,f in stderrTempFiles:
258        f.seek(0, 0)
259        procData[i] = (procData[i][0], f.read())
260
261    exitCode = None
262    for i,(out,err) in enumerate(procData):
263        res = procs[i].wait()
264        # Detect Ctrl-C in subprocess.
265        if res == -signal.SIGINT:
266            raise KeyboardInterrupt
267
268        results.append((cmd.commands[i], out, err, res))
269        if cmd.pipe_err:
270            # Python treats the exit code as a signed char.
271            if res < 0:
272                exitCode = min(exitCode, res)
273            else:
274                exitCode = max(exitCode, res)
275        else:
276            exitCode = res
277
278    # Make sure opened_files is released by other (child) processes.
279    if kIsWindows:
280        for f in opened_files:
281            if f[0] is not None:
282                WinWaitReleased(f[0])
283
284    # Remove any named temporary files we created.
285    for f in named_temp_files:
286        RemoveForce(f)
287
288    if cmd.negate:
289        exitCode = not exitCode
290
291    return exitCode
292
293def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
294    ln = ' &&\n'.join(commands)
295    try:
296        cmd = ShUtil.ShParser(ln, litConfig.isWindows).parse()
297    except:
298        return (Test.FAIL, "shell parser error on: %r" % ln)
299
300    results = []
301    try:
302        exitCode = executeShCmd(cmd, test.config, cwd, results)
303    except InternalShellError,e:
304        out = ''
305        err = e.message
306        exitCode = 255
307
308    out = err = ''
309    for i,(cmd, cmd_out,cmd_err,res) in enumerate(results):
310        out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
311        out += 'Command %d Result: %r\n' % (i, res)
312        out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
313        out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
314
315    return out, err, exitCode
316
317def executeTclScriptInternal(test, litConfig, tmpBase, commands, cwd):
318    import TclUtil
319    cmds = []
320    for ln in commands:
321        # Given the unfortunate way LLVM's test are written, the line gets
322        # backslash substitution done twice.
323        ln = TclUtil.TclLexer(ln).lex_unquoted(process_all = True)
324
325        try:
326            tokens = list(TclUtil.TclLexer(ln).lex())
327        except:
328            return (Test.FAIL, "Tcl lexer error on: %r" % ln)
329
330        # Validate there are no control tokens.
331        for t in tokens:
332            if not isinstance(t, str):
333                return (Test.FAIL,
334                        "Invalid test line: %r containing %r" % (ln, t))
335
336        try:
337            cmds.append(TclUtil.TclExecCommand(tokens).parse_pipeline())
338        except:
339            return (Test.FAIL, "Tcl 'exec' parse error on: %r" % ln)
340
341    if litConfig.useValgrind:
342        for pipeline in cmds:
343            if pipeline.commands:
344                # Only valgrind the first command in each pipeline, to avoid
345                # valgrinding things like grep, not, and FileCheck.
346                cmd = pipeline.commands[0]
347                cmd.args = litConfig.valgrindArgs + cmd.args
348
349    cmd = cmds[0]
350    for c in cmds[1:]:
351        cmd = ShUtil.Seq(cmd, '&&', c)
352
353    # FIXME: This is lame, we shouldn't need bash. See PR5240.
354    bashPath = litConfig.getBashPath()
355    if litConfig.useTclAsSh and bashPath:
356        script = tmpBase + '.script'
357
358        # Write script file
359        f = open(script,'w')
360        print >>f, 'set -o pipefail'
361        cmd.toShell(f, pipefail = True)
362        f.close()
363
364        if 0:
365            print >>sys.stdout, cmd
366            print >>sys.stdout, open(script).read()
367            print >>sys.stdout
368            return '', '', 0
369
370        command = [litConfig.getBashPath(), script]
371        out,err,exitCode = executeCommand(command, cwd=cwd,
372                                          env=test.config.environment)
373
374        return out,err,exitCode
375    else:
376        results = []
377        try:
378            exitCode = executeShCmd(cmd, test.config, cwd, results)
379        except InternalShellError,e:
380            results.append((e.command, '', e.message + '\n', 255))
381            exitCode = 255
382
383    out = err = ''
384
385    for i,(cmd, cmd_out, cmd_err, res) in enumerate(results):
386        out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
387        out += 'Command %d Result: %r\n' % (i, res)
388        out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
389        out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
390
391    return out, err, exitCode
392
393def executeScript(test, litConfig, tmpBase, commands, cwd):
394    bashPath = litConfig.getBashPath();
395    isWin32CMDEXE = (litConfig.isWindows and not bashPath)
396    script = tmpBase + '.script'
397    if isWin32CMDEXE:
398        script += '.bat'
399
400    # Write script file
401    f = open(script,'w')
402    if isWin32CMDEXE:
403        f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
404    else:
405        f.write(' &&\n'.join(commands))
406    f.write('\n')
407    f.close()
408
409    if isWin32CMDEXE:
410        command = ['cmd','/c', script]
411    else:
412        if bashPath:
413            command = [bashPath, script]
414        else:
415            command = ['/bin/sh', script]
416        if litConfig.useValgrind:
417            # FIXME: Running valgrind on sh is overkill. We probably could just
418            # run on clang with no real loss.
419            command = litConfig.valgrindArgs + command
420
421    return executeCommand(command, cwd=cwd, env=test.config.environment)
422
423def isExpectedFail(xfails, xtargets, target_triple):
424    # Check if any xfail matches this target.
425    for item in xfails:
426        if item == '*' or item in target_triple:
427            break
428    else:
429        return False
430
431    # If so, see if it is expected to pass on this target.
432    #
433    # FIXME: Rename XTARGET to something that makes sense, like XPASS.
434    for item in xtargets:
435        if item == '*' or item in target_triple:
436            return False
437
438    return True
439
440def parseIntegratedTestScript(test, normalize_slashes=False,
441                              extra_substitutions=[]):
442    """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
443    script and extract the lines to 'RUN' as well as 'XFAIL' and 'XTARGET'
444    information. The RUN lines also will have variable substitution performed.
445    """
446
447    # Get the temporary location, this is always relative to the test suite
448    # root, not test source root.
449    #
450    # FIXME: This should not be here?
451    sourcepath = test.getSourcePath()
452    sourcedir = os.path.dirname(sourcepath)
453    execpath = test.getExecPath()
454    execdir,execbase = os.path.split(execpath)
455    tmpDir = os.path.join(execdir, 'Output')
456    tmpBase = os.path.join(tmpDir, execbase)
457    if test.index is not None:
458        tmpBase += '_%d' % test.index
459
460    # Normalize slashes, if requested.
461    if normalize_slashes:
462        sourcepath = sourcepath.replace('\\', '/')
463        sourcedir = sourcedir.replace('\\', '/')
464        tmpDir = tmpDir.replace('\\', '/')
465        tmpBase = tmpBase.replace('\\', '/')
466
467    # We use #_MARKER_# to hide %% while we do the other substitutions.
468    substitutions = list(extra_substitutions)
469    substitutions.extend([('%%', '#_MARKER_#')])
470    substitutions.extend(test.config.substitutions)
471    substitutions.extend([('%s', sourcepath),
472                          ('%S', sourcedir),
473                          ('%p', sourcedir),
474                          ('%{pathsep}', os.pathsep),
475                          ('%t', tmpBase + '.tmp'),
476                          ('%T', tmpDir),
477                          # FIXME: Remove this once we kill DejaGNU.
478                          ('%abs_tmp', tmpBase + '.tmp'),
479                          ('#_MARKER_#', '%')])
480
481    # Collect the test lines from the script.
482    script = []
483    xfails = []
484    xtargets = []
485    requires = []
486    for ln in open(sourcepath):
487        if 'RUN:' in ln:
488            # Isolate the command to run.
489            index = ln.index('RUN:')
490            ln = ln[index+4:]
491
492            # Trim trailing whitespace.
493            ln = ln.rstrip()
494
495            # Collapse lines with trailing '\\'.
496            if script and script[-1][-1] == '\\':
497                script[-1] = script[-1][:-1] + ln
498            else:
499                script.append(ln)
500        elif 'XFAIL:' in ln:
501            items = ln[ln.index('XFAIL:') + 6:].split(',')
502            xfails.extend([s.strip() for s in items])
503        elif 'XTARGET:' in ln:
504            items = ln[ln.index('XTARGET:') + 8:].split(',')
505            xtargets.extend([s.strip() for s in items])
506        elif 'REQUIRES:' in ln:
507            items = ln[ln.index('REQUIRES:') + 9:].split(',')
508            requires.extend([s.strip() for s in items])
509        elif 'END.' in ln:
510            # Check for END. lines.
511            if ln[ln.index('END.'):].strip() == 'END.':
512                break
513
514    # Apply substitutions to the script.  Allow full regular
515    # expression syntax.  Replace each matching occurrence of regular
516    # expression pattern a with substitution b in line ln.
517    def processLine(ln):
518        # Apply substitutions
519        for a,b in substitutions:
520            if kIsWindows:
521                b = b.replace("\\","\\\\")
522            ln = re.sub(a, b, ln)
523
524        # Strip the trailing newline and any extra whitespace.
525        return ln.strip()
526    script = map(processLine, script)
527
528    # Verify the script contains a run line.
529    if not script:
530        return (Test.UNRESOLVED, "Test has no run line!")
531
532    # Check for unterminated run lines.
533    if script[-1][-1] == '\\':
534        return (Test.UNRESOLVED, "Test has unterminated run lines (with '\\')")
535
536    # Check that we have the required features:
537    missing_required_features = [f for f in requires
538                                 if f not in test.config.available_features]
539    if missing_required_features:
540        msg = ', '.join(missing_required_features)
541        return (Test.UNSUPPORTED,
542                "Test requires the following features: %s" % msg)
543
544    isXFail = isExpectedFail(xfails, xtargets, test.suite.config.target_triple)
545    return script,isXFail,tmpBase,execdir
546
547def formatTestOutput(status, out, err, exitCode, failDueToStderr, script):
548    output = StringIO.StringIO()
549    print >>output, "Script:"
550    print >>output, "--"
551    print >>output, '\n'.join(script)
552    print >>output, "--"
553    print >>output, "Exit Code: %r" % exitCode,
554    if failDueToStderr:
555        print >>output, "(but there was output on stderr)"
556    else:
557        print >>output
558    if out:
559        print >>output, "Command Output (stdout):"
560        print >>output, "--"
561        output.write(out)
562        print >>output, "--"
563    if err:
564        print >>output, "Command Output (stderr):"
565        print >>output, "--"
566        output.write(err)
567        print >>output, "--"
568    return (status, output.getvalue())
569
570def executeTclTest(test, litConfig):
571    if test.config.unsupported:
572        return (Test.UNSUPPORTED, 'Test is unsupported')
573
574    # Parse the test script, normalizing slashes in substitutions on Windows
575    # (since otherwise Tcl style lexing will treat them as escapes).
576    res = parseIntegratedTestScript(test, normalize_slashes=kIsWindows)
577    if len(res) == 2:
578        return res
579
580    script, isXFail, tmpBase, execdir = res
581
582    if litConfig.noExecute:
583        return (Test.PASS, '')
584
585    # Create the output directory if it does not already exist.
586    Util.mkdir_p(os.path.dirname(tmpBase))
587
588    res = executeTclScriptInternal(test, litConfig, tmpBase, script, execdir)
589    if len(res) == 2:
590        return res
591
592    # Test for failure. In addition to the exit code, Tcl commands are
593    # considered to fail if there is any standard error output.
594    out,err,exitCode = res
595    if isXFail:
596        ok = exitCode != 0 or err and not litConfig.ignoreStdErr
597        if ok:
598            status = Test.XFAIL
599        else:
600            status = Test.XPASS
601    else:
602        ok = exitCode == 0 and (not err or litConfig.ignoreStdErr)
603        if ok:
604            status = Test.PASS
605        else:
606            status = Test.FAIL
607
608    if ok:
609        return (status,'')
610
611    # Set a flag for formatTestOutput so it can explain why the test was
612    # considered to have failed, despite having an exit code of 0.
613    failDueToStderr = exitCode == 0 and err and not litConfig.ignoreStdErr
614
615    return formatTestOutput(status, out, err, exitCode, failDueToStderr, script)
616
617def executeShTest(test, litConfig, useExternalSh,
618                  extra_substitutions=[]):
619    if test.config.unsupported:
620        return (Test.UNSUPPORTED, 'Test is unsupported')
621
622    res = parseIntegratedTestScript(test, useExternalSh, extra_substitutions)
623    if len(res) == 2:
624        return res
625
626    script, isXFail, tmpBase, execdir = res
627
628    if litConfig.noExecute:
629        return (Test.PASS, '')
630
631    # Create the output directory if it does not already exist.
632    Util.mkdir_p(os.path.dirname(tmpBase))
633
634    if useExternalSh:
635        res = executeScript(test, litConfig, tmpBase, script, execdir)
636    else:
637        res = executeScriptInternal(test, litConfig, tmpBase, script, execdir)
638    if len(res) == 2:
639        return res
640
641    out,err,exitCode = res
642    if isXFail:
643        ok = exitCode != 0
644        if ok:
645            status = Test.XFAIL
646        else:
647            status = Test.XPASS
648    else:
649        ok = exitCode == 0
650        if ok:
651            status = Test.PASS
652        else:
653            status = Test.FAIL
654
655    if ok:
656        return (status,'')
657
658    # Sh tests are not considered to fail just from stderr output.
659    failDueToStderr = False
660
661    return formatTestOutput(status, out, err, exitCode, failDueToStderr, script)
662