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