1from __future__ import absolute_import 2import os, signal, subprocess, sys 3import re 4import platform 5import tempfile 6try: 7 from io import StringIO 8except ImportError: 9 from StringIO import StringIO 10 11import lit.ShUtil as ShUtil 12import lit.Test as Test 13import lit.Util as Util 14 15class InternalShellError(Exception): 16 def __init__(self, command, message): 17 self.command = command 18 self.message = message 19 20kIsWindows = platform.system() == 'Windows' 21 22# Don't use close_fds on Windows. 23kUseCloseFDs = not kIsWindows 24 25# Use temporary files to replace /dev/null on Windows. 26kAvoidDevNull = kIsWindows 27 28def executeCommand(command, cwd=None, env=None): 29 # Close extra file handles on UNIX (on Windows this cannot be done while 30 # also redirecting input). 31 close_fds = not kIsWindows 32 33 p = subprocess.Popen(command, cwd=cwd, 34 stdin=subprocess.PIPE, 35 stdout=subprocess.PIPE, 36 stderr=subprocess.PIPE, 37 env=env, close_fds=close_fds) 38 out,err = p.communicate() 39 exitCode = p.wait() 40 41 # Detect Ctrl-C in subprocess. 42 if exitCode == -signal.SIGINT: 43 raise KeyboardInterrupt 44 45 return out, err, exitCode 46 47def executeShCmd(cmd, cfg, cwd, results): 48 if isinstance(cmd, ShUtil.Seq): 49 if cmd.op == ';': 50 res = executeShCmd(cmd.lhs, cfg, cwd, results) 51 return executeShCmd(cmd.rhs, cfg, cwd, results) 52 53 if cmd.op == '&': 54 raise InternalShellError(cmd,"unsupported shell operator: '&'") 55 56 if cmd.op == '||': 57 res = executeShCmd(cmd.lhs, cfg, cwd, results) 58 if res != 0: 59 res = executeShCmd(cmd.rhs, cfg, cwd, results) 60 return res 61 62 if cmd.op == '&&': 63 res = executeShCmd(cmd.lhs, cfg, cwd, results) 64 if res is None: 65 return res 66 67 if res == 0: 68 res = executeShCmd(cmd.rhs, cfg, cwd, results) 69 return res 70 71 raise ValueError('Unknown shell command: %r' % cmd.op) 72 73 assert isinstance(cmd, ShUtil.Pipeline) 74 procs = [] 75 input = subprocess.PIPE 76 stderrTempFiles = [] 77 opened_files = [] 78 named_temp_files = [] 79 # To avoid deadlock, we use a single stderr stream for piped 80 # output. This is null until we have seen some output using 81 # stderr. 82 for i,j in enumerate(cmd.commands): 83 # Apply the redirections, we use (N,) as a sentinel to indicate stdin, 84 # stdout, stderr for N equal to 0, 1, or 2 respectively. Redirects to or 85 # from a file are represented with a list [file, mode, file-object] 86 # where file-object is initially None. 87 redirects = [(0,), (1,), (2,)] 88 for r in j.redirects: 89 if r[0] == ('>',2): 90 redirects[2] = [r[1], 'w', None] 91 elif r[0] == ('>>',2): 92 redirects[2] = [r[1], 'a', None] 93 elif r[0] == ('>&',2) and r[1] in '012': 94 redirects[2] = redirects[int(r[1])] 95 elif r[0] == ('>&',) or r[0] == ('&>',): 96 redirects[1] = redirects[2] = [r[1], 'w', None] 97 elif r[0] == ('>',): 98 redirects[1] = [r[1], 'w', None] 99 elif r[0] == ('>>',): 100 redirects[1] = [r[1], 'a', None] 101 elif r[0] == ('<',): 102 redirects[0] = [r[1], 'r', None] 103 else: 104 raise InternalShellError(j,"Unsupported redirect: %r" % (r,)) 105 106 # Map from the final redirections to something subprocess can handle. 107 final_redirects = [] 108 for index,r in enumerate(redirects): 109 if r == (0,): 110 result = input 111 elif r == (1,): 112 if index == 0: 113 raise InternalShellError(j,"Unsupported redirect for stdin") 114 elif index == 1: 115 result = subprocess.PIPE 116 else: 117 result = subprocess.STDOUT 118 elif r == (2,): 119 if index != 2: 120 raise InternalShellError(j,"Unsupported redirect on stdout") 121 result = subprocess.PIPE 122 else: 123 if r[2] is None: 124 if kAvoidDevNull and r[0] == '/dev/null': 125 r[2] = tempfile.TemporaryFile(mode=r[1]) 126 else: 127 r[2] = open(r[0], r[1]) 128 # Workaround a Win32 and/or subprocess bug when appending. 129 # 130 # FIXME: Actually, this is probably an instance of PR6753. 131 if r[1] == 'a': 132 r[2].seek(0, 2) 133 opened_files.append(r[2]) 134 result = r[2] 135 final_redirects.append(result) 136 137 stdin, stdout, stderr = final_redirects 138 139 # If stderr wants to come from stdout, but stdout isn't a pipe, then put 140 # stderr on a pipe and treat it as stdout. 141 if (stderr == subprocess.STDOUT and stdout != subprocess.PIPE): 142 stderr = subprocess.PIPE 143 stderrIsStdout = True 144 else: 145 stderrIsStdout = False 146 147 # Don't allow stderr on a PIPE except for the last 148 # process, this could deadlock. 149 # 150 # FIXME: This is slow, but so is deadlock. 151 if stderr == subprocess.PIPE and j != cmd.commands[-1]: 152 stderr = tempfile.TemporaryFile(mode='w+b') 153 stderrTempFiles.append((i, stderr)) 154 155 # Resolve the executable path ourselves. 156 args = list(j.args) 157 args[0] = Util.which(args[0], cfg.environment['PATH']) 158 if not args[0]: 159 raise InternalShellError(j, '%r: command not found' % j.args[0]) 160 161 # Replace uses of /dev/null with temporary files. 162 if kAvoidDevNull: 163 for i,arg in enumerate(args): 164 if arg == "/dev/null": 165 f = tempfile.NamedTemporaryFile(delete=False) 166 f.close() 167 named_temp_files.append(f.name) 168 args[i] = f.name 169 170 procs.append(subprocess.Popen(args, cwd=cwd, 171 stdin = stdin, 172 stdout = stdout, 173 stderr = stderr, 174 env = cfg.environment, 175 close_fds = kUseCloseFDs)) 176 177 # Immediately close stdin for any process taking stdin from us. 178 if stdin == subprocess.PIPE: 179 procs[-1].stdin.close() 180 procs[-1].stdin = None 181 182 # Update the current stdin source. 183 if stdout == subprocess.PIPE: 184 input = procs[-1].stdout 185 elif stderrIsStdout: 186 input = procs[-1].stderr 187 else: 188 input = subprocess.PIPE 189 190 # Explicitly close any redirected files. We need to do this now because we 191 # need to release any handles we may have on the temporary files (important 192 # on Win32, for example). Since we have already spawned the subprocess, our 193 # handles have already been transferred so we do not need them anymore. 194 for f in opened_files: 195 f.close() 196 197 # FIXME: There is probably still deadlock potential here. Yawn. 198 procData = [None] * len(procs) 199 procData[-1] = procs[-1].communicate() 200 201 for i in range(len(procs) - 1): 202 if procs[i].stdout is not None: 203 out = procs[i].stdout.read() 204 else: 205 out = '' 206 if procs[i].stderr is not None: 207 err = procs[i].stderr.read() 208 else: 209 err = '' 210 procData[i] = (out,err) 211 212 # Read stderr out of the temp files. 213 for i,f in stderrTempFiles: 214 f.seek(0, 0) 215 procData[i] = (procData[i][0], f.read()) 216 217 exitCode = None 218 for i,(out,err) in enumerate(procData): 219 res = procs[i].wait() 220 # Detect Ctrl-C in subprocess. 221 if res == -signal.SIGINT: 222 raise KeyboardInterrupt 223 224 results.append((cmd.commands[i], out, err, res)) 225 if cmd.pipe_err: 226 # Python treats the exit code as a signed char. 227 if res < 0: 228 exitCode = min(exitCode, res) 229 else: 230 exitCode = max(exitCode, res) 231 else: 232 exitCode = res 233 234 # Remove any named temporary files we created. 235 for f in named_temp_files: 236 try: 237 os.remove(f) 238 except OSError: 239 pass 240 241 if cmd.negate: 242 exitCode = not exitCode 243 244 return exitCode 245 246def executeScriptInternal(test, litConfig, tmpBase, commands, cwd): 247 cmds = [] 248 for ln in commands: 249 try: 250 cmds.append(ShUtil.ShParser(ln, litConfig.isWindows, 251 test.config.pipefail).parse()) 252 except: 253 return (Test.FAIL, "shell parser error on: %r" % ln) 254 255 cmd = cmds[0] 256 for c in cmds[1:]: 257 cmd = ShUtil.Seq(cmd, '&&', c) 258 259 results = [] 260 try: 261 exitCode = executeShCmd(cmd, test.config, cwd, results) 262 except InternalShellError: 263 e = sys.exc_info()[1] 264 exitCode = 127 265 results.append((e.command, '', e.message, exitCode)) 266 267 out = err = '' 268 for i,(cmd, cmd_out,cmd_err,res) in enumerate(results): 269 out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args)) 270 out += 'Command %d Result: %r\n' % (i, res) 271 out += 'Command %d Output:\n%s\n\n' % (i, cmd_out) 272 out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err) 273 274 return out, err, exitCode 275 276def executeScript(test, litConfig, tmpBase, commands, cwd): 277 bashPath = litConfig.getBashPath(); 278 isWin32CMDEXE = (litConfig.isWindows and not bashPath) 279 script = tmpBase + '.script' 280 if isWin32CMDEXE: 281 script += '.bat' 282 283 # Write script file 284 mode = 'w' 285 if litConfig.isWindows and not isWin32CMDEXE: 286 mode += 'b' # Avoid CRLFs when writing bash scripts. 287 f = open(script, mode) 288 if isWin32CMDEXE: 289 f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands)) 290 else: 291 if test.config.pipefail: 292 f.write('set -o pipefail;') 293 f.write('{ ' + '; } &&\n{ '.join(commands) + '; }') 294 f.write('\n') 295 f.close() 296 297 if isWin32CMDEXE: 298 command = ['cmd','/c', script] 299 else: 300 if bashPath: 301 command = [bashPath, script] 302 else: 303 command = ['/bin/sh', script] 304 if litConfig.useValgrind: 305 # FIXME: Running valgrind on sh is overkill. We probably could just 306 # run on clang with no real loss. 307 command = litConfig.valgrindArgs + command 308 309 return executeCommand(command, cwd=cwd, env=test.config.environment) 310 311def isExpectedFail(test, xfails): 312 # Check if any of the xfails match an available feature or the target. 313 for item in xfails: 314 # If this is the wildcard, it always fails. 315 if item == '*': 316 return True 317 318 # If this is an exact match for one of the features, it fails. 319 if item in test.config.available_features: 320 return True 321 322 # If this is a part of the target triple, it fails. 323 if item in test.suite.config.target_triple: 324 return True 325 326 return False 327 328def parseIntegratedTestScript(test, normalize_slashes=False, 329 extra_substitutions=[]): 330 """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test 331 script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES' 332 information. The RUN lines also will have variable substitution performed. 333 """ 334 335 # Get the temporary location, this is always relative to the test suite 336 # root, not test source root. 337 # 338 # FIXME: This should not be here? 339 sourcepath = test.getSourcePath() 340 sourcedir = os.path.dirname(sourcepath) 341 execpath = test.getExecPath() 342 execdir,execbase = os.path.split(execpath) 343 tmpDir = os.path.join(execdir, 'Output') 344 tmpBase = os.path.join(tmpDir, execbase) 345 if test.index is not None: 346 tmpBase += '_%d' % test.index 347 348 # Normalize slashes, if requested. 349 if normalize_slashes: 350 sourcepath = sourcepath.replace('\\', '/') 351 sourcedir = sourcedir.replace('\\', '/') 352 tmpDir = tmpDir.replace('\\', '/') 353 tmpBase = tmpBase.replace('\\', '/') 354 355 # We use #_MARKER_# to hide %% while we do the other substitutions. 356 substitutions = list(extra_substitutions) 357 substitutions.extend([('%%', '#_MARKER_#')]) 358 substitutions.extend(test.config.substitutions) 359 substitutions.extend([('%s', sourcepath), 360 ('%S', sourcedir), 361 ('%p', sourcedir), 362 ('%{pathsep}', os.pathsep), 363 ('%t', tmpBase + '.tmp'), 364 ('%T', tmpDir), 365 ('#_MARKER_#', '%')]) 366 367 # Collect the test lines from the script. 368 script = [] 369 xfails = [] 370 requires = [] 371 line_number = 0 372 for ln in open(sourcepath): 373 line_number += 1 374 if 'RUN:' in ln: 375 # Isolate the command to run. 376 index = ln.index('RUN:') 377 ln = ln[index+4:] 378 379 # Trim trailing whitespace. 380 ln = ln.rstrip() 381 382 # Substitute line number expressions 383 ln = re.sub('%\(line\)', str(line_number), ln) 384 def replace_line_number(match): 385 if match.group(1) == '+': 386 return str(line_number + int(match.group(2))) 387 if match.group(1) == '-': 388 return str(line_number - int(match.group(2))) 389 ln = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, ln) 390 391 # Collapse lines with trailing '\\'. 392 if script and script[-1][-1] == '\\': 393 script[-1] = script[-1][:-1] + ln 394 else: 395 script.append(ln) 396 elif 'XFAIL:' in ln: 397 items = ln[ln.index('XFAIL:') + 6:].split(',') 398 xfails.extend([s.strip() for s in items]) 399 elif 'REQUIRES:' in ln: 400 items = ln[ln.index('REQUIRES:') + 9:].split(',') 401 requires.extend([s.strip() for s in items]) 402 elif 'END.' in ln: 403 # Check for END. lines. 404 if ln[ln.index('END.'):].strip() == 'END.': 405 break 406 407 # Apply substitutions to the script. Allow full regular 408 # expression syntax. Replace each matching occurrence of regular 409 # expression pattern a with substitution b in line ln. 410 def processLine(ln): 411 # Apply substitutions 412 for a,b in substitutions: 413 if kIsWindows: 414 b = b.replace("\\","\\\\") 415 ln = re.sub(a, b, ln) 416 417 # Strip the trailing newline and any extra whitespace. 418 return ln.strip() 419 script = map(processLine, script) 420 421 # Verify the script contains a run line. 422 if not script: 423 return (Test.UNRESOLVED, "Test has no run line!") 424 425 # Check for unterminated run lines. 426 if script[-1][-1] == '\\': 427 return (Test.UNRESOLVED, "Test has unterminated run lines (with '\\')") 428 429 # Check that we have the required features: 430 missing_required_features = [f for f in requires 431 if f not in test.config.available_features] 432 if missing_required_features: 433 msg = ', '.join(missing_required_features) 434 return (Test.UNSUPPORTED, 435 "Test requires the following features: %s" % msg) 436 437 isXFail = isExpectedFail(test, xfails) 438 return script,isXFail,tmpBase,execdir 439 440def formatTestOutput(status, out, err, exitCode, script): 441 output = StringIO() 442 output.write(u"Script:\n") 443 output.write(u"--\n") 444 output.write(u'\n'.join(script)) 445 output.write(u"\n--\n") 446 output.write(u"Exit Code: %r\n\n" % exitCode) 447 if out: 448 output.write(u"Command Output (stdout):\n") 449 output.write(u"--\n") 450 output.write(unicode(out)) 451 output.write(u"--\n") 452 if err: 453 output.write(u"Command Output (stderr):\n") 454 output.write(u"--\n") 455 output.write(unicode(err)) 456 output.write(u"--\n") 457 return (status, output.getvalue()) 458 459def executeShTest(test, litConfig, useExternalSh, 460 extra_substitutions=[]): 461 if test.config.unsupported: 462 return (Test.UNSUPPORTED, 'Test is unsupported') 463 464 res = parseIntegratedTestScript(test, useExternalSh, extra_substitutions) 465 if len(res) == 2: 466 return res 467 468 script, isXFail, tmpBase, execdir = res 469 470 if litConfig.noExecute: 471 return (Test.PASS, '') 472 473 # Create the output directory if it does not already exist. 474 Util.mkdir_p(os.path.dirname(tmpBase)) 475 476 if useExternalSh: 477 res = executeScript(test, litConfig, tmpBase, script, execdir) 478 else: 479 res = executeScriptInternal(test, litConfig, tmpBase, script, execdir) 480 if len(res) == 2: 481 return res 482 483 out,err,exitCode = res 484 if isXFail: 485 ok = exitCode != 0 486 if ok: 487 status = Test.XFAIL 488 else: 489 status = Test.XPASS 490 else: 491 ok = exitCode == 0 492 if ok: 493 status = Test.PASS 494 else: 495 status = Test.FAIL 496 497 if ok: 498 return (status,'') 499 500 return formatTestOutput(status, out, err, exitCode, script) 501