1 from __future__ import absolute_import
2 import os, signal, subprocess, sys
7 from io import StringIO
9 from StringIO import StringIO
11 import lit.ShUtil as ShUtil
12 import lit.Test as Test
15 class InternalShellError(Exception):
16 def __init__(self, command, message):
17 self.command = command
18 self.message = message
20 kIsWindows = platform.system() == 'Windows'
22 # Don't use close_fds on Windows.
23 kUseCloseFDs = not kIsWindows
25 # Use temporary files to replace /dev/null on Windows.
26 kAvoidDevNull = kIsWindows
28 def executeShCmd(cmd, cfg, cwd, results):
29 if isinstance(cmd, ShUtil.Seq):
31 res = executeShCmd(cmd.lhs, cfg, cwd, results)
32 return executeShCmd(cmd.rhs, cfg, cwd, results)
35 raise InternalShellError(cmd,"unsupported shell operator: '&'")
38 res = executeShCmd(cmd.lhs, cfg, cwd, results)
40 res = executeShCmd(cmd.rhs, cfg, cwd, results)
44 res = executeShCmd(cmd.lhs, cfg, cwd, results)
49 res = executeShCmd(cmd.rhs, cfg, cwd, results)
52 raise ValueError('Unknown shell command: %r' % cmd.op)
54 assert isinstance(cmd, ShUtil.Pipeline)
56 input = subprocess.PIPE
60 # To avoid deadlock, we use a single stderr stream for piped
61 # output. This is null until we have seen some output using
63 for i,j in enumerate(cmd.commands):
64 # Apply the redirections, we use (N,) as a sentinel to indicate stdin,
65 # stdout, stderr for N equal to 0, 1, or 2 respectively. Redirects to or
66 # from a file are represented with a list [file, mode, file-object]
67 # where file-object is initially None.
68 redirects = [(0,), (1,), (2,)]
71 redirects[2] = [r[1], 'w', None]
72 elif r[0] == ('>>',2):
73 redirects[2] = [r[1], 'a', None]
74 elif r[0] == ('>&',2) and r[1] in '012':
75 redirects[2] = redirects[int(r[1])]
76 elif r[0] == ('>&',) or r[0] == ('&>',):
77 redirects[1] = redirects[2] = [r[1], 'w', None]
79 redirects[1] = [r[1], 'w', None]
81 redirects[1] = [r[1], 'a', None]
83 redirects[0] = [r[1], 'r', None]
85 raise InternalShellError(j,"Unsupported redirect: %r" % (r,))
87 # Map from the final redirections to something subprocess can handle.
89 for index,r in enumerate(redirects):
94 raise InternalShellError(j,"Unsupported redirect for stdin")
96 result = subprocess.PIPE
98 result = subprocess.STDOUT
101 raise InternalShellError(j,"Unsupported redirect on stdout")
102 result = subprocess.PIPE
105 if kAvoidDevNull and r[0] == '/dev/null':
106 r[2] = tempfile.TemporaryFile(mode=r[1])
108 r[2] = open(r[0], r[1])
109 # Workaround a Win32 and/or subprocess bug when appending.
111 # FIXME: Actually, this is probably an instance of PR6753.
114 opened_files.append(r[2])
116 final_redirects.append(result)
118 stdin, stdout, stderr = final_redirects
120 # If stderr wants to come from stdout, but stdout isn't a pipe, then put
121 # stderr on a pipe and treat it as stdout.
122 if (stderr == subprocess.STDOUT and stdout != subprocess.PIPE):
123 stderr = subprocess.PIPE
124 stderrIsStdout = True
126 stderrIsStdout = False
128 # Don't allow stderr on a PIPE except for the last
129 # process, this could deadlock.
131 # FIXME: This is slow, but so is deadlock.
132 if stderr == subprocess.PIPE and j != cmd.commands[-1]:
133 stderr = tempfile.TemporaryFile(mode='w+b')
134 stderrTempFiles.append((i, stderr))
136 # Resolve the executable path ourselves.
138 args[0] = lit.util.which(args[0], cfg.environment['PATH'])
140 raise InternalShellError(j, '%r: command not found' % j.args[0])
142 # Replace uses of /dev/null with temporary files.
144 for i,arg in enumerate(args):
145 if arg == "/dev/null":
146 f = tempfile.NamedTemporaryFile(delete=False)
148 named_temp_files.append(f.name)
151 procs.append(subprocess.Popen(args, cwd=cwd,
155 env = cfg.environment,
156 close_fds = kUseCloseFDs))
158 # Immediately close stdin for any process taking stdin from us.
159 if stdin == subprocess.PIPE:
160 procs[-1].stdin.close()
161 procs[-1].stdin = None
163 # Update the current stdin source.
164 if stdout == subprocess.PIPE:
165 input = procs[-1].stdout
167 input = procs[-1].stderr
169 input = subprocess.PIPE
171 # Explicitly close any redirected files. We need to do this now because we
172 # need to release any handles we may have on the temporary files (important
173 # on Win32, for example). Since we have already spawned the subprocess, our
174 # handles have already been transferred so we do not need them anymore.
175 for f in opened_files:
178 # FIXME: There is probably still deadlock potential here. Yawn.
179 procData = [None] * len(procs)
180 procData[-1] = procs[-1].communicate()
182 for i in range(len(procs) - 1):
183 if procs[i].stdout is not None:
184 out = procs[i].stdout.read()
187 if procs[i].stderr is not None:
188 err = procs[i].stderr.read()
191 procData[i] = (out,err)
193 # Read stderr out of the temp files.
194 for i,f in stderrTempFiles:
196 procData[i] = (procData[i][0], f.read())
199 for i,(out,err) in enumerate(procData):
200 res = procs[i].wait()
201 # Detect Ctrl-C in subprocess.
202 if res == -signal.SIGINT:
203 raise KeyboardInterrupt
205 results.append((cmd.commands[i], out, err, res))
207 # Python treats the exit code as a signed char.
211 exitCode = min(exitCode, res)
213 exitCode = max(exitCode, res)
217 # Remove any named temporary files we created.
218 for f in named_temp_files:
225 exitCode = not exitCode
229 def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
233 cmds.append(ShUtil.ShParser(ln, litConfig.isWindows,
234 test.config.pipefail).parse())
236 return (Test.FAIL, "shell parser error on: %r" % ln)
240 cmd = ShUtil.Seq(cmd, '&&', c)
244 exitCode = executeShCmd(cmd, test.config, cwd, results)
245 except InternalShellError:
246 e = sys.exc_info()[1]
248 results.append((e.command, '', e.message, exitCode))
251 for i,(cmd, cmd_out,cmd_err,res) in enumerate(results):
252 out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
253 out += 'Command %d Result: %r\n' % (i, res)
254 out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
255 out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
257 return out, err, exitCode
259 def executeScript(test, litConfig, tmpBase, commands, cwd):
260 bashPath = litConfig.getBashPath();
261 isWin32CMDEXE = (litConfig.isWindows and not bashPath)
262 script = tmpBase + '.script'
268 if litConfig.isWindows and not isWin32CMDEXE:
269 mode += 'b' # Avoid CRLFs when writing bash scripts.
270 f = open(script, mode)
272 f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
274 if test.config.pipefail:
275 f.write('set -o pipefail;')
276 f.write('{ ' + '; } &&\n{ '.join(commands) + '; }')
281 command = ['cmd','/c', script]
284 command = [bashPath, script]
286 command = ['/bin/sh', script]
287 if litConfig.useValgrind:
288 # FIXME: Running valgrind on sh is overkill. We probably could just
289 # run on clang with no real loss.
290 command = litConfig.valgrindArgs + command
292 return lit.util.executeCommand(command, cwd=cwd,
293 env=test.config.environment)
295 def isExpectedFail(test, xfails):
296 # Check if any of the xfails match an available feature or the target.
298 # If this is the wildcard, it always fails.
302 # If this is an exact match for one of the features, it fails.
303 if item in test.config.available_features:
306 # If this is a part of the target triple, it fails.
307 if item in test.suite.config.target_triple:
312 def parseIntegratedTestScript(test, normalize_slashes=False,
313 extra_substitutions=[]):
314 """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
315 script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES'
316 information. The RUN lines also will have variable substitution performed.
319 # Get the temporary location, this is always relative to the test suite
320 # root, not test source root.
322 # FIXME: This should not be here?
323 sourcepath = test.getSourcePath()
324 sourcedir = os.path.dirname(sourcepath)
325 execpath = test.getExecPath()
326 execdir,execbase = os.path.split(execpath)
327 tmpDir = os.path.join(execdir, 'Output')
328 tmpBase = os.path.join(tmpDir, execbase)
330 # Normalize slashes, if requested.
331 if normalize_slashes:
332 sourcepath = sourcepath.replace('\\', '/')
333 sourcedir = sourcedir.replace('\\', '/')
334 tmpDir = tmpDir.replace('\\', '/')
335 tmpBase = tmpBase.replace('\\', '/')
337 # We use #_MARKER_# to hide %% while we do the other substitutions.
338 substitutions = list(extra_substitutions)
339 substitutions.extend([('%%', '#_MARKER_#')])
340 substitutions.extend(test.config.substitutions)
341 substitutions.extend([('%s', sourcepath),
344 ('%{pathsep}', os.pathsep),
345 ('%t', tmpBase + '.tmp'),
347 ('#_MARKER_#', '%')])
349 # "%/[STpst]" should be normalized.
350 substitutions.extend([
351 ('%/s', sourcepath.replace('\\', '/')),
352 ('%/S', sourcedir.replace('\\', '/')),
353 ('%/p', sourcedir.replace('\\', '/')),
354 ('%/t', tmpBase.replace('\\', '/') + '.tmp'),
355 ('%/T', tmpDir.replace('\\', '/')),
358 # Collect the test lines from the script.
363 for ln in open(sourcepath):
366 # Isolate the command to run.
367 index = ln.index('RUN:')
370 # Trim trailing whitespace.
373 # Substitute line number expressions
374 ln = re.sub('%\(line\)', str(line_number), ln)
375 def replace_line_number(match):
376 if match.group(1) == '+':
377 return str(line_number + int(match.group(2)))
378 if match.group(1) == '-':
379 return str(line_number - int(match.group(2)))
380 ln = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, ln)
382 # Collapse lines with trailing '\\'.
383 if script and script[-1][-1] == '\\':
384 script[-1] = script[-1][:-1] + ln
388 items = ln[ln.index('XFAIL:') + 6:].split(',')
389 xfails.extend([s.strip() for s in items])
390 elif 'REQUIRES:' in ln:
391 items = ln[ln.index('REQUIRES:') + 9:].split(',')
392 requires.extend([s.strip() for s in items])
394 # Check for END. lines.
395 if ln[ln.index('END.'):].strip() == 'END.':
398 # Apply substitutions to the script. Allow full regular
399 # expression syntax. Replace each matching occurrence of regular
400 # expression pattern a with substitution b in line ln.
402 # Apply substitutions
403 for a,b in substitutions:
405 b = b.replace("\\","\\\\")
406 ln = re.sub(a, b, ln)
408 # Strip the trailing newline and any extra whitespace.
410 script = [processLine(ln)
413 # Verify the script contains a run line.
415 return (Test.UNRESOLVED, "Test has no run line!")
417 # Check for unterminated run lines.
418 if script[-1][-1] == '\\':
419 return (Test.UNRESOLVED, "Test has unterminated run lines (with '\\')")
421 # Check that we have the required features:
422 missing_required_features = [f for f in requires
423 if f not in test.config.available_features]
424 if missing_required_features:
425 msg = ', '.join(missing_required_features)
426 return (Test.UNSUPPORTED,
427 "Test requires the following features: %s" % msg)
429 isXFail = isExpectedFail(test, xfails)
430 return script,isXFail,tmpBase,execdir
432 def formatTestOutput(status, out, err, exitCode, script):
434 output.write(u"Script:\n")
435 output.write(u"--\n")
436 output.write(u'\n'.join(script))
437 output.write(u"\n--\n")
438 output.write(u"Exit Code: %r\n\n" % exitCode)
440 output.write(u"Command Output (stdout):\n")
441 output.write(u"--\n")
442 output.write(unicode(out))
443 output.write(u"--\n")
445 output.write(u"Command Output (stderr):\n")
446 output.write(u"--\n")
447 output.write(unicode(err))
448 output.write(u"--\n")
449 return (status, output.getvalue())
451 def executeShTest(test, litConfig, useExternalSh,
452 extra_substitutions=[]):
453 if test.config.unsupported:
454 return (Test.UNSUPPORTED, 'Test is unsupported')
456 res = parseIntegratedTestScript(test, useExternalSh, extra_substitutions)
460 script, isXFail, tmpBase, execdir = res
462 if litConfig.noExecute:
463 return (Test.PASS, '')
465 # Create the output directory if it does not already exist.
466 lit.util.mkdir_p(os.path.dirname(tmpBase))
469 res = executeScript(test, litConfig, tmpBase, script, execdir)
471 res = executeScriptInternal(test, litConfig, tmpBase, script, execdir)
475 out,err,exitCode = res
492 return formatTestOutput(status, out, err, exitCode, script)