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 def executeShCmd(cmd, cfg, cwd, results):
26 if isinstance(cmd, ShUtil.Seq):
28 res = executeShCmd(cmd.lhs, cfg, cwd, results)
29 return executeShCmd(cmd.rhs, cfg, cwd, results)
32 raise InternalShellError(cmd,"unsupported shell operator: '&'")
35 res = executeShCmd(cmd.lhs, cfg, cwd, results)
37 res = executeShCmd(cmd.rhs, cfg, cwd, results)
41 res = executeShCmd(cmd.lhs, cfg, cwd, results)
46 res = executeShCmd(cmd.rhs, cfg, cwd, results)
49 raise ValueError('Unknown shell command: %r' % cmd.op)
51 assert isinstance(cmd, ShUtil.Pipeline)
53 input = subprocess.PIPE
57 # To avoid deadlock, we use a single stderr stream for piped
58 # output. This is null until we have seen some output using
60 for i,j in enumerate(cmd.commands):
61 # Apply the redirections, we use (N,) as a sentinel to indicate stdin,
62 # stdout, stderr for N equal to 0, 1, or 2 respectively. Redirects to or
63 # from a file are represented with a list [file, mode, file-object]
64 # where file-object is initially None.
65 redirects = [(0,), (1,), (2,)]
68 redirects[2] = [r[1], 'w', None]
69 elif r[0] == ('>>',2):
70 redirects[2] = [r[1], 'a', None]
71 elif r[0] == ('>&',2) and r[1] in '012':
72 redirects[2] = redirects[int(r[1])]
73 elif r[0] == ('>&',) or r[0] == ('&>',):
74 redirects[1] = redirects[2] = [r[1], 'w', None]
76 redirects[1] = [r[1], 'w', None]
78 redirects[1] = [r[1], 'a', None]
80 redirects[0] = [r[1], 'r', None]
82 raise InternalShellError(j,"Unsupported redirect: %r" % (r,))
84 # Map from the final redirections to something subprocess can handle.
86 for index,r in enumerate(redirects):
91 raise InternalShellError(j,"Unsupported redirect for stdin")
93 result = subprocess.PIPE
95 result = subprocess.STDOUT
98 raise InternalShellError(j,"Unsupported redirect on stdout")
99 result = subprocess.PIPE
102 if kAvoidDevNull and r[0] == '/dev/null':
103 r[2] = tempfile.TemporaryFile(mode=r[1])
105 r[2] = open(r[0], r[1])
106 # Workaround a Win32 and/or subprocess bug when appending.
108 # FIXME: Actually, this is probably an instance of PR6753.
111 opened_files.append(r[2])
113 final_redirects.append(result)
115 stdin, stdout, stderr = final_redirects
117 # If stderr wants to come from stdout, but stdout isn't a pipe, then put
118 # stderr on a pipe and treat it as stdout.
119 if (stderr == subprocess.STDOUT and stdout != subprocess.PIPE):
120 stderr = subprocess.PIPE
121 stderrIsStdout = True
123 stderrIsStdout = False
125 # Don't allow stderr on a PIPE except for the last
126 # process, this could deadlock.
128 # FIXME: This is slow, but so is deadlock.
129 if stderr == subprocess.PIPE and j != cmd.commands[-1]:
130 stderr = tempfile.TemporaryFile(mode='w+b')
131 stderrTempFiles.append((i, stderr))
133 # Resolve the executable path ourselves.
135 executable = lit.util.which(args[0], cfg.environment['PATH'])
137 raise InternalShellError(j, '%r: command not found' % j.args[0])
139 # Replace uses of /dev/null with temporary files.
141 for i,arg in enumerate(args):
142 if arg == "/dev/null":
143 f = tempfile.NamedTemporaryFile(delete=False)
145 named_temp_files.append(f.name)
149 procs.append(subprocess.Popen(args, cwd=cwd,
150 executable = executable,
154 env = cfg.environment,
155 close_fds = kUseCloseFDs))
157 raise InternalShellError(j, 'Could not create process due to {}'.format(e))
159 # Immediately close stdin for any process taking stdin from us.
160 if stdin == subprocess.PIPE:
161 procs[-1].stdin.close()
162 procs[-1].stdin = None
164 # Update the current stdin source.
165 if stdout == subprocess.PIPE:
166 input = procs[-1].stdout
168 input = procs[-1].stderr
170 input = subprocess.PIPE
172 # Explicitly close any redirected files. We need to do this now because we
173 # need to release any handles we may have on the temporary files (important
174 # on Win32, for example). Since we have already spawned the subprocess, our
175 # handles have already been transferred so we do not need them anymore.
176 for f in opened_files:
179 # FIXME: There is probably still deadlock potential here. Yawn.
180 procData = [None] * len(procs)
181 procData[-1] = procs[-1].communicate()
183 for i in range(len(procs) - 1):
184 if procs[i].stdout is not None:
185 out = procs[i].stdout.read()
188 if procs[i].stderr is not None:
189 err = procs[i].stderr.read()
192 procData[i] = (out,err)
194 # Read stderr out of the temp files.
195 for i,f in stderrTempFiles:
197 procData[i] = (procData[i][0], f.read())
199 def to_string(bytes):
200 if isinstance(bytes, str):
202 return bytes.encode('utf-8')
205 for i,(out,err) in enumerate(procData):
206 res = procs[i].wait()
207 # Detect Ctrl-C in subprocess.
208 if res == -signal.SIGINT:
209 raise KeyboardInterrupt
211 # Ensure the resulting output is always of string type.
213 out = to_string(out.decode('utf-8'))
217 err = to_string(err.decode('utf-8'))
221 results.append((cmd.commands[i], out, err, res))
223 # Python treats the exit code as a signed char.
227 exitCode = min(exitCode, res)
229 exitCode = max(exitCode, res)
233 # Remove any named temporary files we created.
234 for f in named_temp_files:
241 exitCode = not exitCode
245 def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
249 cmds.append(ShUtil.ShParser(ln, litConfig.isWindows,
250 test.config.pipefail).parse())
252 return lit.Test.Result(Test.FAIL, "shell parser error on: %r" % ln)
256 cmd = ShUtil.Seq(cmd, '&&', c)
260 exitCode = executeShCmd(cmd, test.config, cwd, results)
261 except InternalShellError:
262 e = sys.exc_info()[1]
264 results.append((e.command, '', e.message, exitCode))
267 for i,(cmd, cmd_out,cmd_err,res) in enumerate(results):
268 out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
269 out += 'Command %d Result: %r\n' % (i, res)
270 out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
271 out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
273 return out, err, exitCode
275 def executeScript(test, litConfig, tmpBase, commands, cwd):
276 bashPath = litConfig.getBashPath();
277 isWin32CMDEXE = (litConfig.isWindows and not bashPath)
278 script = tmpBase + '.script'
284 if litConfig.isWindows and not isWin32CMDEXE:
285 mode += 'b' # Avoid CRLFs when writing bash scripts.
286 f = open(script, mode)
288 f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
290 if test.config.pipefail:
291 f.write('set -o pipefail;')
292 f.write('{ ' + '; } &&\n{ '.join(commands) + '; }')
297 command = ['cmd','/c', script]
300 command = [bashPath, script]
302 command = ['/bin/sh', script]
303 if litConfig.useValgrind:
304 # FIXME: Running valgrind on sh is overkill. We probably could just
305 # run on clang with no real loss.
306 command = litConfig.valgrindArgs + command
308 return lit.util.executeCommand(command, cwd=cwd,
309 env=test.config.environment)
311 def parseIntegratedTestScriptCommands(source_path):
313 parseIntegratedTestScriptCommands(source_path) -> commands
315 Parse the commands in an integrated test script file into a list of
316 (line_number, command_type, line).
319 # This code is carefully written to be dual compatible with Python 2.5+ and
320 # Python 3 without requiring input files to always have valid codings. The
321 # trick we use is to open the file in binary mode and use the regular
322 # expression library to find the commands, with it scanning strings in
323 # Python2 and bytes in Python3.
325 # Once we find a match, we do require each script line to be decodable to
326 # UTF-8, so we convert the outputs to UTF-8 before returning. This way the
327 # remaining code can work with "strings" agnostic of the executing Python
330 keywords = ['RUN:', 'XFAIL:', 'REQUIRES:', 'UNSUPPORTED:', 'END.']
331 keywords_re = re.compile(
332 to_bytes("(%s)(.*)\n" % ("|".join(k for k in keywords),)))
334 f = open(source_path, 'rb')
336 # Read the entire file contents.
339 # Ensure the data ends with a newline.
340 if not data.endswith(to_bytes('\n')):
341 data = data + to_bytes('\n')
343 # Iterate over the matches.
345 last_match_position = 0
346 for match in keywords_re.finditer(data):
347 # Compute the updated line number by counting the intervening
349 match_position = match.start()
350 line_number += data.count(to_bytes('\n'), last_match_position,
352 last_match_position = match_position
354 # Convert the keyword and line to UTF-8 strings and yield the
355 # command. Note that we take care to return regular strings in
356 # Python 2, to avoid other code having to differentiate between the
357 # str and unicode types.
358 keyword,ln = match.groups()
359 yield (line_number, to_string(keyword[:-1].decode('utf-8')),
360 to_string(ln.decode('utf-8')))
365 def parseIntegratedTestScript(test, normalize_slashes=False,
366 extra_substitutions=[], require_script=True):
367 """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
368 script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES'
369 and 'UNSUPPORTED' information. The RUN lines also will have variable
370 substitution performed. If 'require_script' is False an empty script may be
371 returned. This can be used for test formats where the actual script is
375 # Get the temporary location, this is always relative to the test suite
376 # root, not test source root.
378 # FIXME: This should not be here?
379 sourcepath = test.getSourcePath()
380 sourcedir = os.path.dirname(sourcepath)
381 execpath = test.getExecPath()
382 execdir,execbase = os.path.split(execpath)
383 tmpDir = os.path.join(execdir, 'Output')
384 tmpBase = os.path.join(tmpDir, execbase)
386 # Normalize slashes, if requested.
387 if normalize_slashes:
388 sourcepath = sourcepath.replace('\\', '/')
389 sourcedir = sourcedir.replace('\\', '/')
390 tmpDir = tmpDir.replace('\\', '/')
391 tmpBase = tmpBase.replace('\\', '/')
393 # We use #_MARKER_# to hide %% while we do the other substitutions.
394 substitutions = list(extra_substitutions)
395 substitutions.extend([('%%', '#_MARKER_#')])
396 substitutions.extend(test.config.substitutions)
397 substitutions.extend([('%s', sourcepath),
400 ('%{pathsep}', os.pathsep),
401 ('%t', tmpBase + '.tmp'),
403 ('#_MARKER_#', '%')])
405 # "%/[STpst]" should be normalized.
406 substitutions.extend([
407 ('%/s', sourcepath.replace('\\', '/')),
408 ('%/S', sourcedir.replace('\\', '/')),
409 ('%/p', sourcedir.replace('\\', '/')),
410 ('%/t', tmpBase.replace('\\', '/') + '.tmp'),
411 ('%/T', tmpDir.replace('\\', '/')),
414 # Collect the test lines from the script.
418 for line_number, command_type, ln in \
419 parseIntegratedTestScriptCommands(sourcepath):
420 if command_type == 'RUN':
421 # Trim trailing whitespace.
424 # Substitute line number expressions
425 ln = re.sub('%\(line\)', str(line_number), ln)
426 def replace_line_number(match):
427 if match.group(1) == '+':
428 return str(line_number + int(match.group(2)))
429 if match.group(1) == '-':
430 return str(line_number - int(match.group(2)))
431 ln = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, ln)
433 # Collapse lines with trailing '\\'.
434 if script and script[-1][-1] == '\\':
435 script[-1] = script[-1][:-1] + ln
438 elif command_type == 'XFAIL':
439 test.xfails.extend([s.strip() for s in ln.split(',')])
440 elif command_type == 'REQUIRES':
441 requires.extend([s.strip() for s in ln.split(',')])
442 elif command_type == 'UNSUPPORTED':
443 unsupported.extend([s.strip() for s in ln.split(',')])
444 elif command_type == 'END':
445 # END commands are only honored if the rest of the line is empty.
449 raise ValueError("unknown script command type: %r" % (
452 # Apply substitutions to the script. Allow full regular
453 # expression syntax. Replace each matching occurrence of regular
454 # expression pattern a with substitution b in line ln.
456 # Apply substitutions
457 for a,b in substitutions:
459 b = b.replace("\\","\\\\")
460 ln = re.sub(a, b, ln)
462 # Strip the trailing newline and any extra whitespace.
464 script = [processLine(ln)
467 # Verify the script contains a run line.
468 if require_script and not script:
469 return lit.Test.Result(Test.UNRESOLVED, "Test has no run line!")
471 # Check for unterminated run lines.
472 if script and script[-1][-1] == '\\':
473 return lit.Test.Result(Test.UNRESOLVED,
474 "Test has unterminated run lines (with '\\')")
476 # Check that we have the required features:
477 missing_required_features = [f for f in requires
478 if f not in test.config.available_features]
479 if missing_required_features:
480 msg = ', '.join(missing_required_features)
481 return lit.Test.Result(Test.UNSUPPORTED,
482 "Test requires the following features: %s" % msg)
483 unsupported_features = [f for f in unsupported
484 if f in test.config.available_features]
485 if unsupported_features:
486 msg = ', '.join(unsupported_features)
487 return lit.Test.Result(Test.UNSUPPORTED,
488 "Test is unsupported with the following features: %s" % msg)
490 return script,tmpBase,execdir
492 def _runShTest(test, litConfig, useExternalSh,
493 script, tmpBase, execdir):
494 # Create the output directory if it does not already exist.
495 lit.util.mkdir_p(os.path.dirname(tmpBase))
498 res = executeScript(test, litConfig, tmpBase, script, execdir)
500 res = executeScriptInternal(test, litConfig, tmpBase, script, execdir)
501 if isinstance(res, lit.Test.Result):
504 out,err,exitCode = res
510 # Form the output log.
511 output = """Script:\n--\n%s\n--\nExit Code: %d\n\n""" % (
512 '\n'.join(script), exitCode)
514 # Append the outputs, if present.
516 output += """Command Output (stdout):\n--\n%s\n--\n""" % (out,)
518 output += """Command Output (stderr):\n--\n%s\n--\n""" % (err,)
520 return lit.Test.Result(status, output)
523 def executeShTest(test, litConfig, useExternalSh,
524 extra_substitutions=[]):
525 if test.config.unsupported:
526 return (Test.UNSUPPORTED, 'Test is unsupported')
528 res = parseIntegratedTestScript(test, useExternalSh, extra_substitutions)
529 if isinstance(res, lit.Test.Result):
531 if litConfig.noExecute:
532 return lit.Test.Result(Test.PASS)
534 script, tmpBase, execdir = res
535 return _runShTest(test, litConfig, useExternalSh, script, tmpBase, execdir)