1 from __future__ import absolute_import
2 import os, signal, subprocess, sys
7 import lit.ShUtil as ShUtil
8 import lit.Test as Test
10 from lit.util import to_bytes, to_string
12 class InternalShellError(Exception):
13 def __init__(self, command, message):
14 self.command = command
15 self.message = message
17 kIsWindows = platform.system() == 'Windows'
19 # Don't use close_fds on Windows.
20 kUseCloseFDs = not kIsWindows
22 # Use temporary files to replace /dev/null on Windows.
23 kAvoidDevNull = kIsWindows
25 class ShellEnvironment(object):
27 """Mutable shell environment containing things like CWD and env vars.
29 Environment variables are not implemented, but cwd tracking is.
32 def __init__(self, cwd, env):
36 def executeShCmd(cmd, shenv, results):
37 if isinstance(cmd, ShUtil.Seq):
39 res = executeShCmd(cmd.lhs, shenv, results)
40 return executeShCmd(cmd.rhs, shenv, results)
43 raise InternalShellError(cmd,"unsupported shell operator: '&'")
46 res = executeShCmd(cmd.lhs, shenv, results)
48 res = executeShCmd(cmd.rhs, shenv, results)
52 res = executeShCmd(cmd.lhs, shenv, results)
57 res = executeShCmd(cmd.rhs, shenv, results)
60 raise ValueError('Unknown shell command: %r' % cmd.op)
61 assert isinstance(cmd, ShUtil.Pipeline)
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):
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.
80 input = subprocess.PIPE
84 # To avoid deadlock, we use a single stderr stream for piped
85 # output. This is null until we have seen some output using
87 for i,j in enumerate(cmd.commands):
88 # Reference the global environment by default.
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)
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.
102 cmd_shenv.env[key] = val
103 j.args = j.args[arg_idx+1:]
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:
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]
120 redirects[1] = [r[1], 'w', None]
121 elif r[0] == ('>>',):
122 redirects[1] = [r[1], 'a', None]
124 redirects[0] = [r[1], 'r', None]
126 raise InternalShellError(j,"Unsupported redirect: %r" % (r,))
128 # Map from the final redirections to something subprocess can handle.
130 for index,r in enumerate(redirects):
135 raise InternalShellError(j,"Unsupported redirect for stdin")
137 result = subprocess.PIPE
139 result = subprocess.STDOUT
142 raise InternalShellError(j,"Unsupported redirect on stdout")
143 result = subprocess.PIPE
146 if kAvoidDevNull and r[0] == '/dev/null':
147 r[2] = tempfile.TemporaryFile(mode=r[1])
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.
154 # FIXME: Actually, this is probably an instance of PR6753.
157 opened_files.append(r[2])
159 final_redirects.append(result)
161 stdin, stdout, stderr = final_redirects
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
169 stderrIsStdout = False
171 # Don't allow stderr on a PIPE except for the last
172 # process, this could deadlock.
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))
179 # Resolve the executable path ourselves.
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
188 executable = lit.util.which(args[0], cmd_shenv.env['PATH'])
190 raise InternalShellError(j, '%r: command not found' % j.args[0])
192 # Replace uses of /dev/null with temporary files.
194 for i,arg in enumerate(args):
195 if arg == "/dev/null":
196 f = tempfile.NamedTemporaryFile(delete=False)
198 named_temp_files.append(f.name)
202 procs.append(subprocess.Popen(args, cwd=cmd_shenv.cwd,
203 executable = executable,
208 close_fds = kUseCloseFDs))
210 raise InternalShellError(j, 'Could not create process due to {}'.format(e))
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
217 # Update the current stdin source.
218 if stdout == subprocess.PIPE:
219 input = procs[-1].stdout
221 input = procs[-1].stderr
223 input = subprocess.PIPE
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:
232 # FIXME: There is probably still deadlock potential here. Yawn.
233 procData = [None] * len(procs)
234 procData[-1] = procs[-1].communicate()
236 for i in range(len(procs) - 1):
237 if procs[i].stdout is not None:
238 out = procs[i].stdout.read()
241 if procs[i].stderr is not None:
242 err = procs[i].stderr.read()
245 procData[i] = (out,err)
247 # Read stderr out of the temp files.
248 for i,f in stderrTempFiles:
250 procData[i] = (procData[i][0], f.read())
252 def to_string(bytes):
253 if isinstance(bytes, str):
255 return bytes.encode('utf-8')
258 for i,(out,err) in enumerate(procData):
259 res = procs[i].wait()
260 # On Windows, manually close the process handles.
262 procs[i]._handle.Close()
263 # Detect Ctrl-C in subprocess.
264 if res == -signal.SIGINT:
265 raise KeyboardInterrupt
267 # Ensure the resulting output is always of string type.
269 out = to_string(out.decode('utf-8'))
273 err = to_string(err.decode('utf-8'))
277 results.append((cmd.commands[i], out, err, res))
279 # Python treats the exit code as a signed char.
283 exitCode = min(exitCode, res)
285 exitCode = max(exitCode, res)
289 # Remove any named temporary files we created.
290 for f in named_temp_files:
297 exitCode = not exitCode
301 def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
305 cmds.append(ShUtil.ShParser(ln, litConfig.isWindows,
306 test.config.pipefail).parse())
308 return lit.Test.Result(Test.FAIL, "shell parser error on: %r" % ln)
312 cmd = ShUtil.Seq(cmd, '&&', c)
316 shenv = ShellEnvironment(cwd, test.config.environment)
317 exitCode = executeShCmd(cmd, shenv, results)
318 except InternalShellError:
319 e = sys.exc_info()[1]
321 results.append((e.command, '', e.message, exitCode))
324 for i,(cmd, cmd_out,cmd_err,res) in enumerate(results):
325 out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
326 out += 'Command %d Result: %r\n' % (i, res)
327 out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
328 out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
330 return out, err, exitCode
332 def executeScript(test, litConfig, tmpBase, commands, cwd):
333 bashPath = litConfig.getBashPath();
334 isWin32CMDEXE = (litConfig.isWindows and not bashPath)
335 script = tmpBase + '.script'
341 if litConfig.isWindows and not isWin32CMDEXE:
342 mode += 'b' # Avoid CRLFs when writing bash scripts.
343 f = open(script, mode)
345 f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
347 if test.config.pipefail:
348 f.write('set -o pipefail;')
349 f.write('{ ' + '; } &&\n{ '.join(commands) + '; }')
354 command = ['cmd','/c', script]
357 command = [bashPath, script]
359 command = ['/bin/sh', script]
360 if litConfig.useValgrind:
361 # FIXME: Running valgrind on sh is overkill. We probably could just
362 # run on clang with no real loss.
363 command = litConfig.valgrindArgs + command
365 return lit.util.executeCommand(command, cwd=cwd,
366 env=test.config.environment)
368 def parseIntegratedTestScriptCommands(source_path):
370 parseIntegratedTestScriptCommands(source_path) -> commands
372 Parse the commands in an integrated test script file into a list of
373 (line_number, command_type, line).
376 # This code is carefully written to be dual compatible with Python 2.5+ and
377 # Python 3 without requiring input files to always have valid codings. The
378 # trick we use is to open the file in binary mode and use the regular
379 # expression library to find the commands, with it scanning strings in
380 # Python2 and bytes in Python3.
382 # Once we find a match, we do require each script line to be decodable to
383 # UTF-8, so we convert the outputs to UTF-8 before returning. This way the
384 # remaining code can work with "strings" agnostic of the executing Python
387 keywords = ['RUN:', 'XFAIL:', 'REQUIRES:', 'UNSUPPORTED:', 'END.']
388 keywords_re = re.compile(
389 to_bytes("(%s)(.*)\n" % ("|".join(k for k in keywords),)))
391 f = open(source_path, 'rb')
393 # Read the entire file contents.
396 # Ensure the data ends with a newline.
397 if not data.endswith(to_bytes('\n')):
398 data = data + to_bytes('\n')
400 # Iterate over the matches.
402 last_match_position = 0
403 for match in keywords_re.finditer(data):
404 # Compute the updated line number by counting the intervening
406 match_position = match.start()
407 line_number += data.count(to_bytes('\n'), last_match_position,
409 last_match_position = match_position
411 # Convert the keyword and line to UTF-8 strings and yield the
412 # command. Note that we take care to return regular strings in
413 # Python 2, to avoid other code having to differentiate between the
414 # str and unicode types.
415 keyword,ln = match.groups()
416 yield (line_number, to_string(keyword[:-1].decode('utf-8')),
417 to_string(ln.decode('utf-8')))
422 def parseIntegratedTestScript(test, normalize_slashes=False,
423 extra_substitutions=[], require_script=True):
424 """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
425 script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES'
426 and 'UNSUPPORTED' information. The RUN lines also will have variable
427 substitution performed. If 'require_script' is False an empty script may be
428 returned. This can be used for test formats where the actual script is
432 # Get the temporary location, this is always relative to the test suite
433 # root, not test source root.
435 # FIXME: This should not be here?
436 sourcepath = test.getSourcePath()
437 sourcedir = os.path.dirname(sourcepath)
438 execpath = test.getExecPath()
439 execdir,execbase = os.path.split(execpath)
440 tmpDir = os.path.join(execdir, 'Output')
441 tmpBase = os.path.join(tmpDir, execbase)
443 # Normalize slashes, if requested.
444 if normalize_slashes:
445 sourcepath = sourcepath.replace('\\', '/')
446 sourcedir = sourcedir.replace('\\', '/')
447 tmpDir = tmpDir.replace('\\', '/')
448 tmpBase = tmpBase.replace('\\', '/')
450 # We use #_MARKER_# to hide %% while we do the other substitutions.
451 substitutions = list(extra_substitutions)
452 substitutions.extend([('%%', '#_MARKER_#')])
453 substitutions.extend(test.config.substitutions)
454 substitutions.extend([('%s', sourcepath),
457 ('%{pathsep}', os.pathsep),
458 ('%t', tmpBase + '.tmp'),
460 ('#_MARKER_#', '%')])
462 # "%/[STpst]" should be normalized.
463 substitutions.extend([
464 ('%/s', sourcepath.replace('\\', '/')),
465 ('%/S', sourcedir.replace('\\', '/')),
466 ('%/p', sourcedir.replace('\\', '/')),
467 ('%/t', tmpBase.replace('\\', '/') + '.tmp'),
468 ('%/T', tmpDir.replace('\\', '/')),
471 # Collect the test lines from the script.
475 for line_number, command_type, ln in \
476 parseIntegratedTestScriptCommands(sourcepath):
477 if command_type == 'RUN':
478 # Trim trailing whitespace.
481 # Substitute line number expressions
482 ln = re.sub('%\(line\)', str(line_number), ln)
483 def replace_line_number(match):
484 if match.group(1) == '+':
485 return str(line_number + int(match.group(2)))
486 if match.group(1) == '-':
487 return str(line_number - int(match.group(2)))
488 ln = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, ln)
490 # Collapse lines with trailing '\\'.
491 if script and script[-1][-1] == '\\':
492 script[-1] = script[-1][:-1] + ln
495 elif command_type == 'XFAIL':
496 test.xfails.extend([s.strip() for s in ln.split(',')])
497 elif command_type == 'REQUIRES':
498 requires.extend([s.strip() for s in ln.split(',')])
499 elif command_type == 'UNSUPPORTED':
500 unsupported.extend([s.strip() for s in ln.split(',')])
501 elif command_type == 'END':
502 # END commands are only honored if the rest of the line is empty.
506 raise ValueError("unknown script command type: %r" % (
509 # Apply substitutions to the script. Allow full regular
510 # expression syntax. Replace each matching occurrence of regular
511 # expression pattern a with substitution b in line ln.
513 # Apply substitutions
514 for a,b in substitutions:
516 b = b.replace("\\","\\\\")
517 ln = re.sub(a, b, ln)
519 # Strip the trailing newline and any extra whitespace.
521 script = [processLine(ln)
524 # Verify the script contains a run line.
525 if require_script and not script:
526 return lit.Test.Result(Test.UNRESOLVED, "Test has no run line!")
528 # Check for unterminated run lines.
529 if script and script[-1][-1] == '\\':
530 return lit.Test.Result(Test.UNRESOLVED,
531 "Test has unterminated run lines (with '\\')")
533 # Check that we have the required features:
534 missing_required_features = [f for f in requires
535 if f not in test.config.available_features]
536 if missing_required_features:
537 msg = ', '.join(missing_required_features)
538 return lit.Test.Result(Test.UNSUPPORTED,
539 "Test requires the following features: %s" % msg)
540 unsupported_features = [f for f in unsupported
541 if f in test.config.available_features]
542 if unsupported_features:
543 msg = ', '.join(unsupported_features)
544 return lit.Test.Result(Test.UNSUPPORTED,
545 "Test is unsupported with the following features: %s" % msg)
547 if test.config.limit_to_features:
548 # Check that we have one of the limit_to_features features in requires.
549 limit_to_features_tests = [f for f in test.config.limit_to_features
551 if not limit_to_features_tests:
552 msg = ', '.join(test.config.limit_to_features)
553 return lit.Test.Result(Test.UNSUPPORTED,
554 "Test requires one of the limit_to_features features %s" % msg)
556 return script,tmpBase,execdir
558 def _runShTest(test, litConfig, useExternalSh,
559 script, tmpBase, execdir):
560 # Create the output directory if it does not already exist.
561 lit.util.mkdir_p(os.path.dirname(tmpBase))
564 res = executeScript(test, litConfig, tmpBase, script, execdir)
566 res = executeScriptInternal(test, litConfig, tmpBase, script, execdir)
567 if isinstance(res, lit.Test.Result):
570 out,err,exitCode = res
576 # Form the output log.
577 output = """Script:\n--\n%s\n--\nExit Code: %d\n\n""" % (
578 '\n'.join(script), exitCode)
580 # Append the outputs, if present.
582 output += """Command Output (stdout):\n--\n%s\n--\n""" % (out,)
584 output += """Command Output (stderr):\n--\n%s\n--\n""" % (err,)
586 return lit.Test.Result(status, output)
589 def executeShTest(test, litConfig, useExternalSh,
590 extra_substitutions=[]):
591 if test.config.unsupported:
592 return (Test.UNSUPPORTED, 'Test is unsupported')
594 res = parseIntegratedTestScript(test, useExternalSh, extra_substitutions)
595 if isinstance(res, lit.Test.Result):
597 if litConfig.noExecute:
598 return lit.Test.Result(Test.PASS)
600 script, tmpBase, execdir = res
601 return _runShTest(test, litConfig, useExternalSh, script, tmpBase, execdir)