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