lit/TestRunner.py: Factor out Substitution construction; NFC
[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, keywords):
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_re = re.compile(
385         to_bytes("(%s)(.*)\n" % ("|".join(k for k in keywords),)))
386
387     f = open(source_path, 'rb')
388     try:
389         # Read the entire file contents.
390         data = f.read()
391
392         # Ensure the data ends with a newline.
393         if not data.endswith(to_bytes('\n')):
394             data = data + to_bytes('\n')
395
396         # Iterate over the matches.
397         line_number = 1
398         last_match_position = 0
399         for match in keywords_re.finditer(data):
400             # Compute the updated line number by counting the intervening
401             # newlines.
402             match_position = match.start()
403             line_number += data.count(to_bytes('\n'), last_match_position,
404                                       match_position)
405             last_match_position = match_position
406
407             # Convert the keyword and line to UTF-8 strings and yield the
408             # command. Note that we take care to return regular strings in
409             # Python 2, to avoid other code having to differentiate between the
410             # str and unicode types.
411             keyword,ln = match.groups()
412             yield (line_number, to_string(keyword[:-1].decode('utf-8')),
413                    to_string(ln.decode('utf-8')))
414     finally:
415         f.close()
416
417 def getTempPaths(test):
418     """Get the temporary location, this is always relative to the test suite
419     root, not test source root."""
420     execpath = test.getExecPath()
421     execdir,execbase = os.path.split(execpath)
422     tmpDir = os.path.join(execdir, 'Output')
423     tmpBase = os.path.join(tmpDir, execbase)
424     return tmpDir, tmpBase
425
426 def getDefaultSubstitutions(test, tmpDir, tmpBase, normalize_slashes=False):
427     sourcepath = test.getSourcePath()
428     sourcedir = os.path.dirname(sourcepath)
429
430     # Normalize slashes, if requested.
431     if normalize_slashes:
432         sourcepath = sourcepath.replace('\\', '/')
433         sourcedir = sourcedir.replace('\\', '/')
434         tmpDir = tmpDir.replace('\\', '/')
435         tmpBase = tmpBase.replace('\\', '/')
436
437     # We use #_MARKER_# to hide %% while we do the other substitutions.
438     substitutions = []
439     substitutions.extend([('%%', '#_MARKER_#')])
440     substitutions.extend(test.config.substitutions)
441     substitutions.extend([('%s', sourcepath),
442                           ('%S', sourcedir),
443                           ('%p', sourcedir),
444                           ('%{pathsep}', os.pathsep),
445                           ('%t', tmpBase + '.tmp'),
446                           ('%T', tmpDir),
447                           ('#_MARKER_#', '%')])
448
449     # "%/[STpst]" should be normalized.
450     substitutions.extend([
451             ('%/s', sourcepath.replace('\\', '/')),
452             ('%/S', sourcedir.replace('\\', '/')),
453             ('%/p', sourcedir.replace('\\', '/')),
454             ('%/t', tmpBase.replace('\\', '/') + '.tmp'),
455             ('%/T', tmpDir.replace('\\', '/')),
456             ])
457     return substitutions
458
459 def parseIntegratedTestScript(test, substitutions, require_script=True):
460     """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
461     script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES'
462     and 'UNSUPPORTED' information. The RUN lines also will have variable
463     substitution performed. If 'require_script' is False an empty script may be
464     returned. This can be used for test formats where the actual script is
465     optional or ignored.
466     """
467     # Collect the test lines from the script.
468     sourcepath = test.getSourcePath()
469     script = []
470     requires = []
471     unsupported = []
472     keywords = ['RUN:', 'XFAIL:', 'REQUIRES:', 'UNSUPPORTED:', 'END.']
473     for line_number, command_type, ln in \
474             parseIntegratedTestScriptCommands(sourcepath, keywords):
475         if command_type == 'RUN':
476             # Trim trailing whitespace.
477             ln = ln.rstrip()
478
479             # Substitute line number expressions
480             ln = re.sub('%\(line\)', str(line_number), ln)
481             def replace_line_number(match):
482                 if match.group(1) == '+':
483                     return str(line_number + int(match.group(2)))
484                 if match.group(1) == '-':
485                     return str(line_number - int(match.group(2)))
486             ln = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, ln)
487
488             # Collapse lines with trailing '\\'.
489             if script and script[-1][-1] == '\\':
490                 script[-1] = script[-1][:-1] + ln
491             else:
492                 script.append(ln)
493         elif command_type == 'XFAIL':
494             test.xfails.extend([s.strip() for s in ln.split(',')])
495         elif command_type == 'REQUIRES':
496             requires.extend([s.strip() for s in ln.split(',')])
497         elif command_type == 'UNSUPPORTED':
498             unsupported.extend([s.strip() for s in ln.split(',')])
499         elif command_type == 'END':
500             # END commands are only honored if the rest of the line is empty.
501             if not ln.strip():
502                 break
503         else:
504             raise ValueError("unknown script command type: %r" % (
505                     command_type,))
506
507     # Apply substitutions to the script.  Allow full regular
508     # expression syntax.  Replace each matching occurrence of regular
509     # expression pattern a with substitution b in line ln.
510     def processLine(ln):
511         # Apply substitutions
512         for a,b in substitutions:
513             if kIsWindows:
514                 b = b.replace("\\","\\\\")
515             ln = re.sub(a, b, ln)
516
517         # Strip the trailing newline and any extra whitespace.
518         return ln.strip()
519     script = [processLine(ln)
520               for ln in script]
521
522     # Verify the script contains a run line.
523     if require_script and not script:
524         return lit.Test.Result(Test.UNRESOLVED, "Test has no run line!")
525
526     # Check for unterminated run lines.
527     if script and script[-1][-1] == '\\':
528         return lit.Test.Result(Test.UNRESOLVED,
529                                "Test has unterminated run lines (with '\\')")
530
531     # Check that we have the required features:
532     missing_required_features = [f for f in requires
533                                  if f not in test.config.available_features]
534     if missing_required_features:
535         msg = ', '.join(missing_required_features)
536         return lit.Test.Result(Test.UNSUPPORTED,
537                                "Test requires the following features: %s" % msg)
538     unsupported_features = [f for f in unsupported
539                             if f in test.config.available_features]
540     if unsupported_features:
541         msg = ', '.join(unsupported_features)
542         return lit.Test.Result(Test.UNSUPPORTED,
543                     "Test is unsupported with the following features: %s" % msg)
544
545     unsupported_targets = [f for f in unsupported
546                            if f in test.suite.config.target_triple]
547     if unsupported_targets:
548       return lit.Test.Result(Test.UNSUPPORTED,
549                   "Test is unsupported with the following triple: %s" % (
550                       test.suite.config.target_triple,))
551
552     if test.config.limit_to_features:
553         # Check that we have one of the limit_to_features features in requires.
554         limit_to_features_tests = [f for f in test.config.limit_to_features
555                                    if f in requires]
556         if not limit_to_features_tests:
557             msg = ', '.join(test.config.limit_to_features)
558             return lit.Test.Result(Test.UNSUPPORTED,
559                  "Test requires one of the limit_to_features features %s" % msg)
560
561     return script
562
563 def _runShTest(test, litConfig, useExternalSh, script, tmpBase):
564     # Create the output directory if it does not already exist.
565     lit.util.mkdir_p(os.path.dirname(tmpBase))
566
567     execdir = os.path.dirname(test.getExecPath())
568     if useExternalSh:
569         res = executeScript(test, litConfig, tmpBase, script, execdir)
570     else:
571         res = executeScriptInternal(test, litConfig, tmpBase, script, execdir)
572     if isinstance(res, lit.Test.Result):
573         return res
574
575     out,err,exitCode = res
576     if exitCode == 0:
577         status = Test.PASS
578     else:
579         status = Test.FAIL
580
581     # Form the output log.
582     output = """Script:\n--\n%s\n--\nExit Code: %d\n\n""" % (
583         '\n'.join(script), exitCode)
584
585     # Append the outputs, if present.
586     if out:
587         output += """Command Output (stdout):\n--\n%s\n--\n""" % (out,)
588     if err:
589         output += """Command Output (stderr):\n--\n%s\n--\n""" % (err,)
590
591     return lit.Test.Result(status, output)
592
593
594 def executeShTest(test, litConfig, useExternalSh,
595                   extra_substitutions=[]):
596     if test.config.unsupported:
597         return (Test.UNSUPPORTED, 'Test is unsupported')
598
599     tmpDir, tmpBase = getTempPaths(test)
600     substitutions = list(extra_substitutions)
601     substitutions += getDefaultSubstitutions(test, tmpDir, tmpBase,
602                                              normalize_slashes=useExternalSh)
603     script = parseIntegratedTestScript(test, substitutions)
604     if isinstance(script, lit.Test.Result):
605         return script
606     if litConfig.noExecute:
607         return lit.Test.Result(Test.PASS)
608
609     # Re-run failed tests up to test_retry_attempts times.
610     attempts = 1
611     if hasattr(test.config, 'test_retry_attempts'):
612         attempts += test.config.test_retry_attempts
613     for i in range(attempts):
614         res = _runShTest(test, litConfig, useExternalSh, script, tmpBase)
615         if res.code != Test.FAIL:
616             break
617     # If we had to run the test more than once, count it as a flaky pass. These
618     # will be printed separately in the test summary.
619     if i > 0 and res.code == Test.PASS:
620         res.code = Test.FLAKYPASS
621     return res