lit: Workaround a Win32/subprocess bug when appending.
[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                     # Workaround a Win32 and/or subprocess bug when appending.
116                     if r[1] == 'a':
117                         r[2].seek(0, os.SEEK_END)
118                 result = r[2]
119             final_redirects.append(result)
120
121         stdin, stdout, stderr = final_redirects
122
123         # If stderr wants to come from stdout, but stdout isn't a pipe, then put
124         # stderr on a pipe and treat it as stdout.
125         if (stderr == subprocess.STDOUT and stdout != subprocess.PIPE):
126             stderr = subprocess.PIPE
127             stderrIsStdout = True
128         else:
129             stderrIsStdout = False
130
131             # Don't allow stderr on a PIPE except for the last
132             # process, this could deadlock.
133             #
134             # FIXME: This is slow, but so is deadlock.
135             if stderr == subprocess.PIPE and j != cmd.commands[-1]:
136                 stderr = tempfile.TemporaryFile(mode='w+b')
137                 stderrTempFiles.append((i, stderr))
138
139         # Resolve the executable path ourselves.
140         args = list(j.args)
141         args[0] = Util.which(args[0], cfg.environment['PATH'])
142         if not args[0]:
143             raise InternalShellError(j, '%r: command not found' % j.args[0])
144
145         procs.append(subprocess.Popen(args, cwd=cwd,
146                                       stdin = stdin,
147                                       stdout = stdout,
148                                       stderr = stderr,
149                                       env = cfg.environment,
150                                       close_fds = kUseCloseFDs))
151
152         # Immediately close stdin for any process taking stdin from us.
153         if stdin == subprocess.PIPE:
154             procs[-1].stdin.close()
155             procs[-1].stdin = None
156
157         # Update the current stdin source.
158         if stdout == subprocess.PIPE:
159             input = procs[-1].stdout
160         elif stderrIsStdout:
161             input = procs[-1].stderr
162         else:
163             input = subprocess.PIPE
164
165     # FIXME: There is probably still deadlock potential here. Yawn.
166     procData = [None] * len(procs)
167     procData[-1] = procs[-1].communicate()
168
169     for i in range(len(procs) - 1):
170         if procs[i].stdout is not None:
171             out = procs[i].stdout.read()
172         else:
173             out = ''
174         if procs[i].stderr is not None:
175             err = procs[i].stderr.read()
176         else:
177             err = ''
178         procData[i] = (out,err)
179         
180     # Read stderr out of the temp files.
181     for i,f in stderrTempFiles:
182         f.seek(0, 0)
183         procData[i] = (procData[i][0], f.read())
184
185     exitCode = None
186     for i,(out,err) in enumerate(procData):
187         res = procs[i].wait()
188         # Detect Ctrl-C in subprocess.
189         if res == -signal.SIGINT:
190             raise KeyboardInterrupt
191
192         results.append((cmd.commands[i], out, err, res))
193         if cmd.pipe_err:
194             # Python treats the exit code as a signed char.
195             if res < 0:
196                 exitCode = min(exitCode, res)
197             else:
198                 exitCode = max(exitCode, res)
199         else:
200             exitCode = res
201
202     if cmd.negate:
203         exitCode = not exitCode
204
205     return exitCode
206
207 def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
208     ln = ' &&\n'.join(commands)
209     try:
210         cmd = ShUtil.ShParser(ln, litConfig.isWindows).parse()
211     except:
212         return (Test.FAIL, "shell parser error on: %r" % ln)
213
214     results = []
215     try:
216         exitCode = executeShCmd(cmd, test.config, cwd, results)
217     except InternalShellError,e:
218         out = ''
219         err = e.message
220         exitCode = 255
221
222     out = err = ''
223     for i,(cmd, cmd_out,cmd_err,res) in enumerate(results):
224         out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
225         out += 'Command %d Result: %r\n' % (i, res)
226         out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
227         out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
228
229     return out, err, exitCode
230
231 def executeTclScriptInternal(test, litConfig, tmpBase, commands, cwd):
232     import TclUtil
233     cmds = []
234     for ln in commands:
235         # Given the unfortunate way LLVM's test are written, the line gets
236         # backslash substitution done twice.
237         ln = TclUtil.TclLexer(ln).lex_unquoted(process_all = True)
238
239         try:
240             tokens = list(TclUtil.TclLexer(ln).lex())
241         except:
242             return (Test.FAIL, "Tcl lexer error on: %r" % ln)
243
244         # Validate there are no control tokens.
245         for t in tokens:
246             if not isinstance(t, str):
247                 return (Test.FAIL,
248                         "Invalid test line: %r containing %r" % (ln, t))
249
250         try:
251             cmds.append(TclUtil.TclExecCommand(tokens).parse_pipeline())
252         except:
253             return (Test.FAIL, "Tcl 'exec' parse error on: %r" % ln)
254
255     cmd = cmds[0]
256     for c in cmds[1:]:
257         cmd = ShUtil.Seq(cmd, '&&', c)
258
259     # FIXME: This is lame, we shouldn't need bash. See PR5240.
260     bashPath = litConfig.getBashPath()
261     if litConfig.useTclAsSh and bashPath:
262         script = tmpBase + '.script'
263
264         # Write script file
265         f = open(script,'w')
266         print >>f, 'set -o pipefail'
267         cmd.toShell(f, pipefail = True)
268         f.close()
269
270         if 0:
271             print >>sys.stdout, cmd
272             print >>sys.stdout, open(script).read()
273             print >>sys.stdout
274             return '', '', 0
275
276         command = [litConfig.getBashPath(), script]
277         out,err,exitCode = executeCommand(command, cwd=cwd,
278                                           env=test.config.environment)
279
280         # Tcl commands fail on standard error output.
281         if err:
282             exitCode = 1
283             out = 'Command has output on stderr!\n\n' + out
284
285         return out,err,exitCode
286     else:
287         results = []
288         try:
289             exitCode = executeShCmd(cmd, test.config, cwd, results)
290         except InternalShellError,e:
291             results.append((e.command, '', e.message + '\n', 255))
292             exitCode = 255
293
294     out = err = ''
295
296     # Tcl commands fail on standard error output.
297     if [True for _,_,err,res in results if err]:
298         exitCode = 1
299         out += 'Command has output on stderr!\n\n'
300
301     for i,(cmd, cmd_out, cmd_err, res) in enumerate(results):
302         out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
303         out += 'Command %d Result: %r\n' % (i, res)
304         out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
305         out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
306
307     return out, err, exitCode
308
309 def executeScript(test, litConfig, tmpBase, commands, cwd):
310     script = tmpBase + '.script'
311     if litConfig.isWindows:
312         script += '.bat'
313
314     # Write script file
315     f = open(script,'w')
316     if litConfig.isWindows:
317         f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
318     else:
319         f.write(' &&\n'.join(commands))
320     f.write('\n')
321     f.close()
322
323     if litConfig.isWindows:
324         command = ['cmd','/c', script]
325     else:
326         command = ['/bin/sh', script]
327         if litConfig.useValgrind:
328             # FIXME: Running valgrind on sh is overkill. We probably could just
329             # run on clang with no real loss.
330             valgrindArgs = ['valgrind', '-q',
331                             '--tool=memcheck', '--trace-children=yes',
332                             '--error-exitcode=123']
333             valgrindArgs.extend(litConfig.valgrindArgs)
334
335             command = valgrindArgs + command
336
337     return executeCommand(command, cwd=cwd, env=test.config.environment)
338
339 def isExpectedFail(xfails, xtargets, target_triple):
340     # Check if any xfail matches this target.
341     for item in xfails:
342         if item == '*' or item in target_triple:
343             break
344     else:
345         return False
346
347     # If so, see if it is expected to pass on this target.
348     #
349     # FIXME: Rename XTARGET to something that makes sense, like XPASS.
350     for item in xtargets:
351         if item == '*' or item in target_triple:
352             return False
353
354     return True
355
356 def parseIntegratedTestScript(test, requireAndAnd):
357     """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
358     script and extract the lines to 'RUN' as well as 'XFAIL' and 'XTARGET'
359     information. The RUN lines also will have variable substitution performed.
360     """
361
362     # Get the temporary location, this is always relative to the test suite
363     # root, not test source root.
364     #
365     # FIXME: This should not be here?
366     sourcepath = test.getSourcePath()
367     execpath = test.getExecPath()
368     execdir,execbase = os.path.split(execpath)
369     tmpBase = os.path.join(execdir, 'Output', execbase)
370
371     # We use #_MARKER_# to hide %% while we do the other substitutions.
372     substitutions = [('%%', '#_MARKER_#')]
373     substitutions.extend(test.config.substitutions)
374     substitutions.extend([('%s', sourcepath),
375                           ('%S', os.path.dirname(sourcepath)),
376                           ('%p', os.path.dirname(sourcepath)),
377                           ('%t', tmpBase + '.tmp'),
378                           # FIXME: Remove this once we kill DejaGNU.
379                           ('%abs_tmp', tmpBase + '.tmp'),
380                           ('#_MARKER_#', '%')])
381
382     # Collect the test lines from the script.
383     script = []
384     xfails = []
385     xtargets = []
386     for ln in open(sourcepath):
387         if 'RUN:' in ln:
388             # Isolate the command to run.
389             index = ln.index('RUN:')
390             ln = ln[index+4:]
391
392             # Trim trailing whitespace.
393             ln = ln.rstrip()
394
395             # Collapse lines with trailing '\\'.
396             if script and script[-1][-1] == '\\':
397                 script[-1] = script[-1][:-1] + ln
398             else:
399                 script.append(ln)
400         elif 'XFAIL:' in ln:
401             items = ln[ln.index('XFAIL:') + 6:].split(',')
402             xfails.extend([s.strip() for s in items])
403         elif 'XTARGET:' in ln:
404             items = ln[ln.index('XTARGET:') + 8:].split(',')
405             xtargets.extend([s.strip() for s in items])
406         elif 'END.' in ln:
407             # Check for END. lines.
408             if ln[ln.index('END.'):].strip() == 'END.':
409                 break
410
411     # Apply substitutions to the script.
412     def processLine(ln):
413         # Apply substitutions
414         for a,b in substitutions:
415             ln = ln.replace(a,b)
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     if script[-1][-1] == '\\':
426         return (Test.UNRESOLVED, "Test has unterminated run lines (with '\\')")
427
428     # Validate interior lines for '&&', a lovely historical artifact.
429     if requireAndAnd:
430         for i in range(len(script) - 1):
431             ln = script[i]
432
433             if not ln.endswith('&&'):
434                 return (Test.FAIL,
435                         ("MISSING \'&&\': %s\n"  +
436                          "FOLLOWED BY   : %s\n") % (ln, script[i + 1]))
437
438             # Strip off '&&'
439             script[i] = ln[:-2]
440
441     isXFail = isExpectedFail(xfails, xtargets, test.suite.config.target_triple)
442     return script,isXFail,tmpBase,execdir
443
444 def 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
461 def executeTclTest(test, litConfig):
462     if test.config.unsupported:
463         return (Test.UNSUPPORTED, 'Test is unsupported')
464
465     res = parseIntegratedTestScript(test, False)
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
494 def executeShTest(test, litConfig, useExternalSh, requireAndAnd):
495     if test.config.unsupported:
496         return (Test.UNSUPPORTED, 'Test is unsupported')
497
498     res = parseIntegratedTestScript(test, requireAndAnd)
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)