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