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