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