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