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