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