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