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 # Update the cwd in the environment.
66 if len(cmd.commands[0].args) != 2:
67 raise ValueError('cd supports only one argument')
68 newdir = cmd.commands[0].args[1]
69 if os.path.isabs(newdir):
72 shenv.cwd = os.path.join(shenv.cwd, newdir)
76 input = subprocess.PIPE
80 # To avoid deadlock, we use a single stderr stream for piped
81 # output. This is null until we have seen some output using
83 for i,j in enumerate(cmd.commands):
84 # Apply the redirections, we use (N,) as a sentinel to indicate stdin,
85 # stdout, stderr for N equal to 0, 1, or 2 respectively. Redirects to or
86 # from a file are represented with a list [file, mode, file-object]
87 # where file-object is initially None.
88 redirects = [(0,), (1,), (2,)]
91 redirects[2] = [r[1], 'w', None]
92 elif r[0] == ('>>',2):
93 redirects[2] = [r[1], 'a', None]
94 elif r[0] == ('>&',2) and r[1] in '012':
95 redirects[2] = redirects[int(r[1])]
96 elif r[0] == ('>&',) or r[0] == ('&>',):
97 redirects[1] = redirects[2] = [r[1], 'w', None]
99 redirects[1] = [r[1], 'w', None]
100 elif r[0] == ('>>',):
101 redirects[1] = [r[1], 'a', None]
103 redirects[0] = [r[1], 'r', None]
105 raise InternalShellError(j,"Unsupported redirect: %r" % (r,))
107 # Map from the final redirections to something subprocess can handle.
109 for index,r in enumerate(redirects):
114 raise InternalShellError(j,"Unsupported redirect for stdin")
116 result = subprocess.PIPE
118 result = subprocess.STDOUT
121 raise InternalShellError(j,"Unsupported redirect on stdout")
122 result = subprocess.PIPE
125 if kAvoidDevNull and r[0] == '/dev/null':
126 r[2] = tempfile.TemporaryFile(mode=r[1])
128 # Make sure relative paths are relative to the cwd.
129 redir_filename = os.path.join(shenv.cwd, r[0])
130 r[2] = open(redir_filename, r[1])
131 # Workaround a Win32 and/or subprocess bug when appending.
133 # FIXME: Actually, this is probably an instance of PR6753.
136 opened_files.append(r[2])
138 final_redirects.append(result)
140 stdin, stdout, stderr = final_redirects
142 # If stderr wants to come from stdout, but stdout isn't a pipe, then put
143 # stderr on a pipe and treat it as stdout.
144 if (stderr == subprocess.STDOUT and stdout != subprocess.PIPE):
145 stderr = subprocess.PIPE
146 stderrIsStdout = True
148 stderrIsStdout = False
150 # Don't allow stderr on a PIPE except for the last
151 # process, this could deadlock.
153 # FIXME: This is slow, but so is deadlock.
154 if stderr == subprocess.PIPE and j != cmd.commands[-1]:
155 stderr = tempfile.TemporaryFile(mode='w+b')
156 stderrTempFiles.append((i, stderr))
158 # Resolve the executable path ourselves.
160 executable = lit.util.which(args[0], shenv.env['PATH'])
162 raise InternalShellError(j, '%r: command not found' % j.args[0])
164 # Replace uses of /dev/null with temporary files.
166 for i,arg in enumerate(args):
167 if arg == "/dev/null":
168 f = tempfile.NamedTemporaryFile(delete=False)
170 named_temp_files.append(f.name)
174 procs.append(subprocess.Popen(args, cwd=shenv.cwd,
175 executable = executable,
180 close_fds = kUseCloseFDs))
182 raise InternalShellError(j, 'Could not create process due to {}'.format(e))
184 # Immediately close stdin for any process taking stdin from us.
185 if stdin == subprocess.PIPE:
186 procs[-1].stdin.close()
187 procs[-1].stdin = None
189 # Update the current stdin source.
190 if stdout == subprocess.PIPE:
191 input = procs[-1].stdout
193 input = procs[-1].stderr
195 input = subprocess.PIPE
197 # Explicitly close any redirected files. We need to do this now because we
198 # need to release any handles we may have on the temporary files (important
199 # on Win32, for example). Since we have already spawned the subprocess, our
200 # handles have already been transferred so we do not need them anymore.
201 for f in opened_files:
204 # FIXME: There is probably still deadlock potential here. Yawn.
205 procData = [None] * len(procs)
206 procData[-1] = procs[-1].communicate()
208 for i in range(len(procs) - 1):
209 if procs[i].stdout is not None:
210 out = procs[i].stdout.read()
213 if procs[i].stderr is not None:
214 err = procs[i].stderr.read()
217 procData[i] = (out,err)
219 # Read stderr out of the temp files.
220 for i,f in stderrTempFiles:
222 procData[i] = (procData[i][0], f.read())
224 def to_string(bytes):
225 if isinstance(bytes, str):
227 return bytes.encode('utf-8')
230 for i,(out,err) in enumerate(procData):
231 res = procs[i].wait()
232 # Detect Ctrl-C in subprocess.
233 if res == -signal.SIGINT:
234 raise KeyboardInterrupt
236 # Ensure the resulting output is always of string type.
238 out = to_string(out.decode('utf-8'))
242 err = to_string(err.decode('utf-8'))
246 results.append((cmd.commands[i], out, err, res))
248 # Python treats the exit code as a signed char.
252 exitCode = min(exitCode, res)
254 exitCode = max(exitCode, res)
258 # Remove any named temporary files we created.
259 for f in named_temp_files:
266 exitCode = not exitCode
270 def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
274 cmds.append(ShUtil.ShParser(ln, litConfig.isWindows,
275 test.config.pipefail).parse())
277 return lit.Test.Result(Test.FAIL, "shell parser error on: %r" % ln)
281 cmd = ShUtil.Seq(cmd, '&&', c)
285 shenv = ShellEnvironment(cwd, test.config.environment)
286 exitCode = executeShCmd(cmd, shenv, results)
287 except InternalShellError:
288 e = sys.exc_info()[1]
290 results.append((e.command, '', e.message, exitCode))
293 for i,(cmd, cmd_out,cmd_err,res) in enumerate(results):
294 out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
295 out += 'Command %d Result: %r\n' % (i, res)
296 out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
297 out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
299 return out, err, exitCode
301 def executeScript(test, litConfig, tmpBase, commands, cwd):
302 bashPath = litConfig.getBashPath();
303 isWin32CMDEXE = (litConfig.isWindows and not bashPath)
304 script = tmpBase + '.script'
310 if litConfig.isWindows and not isWin32CMDEXE:
311 mode += 'b' # Avoid CRLFs when writing bash scripts.
312 f = open(script, mode)
314 f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
316 if test.config.pipefail:
317 f.write('set -o pipefail;')
318 f.write('{ ' + '; } &&\n{ '.join(commands) + '; }')
323 command = ['cmd','/c', script]
326 command = [bashPath, script]
328 command = ['/bin/sh', script]
329 if litConfig.useValgrind:
330 # FIXME: Running valgrind on sh is overkill. We probably could just
331 # run on clang with no real loss.
332 command = litConfig.valgrindArgs + command
334 return lit.util.executeCommand(command, cwd=cwd,
335 env=test.config.environment)
337 def parseIntegratedTestScriptCommands(source_path):
339 parseIntegratedTestScriptCommands(source_path) -> commands
341 Parse the commands in an integrated test script file into a list of
342 (line_number, command_type, line).
345 # This code is carefully written to be dual compatible with Python 2.5+ and
346 # Python 3 without requiring input files to always have valid codings. The
347 # trick we use is to open the file in binary mode and use the regular
348 # expression library to find the commands, with it scanning strings in
349 # Python2 and bytes in Python3.
351 # Once we find a match, we do require each script line to be decodable to
352 # UTF-8, so we convert the outputs to UTF-8 before returning. This way the
353 # remaining code can work with "strings" agnostic of the executing Python
356 keywords = ['RUN:', 'XFAIL:', 'REQUIRES:', 'UNSUPPORTED:', 'END.']
357 keywords_re = re.compile(
358 to_bytes("(%s)(.*)\n" % ("|".join(k for k in keywords),)))
360 f = open(source_path, 'rb')
362 # Read the entire file contents.
365 # Ensure the data ends with a newline.
366 if not data.endswith(to_bytes('\n')):
367 data = data + to_bytes('\n')
369 # Iterate over the matches.
371 last_match_position = 0
372 for match in keywords_re.finditer(data):
373 # Compute the updated line number by counting the intervening
375 match_position = match.start()
376 line_number += data.count(to_bytes('\n'), last_match_position,
378 last_match_position = match_position
380 # Convert the keyword and line to UTF-8 strings and yield the
381 # command. Note that we take care to return regular strings in
382 # Python 2, to avoid other code having to differentiate between the
383 # str and unicode types.
384 keyword,ln = match.groups()
385 yield (line_number, to_string(keyword[:-1].decode('utf-8')),
386 to_string(ln.decode('utf-8')))
391 def parseIntegratedTestScript(test, normalize_slashes=False,
392 extra_substitutions=[], require_script=True):
393 """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
394 script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES'
395 and 'UNSUPPORTED' information. The RUN lines also will have variable
396 substitution performed. If 'require_script' is False an empty script may be
397 returned. This can be used for test formats where the actual script is
401 # Get the temporary location, this is always relative to the test suite
402 # root, not test source root.
404 # FIXME: This should not be here?
405 sourcepath = test.getSourcePath()
406 sourcedir = os.path.dirname(sourcepath)
407 execpath = test.getExecPath()
408 execdir,execbase = os.path.split(execpath)
409 tmpDir = os.path.join(execdir, 'Output')
410 tmpBase = os.path.join(tmpDir, execbase)
412 # Normalize slashes, if requested.
413 if normalize_slashes:
414 sourcepath = sourcepath.replace('\\', '/')
415 sourcedir = sourcedir.replace('\\', '/')
416 tmpDir = tmpDir.replace('\\', '/')
417 tmpBase = tmpBase.replace('\\', '/')
419 # We use #_MARKER_# to hide %% while we do the other substitutions.
420 substitutions = list(extra_substitutions)
421 substitutions.extend([('%%', '#_MARKER_#')])
422 substitutions.extend(test.config.substitutions)
423 substitutions.extend([('%s', sourcepath),
426 ('%{pathsep}', os.pathsep),
427 ('%t', tmpBase + '.tmp'),
429 ('#_MARKER_#', '%')])
431 # "%/[STpst]" should be normalized.
432 substitutions.extend([
433 ('%/s', sourcepath.replace('\\', '/')),
434 ('%/S', sourcedir.replace('\\', '/')),
435 ('%/p', sourcedir.replace('\\', '/')),
436 ('%/t', tmpBase.replace('\\', '/') + '.tmp'),
437 ('%/T', tmpDir.replace('\\', '/')),
440 # Collect the test lines from the script.
444 for line_number, command_type, ln in \
445 parseIntegratedTestScriptCommands(sourcepath):
446 if command_type == 'RUN':
447 # Trim trailing whitespace.
450 # Substitute line number expressions
451 ln = re.sub('%\(line\)', str(line_number), ln)
452 def replace_line_number(match):
453 if match.group(1) == '+':
454 return str(line_number + int(match.group(2)))
455 if match.group(1) == '-':
456 return str(line_number - int(match.group(2)))
457 ln = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, ln)
459 # Collapse lines with trailing '\\'.
460 if script and script[-1][-1] == '\\':
461 script[-1] = script[-1][:-1] + ln
464 elif command_type == 'XFAIL':
465 test.xfails.extend([s.strip() for s in ln.split(',')])
466 elif command_type == 'REQUIRES':
467 requires.extend([s.strip() for s in ln.split(',')])
468 elif command_type == 'UNSUPPORTED':
469 unsupported.extend([s.strip() for s in ln.split(',')])
470 elif command_type == 'END':
471 # END commands are only honored if the rest of the line is empty.
475 raise ValueError("unknown script command type: %r" % (
478 # Apply substitutions to the script. Allow full regular
479 # expression syntax. Replace each matching occurrence of regular
480 # expression pattern a with substitution b in line ln.
482 # Apply substitutions
483 for a,b in substitutions:
485 b = b.replace("\\","\\\\")
486 ln = re.sub(a, b, ln)
488 # Strip the trailing newline and any extra whitespace.
490 script = [processLine(ln)
493 # Verify the script contains a run line.
494 if require_script and not script:
495 return lit.Test.Result(Test.UNRESOLVED, "Test has no run line!")
497 # Check for unterminated run lines.
498 if script and script[-1][-1] == '\\':
499 return lit.Test.Result(Test.UNRESOLVED,
500 "Test has unterminated run lines (with '\\')")
502 # Check that we have the required features:
503 missing_required_features = [f for f in requires
504 if f not in test.config.available_features]
505 if missing_required_features:
506 msg = ', '.join(missing_required_features)
507 return lit.Test.Result(Test.UNSUPPORTED,
508 "Test requires the following features: %s" % msg)
509 unsupported_features = [f for f in unsupported
510 if f in test.config.available_features]
511 if unsupported_features:
512 msg = ', '.join(unsupported_features)
513 return lit.Test.Result(Test.UNSUPPORTED,
514 "Test is unsupported with the following features: %s" % msg)
516 return script,tmpBase,execdir
518 def _runShTest(test, litConfig, useExternalSh,
519 script, tmpBase, execdir):
520 # Create the output directory if it does not already exist.
521 lit.util.mkdir_p(os.path.dirname(tmpBase))
524 res = executeScript(test, litConfig, tmpBase, script, execdir)
526 res = executeScriptInternal(test, litConfig, tmpBase, script, execdir)
527 if isinstance(res, lit.Test.Result):
530 out,err,exitCode = res
536 # Form the output log.
537 output = """Script:\n--\n%s\n--\nExit Code: %d\n\n""" % (
538 '\n'.join(script), exitCode)
540 # Append the outputs, if present.
542 output += """Command Output (stdout):\n--\n%s\n--\n""" % (out,)
544 output += """Command Output (stderr):\n--\n%s\n--\n""" % (err,)
546 return lit.Test.Result(status, output)
549 def executeShTest(test, litConfig, useExternalSh,
550 extra_substitutions=[]):
551 if test.config.unsupported:
552 return (Test.UNSUPPORTED, 'Test is unsupported')
554 res = parseIntegratedTestScript(test, useExternalSh, extra_substitutions)
555 if isinstance(res, lit.Test.Result):
557 if litConfig.noExecute:
558 return lit.Test.Result(Test.PASS)
560 script, tmpBase, execdir = res
561 return _runShTest(test, litConfig, useExternalSh, script, tmpBase, execdir)