1 from __future__ import absolute_import 2 import os, signal, subprocess, sys 3 import re 4 import platform 5 import tempfile 6 import threading 7 8 import lit.ShUtil as ShUtil 9 import lit.Test as Test 10 import lit.util 11 from lit.util import to_bytes, to_string 12 13 class InternalShellError(Exception): 14 def __init__(self, command, message): 15 self.command = command 16 self.message = message 17 18 kIsWindows = platform.system() == 'Windows' 19 20 # Don't use close_fds on Windows. 21 kUseCloseFDs = not kIsWindows 22 23 # Use temporary files to replace /dev/null on Windows. 24 kAvoidDevNull = kIsWindows 25 26 class ShellEnvironment(object): 27 28 """Mutable shell environment containing things like CWD and env vars. 29 30 Environment variables are not implemented, but cwd tracking is. 31 """ 32 33 def __init__(self, cwd, env): 34 self.cwd = cwd 35 self.env = dict(env) 36 37 class TimeoutHelper(object): 38 """ 39 Object used to helper manage enforcing a timeout in 40 _executeShCmd(). It is passed through recursive calls 41 to collect processes that have been executed so that when 42 the timeout happens they can be killed. 43 """ 44 def __init__(self, timeout): 45 self.timeout = timeout 46 self._procs = [] 47 self._timeoutReached = False 48 self._doneKillPass = False 49 # This lock will be used to protect concurrent access 50 # to _procs and _doneKillPass 51 self._lock = None 52 self._timer = None 53 54 def cancel(self): 55 if not self.active(): 56 return 57 self._timer.cancel() 58 59 def active(self): 60 return self.timeout > 0 61 62 def addProcess(self, proc): 63 if not self.active(): 64 return 65 needToRunKill = False 66 with self._lock: 67 self._procs.append(proc) 68 # Avoid re-entering the lock by finding out if kill needs to be run 69 # again here but call it if necessary once we have left the lock. 70 # We could use a reentrant lock here instead but this code seems 71 # clearer to me. 72 needToRunKill = self._doneKillPass 73 74 # The initial call to _kill() from the timer thread already happened so 75 # we need to call it again from this thread, otherwise this process 76 # will be left to run even though the timeout was already hit 77 if needToRunKill: 78 assert self.timeoutReached() 79 self._kill() 80 81 def startTimer(self): 82 if not self.active(): 83 return 84 85 # Do some late initialisation that's only needed 86 # if there is a timeout set 87 self._lock = threading.Lock() 88 self._timer = threading.Timer(self.timeout, self._handleTimeoutReached) 89 self._timer.start() 90 91 def _handleTimeoutReached(self): 92 self._timeoutReached = True 93 self._kill() 94 95 def timeoutReached(self): 96 return self._timeoutReached 97 98 def _kill(self): 99 """ 100 This method may be called multiple times as we might get unlucky 101 and be in the middle of creating a new process in _executeShCmd() 102 which won't yet be in ``self._procs``. By locking here and in 103 addProcess() we should be able to kill processes launched after 104 the initial call to _kill() 105 """ 106 with self._lock: 107 for p in self._procs: 108 lit.util.killProcessAndChildren(p.pid) 109 # Empty the list and note that we've done a pass over the list 110 self._procs = [] # Python2 doesn't have list.clear() 111 self._doneKillPass = True 112 113 class ShellCommandResult(object): 114 """Captures the result of an individual command.""" 115 116 def __init__(self, command, stdout, stderr, exitCode, timeoutReached, 117 outputFiles = []): 118 self.command = command 119 self.stdout = stdout 120 self.stderr = stderr 121 self.exitCode = exitCode 122 self.timeoutReached = timeoutReached 123 self.outputFiles = list(outputFiles) 124 125 def executeShCmd(cmd, shenv, results, timeout=0): 126 """ 127 Wrapper around _executeShCmd that handles 128 timeout 129 """ 130 # Use the helper even when no timeout is required to make 131 # other code simpler (i.e. avoid bunch of ``!= None`` checks) 132 timeoutHelper = TimeoutHelper(timeout) 133 if timeout > 0: 134 timeoutHelper.startTimer() 135 finalExitCode = _executeShCmd(cmd, shenv, results, timeoutHelper) 136 timeoutHelper.cancel() 137 timeoutInfo = None 138 if timeoutHelper.timeoutReached(): 139 timeoutInfo = 'Reached timeout of {} seconds'.format(timeout) 140 141 return (finalExitCode, timeoutInfo) 142 143 def _executeShCmd(cmd, shenv, results, timeoutHelper): 144 if timeoutHelper.timeoutReached(): 145 # Prevent further recursion if the timeout has been hit 146 # as we should try avoid launching more processes. 147 return None 148 149 if isinstance(cmd, ShUtil.Seq): 150 if cmd.op == ';': 151 res = _executeShCmd(cmd.lhs, shenv, results, timeoutHelper) 152 return _executeShCmd(cmd.rhs, shenv, results, timeoutHelper) 153 154 if cmd.op == '&': 155 raise InternalShellError(cmd,"unsupported shell operator: '&'") 156 157 if cmd.op == '||': 158 res = _executeShCmd(cmd.lhs, shenv, results, timeoutHelper) 159 if res != 0: 160 res = _executeShCmd(cmd.rhs, shenv, results, timeoutHelper) 161 return res 162 163 if cmd.op == '&&': 164 res = _executeShCmd(cmd.lhs, shenv, results, timeoutHelper) 165 if res is None: 166 return res 167 168 if res == 0: 169 res = _executeShCmd(cmd.rhs, shenv, results, timeoutHelper) 170 return res 171 172 raise ValueError('Unknown shell command: %r' % cmd.op) 173 assert isinstance(cmd, ShUtil.Pipeline) 174 175 # Handle shell builtins first. 176 if cmd.commands[0].args[0] == 'cd': 177 if len(cmd.commands) != 1: 178 raise ValueError("'cd' cannot be part of a pipeline") 179 if len(cmd.commands[0].args) != 2: 180 raise ValueError("'cd' supports only one argument") 181 newdir = cmd.commands[0].args[1] 182 # Update the cwd in the parent environment. 183 if os.path.isabs(newdir): 184 shenv.cwd = newdir 185 else: 186 shenv.cwd = os.path.join(shenv.cwd, newdir) 187 # The cd builtin always succeeds. If the directory does not exist, the 188 # following Popen calls will fail instead. 189 return 0 190 191 procs = [] 192 input = subprocess.PIPE 193 stderrTempFiles = [] 194 opened_files = [] 195 named_temp_files = [] 196 # To avoid deadlock, we use a single stderr stream for piped 197 # output. This is null until we have seen some output using 198 # stderr. 199 for i,j in enumerate(cmd.commands): 200 # Reference the global environment by default. 201 cmd_shenv = shenv 202 if j.args[0] == 'env': 203 # Create a copy of the global environment and modify it for this one 204 # command. There might be multiple envs in a pipeline: 205 # env FOO=1 llc < %s | env BAR=2 llvm-mc | FileCheck %s 206 cmd_shenv = ShellEnvironment(shenv.cwd, shenv.env) 207 arg_idx = 1 208 for arg_idx, arg in enumerate(j.args[1:]): 209 # Partition the string into KEY=VALUE. 210 key, eq, val = arg.partition('=') 211 # Stop if there was no equals. 212 if eq == '': 213 break 214 cmd_shenv.env[key] = val 215 j.args = j.args[arg_idx+1:] 216 217 # Apply the redirections, we use (N,) as a sentinel to indicate stdin, 218 # stdout, stderr for N equal to 0, 1, or 2 respectively. Redirects to or 219 # from a file are represented with a list [file, mode, file-object] 220 # where file-object is initially None. 221 redirects = [(0,), (1,), (2,)] 222 for r in j.redirects: 223 if r[0] == ('>',2): 224 redirects[2] = [r[1], 'w', None] 225 elif r[0] == ('>>',2): 226 redirects[2] = [r[1], 'a', None] 227 elif r[0] == ('>&',2) and r[1] in '012': 228 redirects[2] = redirects[int(r[1])] 229 elif r[0] == ('>&',) or r[0] == ('&>',): 230 redirects[1] = redirects[2] = [r[1], 'w', None] 231 elif r[0] == ('>',): 232 redirects[1] = [r[1], 'w', None] 233 elif r[0] == ('>>',): 234 redirects[1] = [r[1], 'a', None] 235 elif r[0] == ('<',): 236 redirects[0] = [r[1], 'r', None] 237 else: 238 raise InternalShellError(j,"Unsupported redirect: %r" % (r,)) 239 240 # Map from the final redirections to something subprocess can handle. 241 final_redirects = [] 242 for index,r in enumerate(redirects): 243 if r == (0,): 244 result = input 245 elif r == (1,): 246 if index == 0: 247 raise InternalShellError(j,"Unsupported redirect for stdin") 248 elif index == 1: 249 result = subprocess.PIPE 250 else: 251 result = subprocess.STDOUT 252 elif r == (2,): 253 if index != 2: 254 raise InternalShellError(j,"Unsupported redirect on stdout") 255 result = subprocess.PIPE 256 else: 257 if r[2] is None: 258 redir_filename = None 259 if kAvoidDevNull and r[0] == '/dev/null': 260 r[2] = tempfile.TemporaryFile(mode=r[1]) 261 elif kIsWindows and r[0] == '/dev/tty': 262 # Simulate /dev/tty on Windows. 263 # "CON" is a special filename for the console. 264 r[2] = open("CON", r[1]) 265 else: 266 # Make sure relative paths are relative to the cwd. 267 redir_filename = os.path.join(cmd_shenv.cwd, r[0]) 268 r[2] = open(redir_filename, r[1]) 269 # Workaround a Win32 and/or subprocess bug when appending. 270 # 271 # FIXME: Actually, this is probably an instance of PR6753. 272 if r[1] == 'a': 273 r[2].seek(0, 2) 274 opened_files.append(tuple(r) + (redir_filename,)) 275 result = r[2] 276 final_redirects.append(result) 277 278 stdin, stdout, stderr = final_redirects 279 280 # If stderr wants to come from stdout, but stdout isn't a pipe, then put 281 # stderr on a pipe and treat it as stdout. 282 if (stderr == subprocess.STDOUT and stdout != subprocess.PIPE): 283 stderr = subprocess.PIPE 284 stderrIsStdout = True 285 else: 286 stderrIsStdout = False 287 288 # Don't allow stderr on a PIPE except for the last 289 # process, this could deadlock. 290 # 291 # FIXME: This is slow, but so is deadlock. 292 if stderr == subprocess.PIPE and j != cmd.commands[-1]: 293 stderr = tempfile.TemporaryFile(mode='w+b') 294 stderrTempFiles.append((i, stderr)) 295 296 # Resolve the executable path ourselves. 297 args = list(j.args) 298 executable = None 299 # For paths relative to cwd, use the cwd of the shell environment. 300 if args[0].startswith('.'): 301 exe_in_cwd = os.path.join(cmd_shenv.cwd, args[0]) 302 if os.path.isfile(exe_in_cwd): 303 executable = exe_in_cwd 304 if not executable: 305 executable = lit.util.which(args[0], cmd_shenv.env['PATH']) 306 if not executable: 307 raise InternalShellError(j, '%r: command not found' % j.args[0]) 308 309 # Replace uses of /dev/null with temporary files. 310 if kAvoidDevNull: 311 for i,arg in enumerate(args): 312 if arg == "/dev/null": 313 f = tempfile.NamedTemporaryFile(delete=False) 314 f.close() 315 named_temp_files.append(f.name) 316 args[i] = f.name 317 318 try: 319 procs.append(subprocess.Popen(args, cwd=cmd_shenv.cwd, 320 executable = executable, 321 stdin = stdin, 322 stdout = stdout, 323 stderr = stderr, 324 env = cmd_shenv.env, 325 close_fds = kUseCloseFDs)) 326 # Let the helper know about this process 327 timeoutHelper.addProcess(procs[-1]) 328 except OSError as e: 329 raise InternalShellError(j, 'Could not create process ({}) due to {}'.format(executable, e)) 330 331 # Immediately close stdin for any process taking stdin from us. 332 if stdin == subprocess.PIPE: 333 procs[-1].stdin.close() 334 procs[-1].stdin = None 335 336 # Update the current stdin source. 337 if stdout == subprocess.PIPE: 338 input = procs[-1].stdout 339 elif stderrIsStdout: 340 input = procs[-1].stderr 341 else: 342 input = subprocess.PIPE 343 344 # Explicitly close any redirected files. We need to do this now because we 345 # need to release any handles we may have on the temporary files (important 346 # on Win32, for example). Since we have already spawned the subprocess, our 347 # handles have already been transferred so we do not need them anymore. 348 for (name, mode, f, path) in opened_files: 349 f.close() 350 351 # FIXME: There is probably still deadlock potential here. Yawn. 352 procData = [None] * len(procs) 353 procData[-1] = procs[-1].communicate() 354 355 for i in range(len(procs) - 1): 356 if procs[i].stdout is not None: 357 out = procs[i].stdout.read() 358 else: 359 out = '' 360 if procs[i].stderr is not None: 361 err = procs[i].stderr.read() 362 else: 363 err = '' 364 procData[i] = (out,err) 365 366 # Read stderr out of the temp files. 367 for i,f in stderrTempFiles: 368 f.seek(0, 0) 369 procData[i] = (procData[i][0], f.read()) 370 371 def to_string(bytes): 372 if isinstance(bytes, str): 373 return bytes 374 return bytes.encode('utf-8') 375 376 exitCode = None 377 for i,(out,err) in enumerate(procData): 378 res = procs[i].wait() 379 # Detect Ctrl-C in subprocess. 380 if res == -signal.SIGINT: 381 raise KeyboardInterrupt 382 383 # Ensure the resulting output is always of string type. 384 try: 385 if out is None: 386 out = '' 387 else: 388 out = to_string(out.decode('utf-8', errors='replace')) 389 except: 390 out = str(out) 391 try: 392 if err is None: 393 err = '' 394 else: 395 err = to_string(err.decode('utf-8', errors='replace')) 396 except: 397 err = str(err) 398 399 # Gather the redirected output files for failed commands. 400 output_files = [] 401 if res != 0: 402 for (name, mode, f, path) in sorted(opened_files): 403 if path is not None and mode in ('w', 'a'): 404 try: 405 with open(path, 'rb') as f: 406 data = f.read() 407 except: 408 data = None 409 if data != None: 410 output_files.append((name, path, data)) 411 412 results.append(ShellCommandResult( 413 cmd.commands[i], out, err, res, timeoutHelper.timeoutReached(), 414 output_files)) 415 if cmd.pipe_err: 416 # Python treats the exit code as a signed char. 417 if exitCode is None: 418 exitCode = res 419 elif res < 0: 420 exitCode = min(exitCode, res) 421 else: 422 exitCode = max(exitCode, res) 423 else: 424 exitCode = res 425 426 # Remove any named temporary files we created. 427 for f in named_temp_files: 428 try: 429 os.remove(f) 430 except OSError: 431 pass 432 433 if cmd.negate: 434 exitCode = not exitCode 435 436 return exitCode 437 438 def executeScriptInternal(test, litConfig, tmpBase, commands, cwd): 439 cmds = [] 440 for ln in commands: 441 try: 442 cmds.append(ShUtil.ShParser(ln, litConfig.isWindows, 443 test.config.pipefail).parse()) 444 except: 445 return lit.Test.Result(Test.FAIL, "shell parser error on: %r" % ln) 446 447 cmd = cmds[0] 448 for c in cmds[1:]: 449 cmd = ShUtil.Seq(cmd, '&&', c) 450 451 results = [] 452 timeoutInfo = None 453 try: 454 shenv = ShellEnvironment(cwd, test.config.environment) 455 exitCode, timeoutInfo = executeShCmd(cmd, shenv, results, timeout=litConfig.maxIndividualTestTime) 456 except InternalShellError: 457 e = sys.exc_info()[1] 458 exitCode = 127 459 results.append( 460 ShellCommandResult(e.command, '', e.message, exitCode, False)) 461 462 out = err = '' 463 for i,result in enumerate(results): 464 # Write the command line run. 465 out += '$ %s\n' % (' '.join('"%s"' % s 466 for s in result.command.args),) 467 468 # If nothing interesting happened, move on. 469 if litConfig.maxIndividualTestTime == 0 and \ 470 result.exitCode == 0 and \ 471 not result.stdout.strip() and not result.stderr.strip(): 472 continue 473 474 # Otherwise, something failed or was printed, show it. 475 476 # Add the command output, if redirected. 477 for (name, path, data) in result.outputFiles: 478 if data.strip(): 479 out += "# redirected output from %r:\n" % (name,) 480 data = to_string(data.decode('utf-8', errors='replace')) 481 if len(data) > 1024: 482 out += data[:1024] + "\n...\n" 483 out += "note: data was truncated\n" 484 else: 485 out += data 486 out += "\n" 487 488 if result.stdout.strip(): 489 out += '# command output:\n%s\n' % (result.stdout,) 490 if result.stderr.strip(): 491 out += '# command stderr:\n%s\n' % (result.stderr,) 492 if not result.stdout.strip() and not result.stderr.strip(): 493 out += "note: command had no output on stdout or stderr\n" 494 495 # Show the error conditions: 496 if result.exitCode != 0: 497 out += "error: command failed with exit status: %d\n" % ( 498 result.exitCode,) 499 if litConfig.maxIndividualTestTime > 0: 500 out += 'error: command reached timeout: %s\n' % ( 501 i, str(result.timeoutReached)) 502 503 return out, err, exitCode, timeoutInfo 504 505 def executeScript(test, litConfig, tmpBase, commands, cwd): 506 bashPath = litConfig.getBashPath(); 507 isWin32CMDEXE = (litConfig.isWindows and not bashPath) 508 script = tmpBase + '.script' 509 if isWin32CMDEXE: 510 script += '.bat' 511 512 # Write script file 513 mode = 'w' 514 if litConfig.isWindows and not isWin32CMDEXE: 515 mode += 'b' # Avoid CRLFs when writing bash scripts. 516 f = open(script, mode) 517 if isWin32CMDEXE: 518 f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands)) 519 else: 520 if test.config.pipefail: 521 f.write('set -o pipefail;') 522 f.write('{ ' + '; } &&\n{ '.join(commands) + '; }') 523 f.write('\n') 524 f.close() 525 526 if isWin32CMDEXE: 527 command = ['cmd','/c', script] 528 else: 529 if bashPath: 530 command = [bashPath, script] 531 else: 532 command = ['/bin/sh', script] 533 if litConfig.useValgrind: 534 # FIXME: Running valgrind on sh is overkill. We probably could just 535 # run on clang with no real loss. 536 command = litConfig.valgrindArgs + command 537 538 try: 539 out, err, exitCode = lit.util.executeCommand(command, cwd=cwd, 540 env=test.config.environment, 541 timeout=litConfig.maxIndividualTestTime) 542 return (out, err, exitCode, None) 543 except lit.util.ExecuteCommandTimeoutException as e: 544 return (e.out, e.err, e.exitCode, e.msg) 545 546 def parseIntegratedTestScriptCommands(source_path, keywords): 547 """ 548 parseIntegratedTestScriptCommands(source_path) -> commands 549 550 Parse the commands in an integrated test script file into a list of 551 (line_number, command_type, line). 552 """ 553 554 # This code is carefully written to be dual compatible with Python 2.5+ and 555 # Python 3 without requiring input files to always have valid codings. The 556 # trick we use is to open the file in binary mode and use the regular 557 # expression library to find the commands, with it scanning strings in 558 # Python2 and bytes in Python3. 559 # 560 # Once we find a match, we do require each script line to be decodable to 561 # UTF-8, so we convert the outputs to UTF-8 before returning. This way the 562 # remaining code can work with "strings" agnostic of the executing Python 563 # version. 564 565 keywords_re = re.compile( 566 to_bytes("(%s)(.*)\n" % ("|".join(re.escape(k) for k in keywords),))) 567 568 f = open(source_path, 'rb') 569 try: 570 # Read the entire file contents. 571 data = f.read() 572 573 # Ensure the data ends with a newline. 574 if not data.endswith(to_bytes('\n')): 575 data = data + to_bytes('\n') 576 577 # Iterate over the matches. 578 line_number = 1 579 last_match_position = 0 580 for match in keywords_re.finditer(data): 581 # Compute the updated line number by counting the intervening 582 # newlines. 583 match_position = match.start() 584 line_number += data.count(to_bytes('\n'), last_match_position, 585 match_position) 586 last_match_position = match_position 587 588 # Convert the keyword and line to UTF-8 strings and yield the 589 # command. Note that we take care to return regular strings in 590 # Python 2, to avoid other code having to differentiate between the 591 # str and unicode types. 592 keyword,ln = match.groups() 593 yield (line_number, to_string(keyword.decode('utf-8')), 594 to_string(ln.decode('utf-8'))) 595 finally: 596 f.close() 597 598 def getTempPaths(test): 599 """Get the temporary location, this is always relative to the test suite 600 root, not test source root.""" 601 execpath = test.getExecPath() 602 execdir,execbase = os.path.split(execpath) 603 tmpDir = os.path.join(execdir, 'Output') 604 tmpBase = os.path.join(tmpDir, execbase) 605 return tmpDir, tmpBase 606 607 def getDefaultSubstitutions(test, tmpDir, tmpBase, normalize_slashes=False): 608 sourcepath = test.getSourcePath() 609 sourcedir = os.path.dirname(sourcepath) 610 611 # Normalize slashes, if requested. 612 if normalize_slashes: 613 sourcepath = sourcepath.replace('\\', '/') 614 sourcedir = sourcedir.replace('\\', '/') 615 tmpDir = tmpDir.replace('\\', '/') 616 tmpBase = tmpBase.replace('\\', '/') 617 618 # We use #_MARKER_# to hide %% while we do the other substitutions. 619 substitutions = [] 620 substitutions.extend([('%%', '#_MARKER_#')]) 621 substitutions.extend(test.config.substitutions) 622 substitutions.extend([('%s', sourcepath), 623 ('%S', sourcedir), 624 ('%p', sourcedir), 625 ('%{pathsep}', os.pathsep), 626 ('%t', tmpBase + '.tmp'), 627 ('%T', tmpDir), 628 ('#_MARKER_#', '%')]) 629 630 # "%/[STpst]" should be normalized. 631 substitutions.extend([ 632 ('%/s', sourcepath.replace('\\', '/')), 633 ('%/S', sourcedir.replace('\\', '/')), 634 ('%/p', sourcedir.replace('\\', '/')), 635 ('%/t', tmpBase.replace('\\', '/') + '.tmp'), 636 ('%/T', tmpDir.replace('\\', '/')), 637 ]) 638 639 # "%:[STpst]" are paths without colons. 640 if kIsWindows: 641 substitutions.extend([ 642 ('%:s', re.sub(r'^(.):', r'\1', sourcepath)), 643 ('%:S', re.sub(r'^(.):', r'\1', sourcedir)), 644 ('%:p', re.sub(r'^(.):', r'\1', sourcedir)), 645 ('%:t', re.sub(r'^(.):', r'\1', tmpBase) + '.tmp'), 646 ('%:T', re.sub(r'^(.):', r'\1', tmpDir)), 647 ]) 648 else: 649 substitutions.extend([ 650 ('%:s', sourcepath), 651 ('%:S', sourcedir), 652 ('%:p', sourcedir), 653 ('%:t', tmpBase + '.tmp'), 654 ('%:T', tmpDir), 655 ]) 656 return substitutions 657 658 def applySubstitutions(script, substitutions): 659 """Apply substitutions to the script. Allow full regular expression syntax. 660 Replace each matching occurrence of regular expression pattern a with 661 substitution b in line ln.""" 662 def processLine(ln): 663 # Apply substitutions 664 for a,b in substitutions: 665 if kIsWindows: 666 b = b.replace("\\","\\\\") 667 ln = re.sub(a, b, ln) 668 669 # Strip the trailing newline and any extra whitespace. 670 return ln.strip() 671 # Note Python 3 map() gives an iterator rather than a list so explicitly 672 # convert to list before returning. 673 return list(map(processLine, script)) 674 675 676 class ParserKind(object): 677 """ 678 An enumeration representing the style of an integrated test keyword or 679 command. 680 681 TAG: A keyword taking no value. Ex 'END.' 682 COMMAND: A Keyword taking a list of shell commands. Ex 'RUN:' 683 LIST: A keyword taking a comma separated list of value. Ex 'XFAIL:' 684 CUSTOM: A keyword with custom parsing semantics. 685 """ 686 TAG = 0 687 COMMAND = 1 688 LIST = 2 689 CUSTOM = 3 690 691 692 class IntegratedTestKeywordParser(object): 693 """A parser for LLVM/Clang style integrated test scripts. 694 695 keyword: The keyword to parse for. It must end in either '.' or ':'. 696 kind: An value of ParserKind. 697 parser: A custom parser. This value may only be specified with 698 ParserKind.CUSTOM. 699 """ 700 def __init__(self, keyword, kind, parser=None, initial_value=None): 701 if not keyword.endswith('.') and not keyword.endswith(':'): 702 raise ValueError("keyword '%s' must end with either '.' or ':' " 703 % keyword) 704 if keyword.endswith('.') and kind in \ 705 [ParserKind.LIST, ParserKind.COMMAND]: 706 raise ValueError("Keyword '%s' should end in ':'" % keyword) 707 708 elif keyword.endswith(':') and kind in [ParserKind.TAG]: 709 raise ValueError("Keyword '%s' should end in '.'" % keyword) 710 if parser is not None and kind != ParserKind.CUSTOM: 711 raise ValueError("custom parsers can only be specified with " 712 "ParserKind.CUSTOM") 713 self.keyword = keyword 714 self.kind = kind 715 self.parsed_lines = [] 716 self.value = initial_value 717 self.parser = parser 718 719 if kind == ParserKind.COMMAND: 720 self.parser = self._handleCommand 721 elif kind == ParserKind.LIST: 722 self.parser = self._handleList 723 elif kind == ParserKind.TAG: 724 if not keyword.endswith('.'): 725 raise ValueError("keyword '%s' should end with '.'" % keyword) 726 self.parser = self._handleTag 727 elif kind == ParserKind.CUSTOM: 728 if parser is None: 729 raise ValueError("ParserKind.CUSTOM requires a custom parser") 730 self.parser = parser 731 else: 732 raise ValueError("Unknown kind '%s'" % kind) 733 734 def parseLine(self, line_number, line): 735 self.parsed_lines += [(line_number, line)] 736 self.value = self.parser(line_number, line, self.value) 737 738 def getValue(self): 739 return self.value 740 741 @staticmethod 742 def _handleTag(line_number, line, output): 743 """A helper for parsing TAG type keywords""" 744 return (not line.strip() or output) 745 746 @staticmethod 747 def _handleCommand(line_number, line, output): 748 """A helper for parsing COMMAND type keywords""" 749 # Trim trailing whitespace. 750 line = line.rstrip() 751 # Substitute line number expressions 752 line = re.sub('%\(line\)', str(line_number), line) 753 754 def replace_line_number(match): 755 if match.group(1) == '+': 756 return str(line_number + int(match.group(2))) 757 if match.group(1) == '-': 758 return str(line_number - int(match.group(2))) 759 line = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, line) 760 # Collapse lines with trailing '\\'. 761 if output and output[-1][-1] == '\\': 762 output[-1] = output[-1][:-1] + line 763 else: 764 if output is None: 765 output = [] 766 output.append(line) 767 return output 768 769 @staticmethod 770 def _handleList(line_number, line, output): 771 """A parser for LIST type keywords""" 772 if output is None: 773 output = [] 774 output.extend([s.strip() for s in line.split(',')]) 775 return output 776 777 778 def parseIntegratedTestScript(test, additional_parsers=[], 779 require_script=True): 780 """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test 781 script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES' 782 'REQUIRES-ANY' and 'UNSUPPORTED' information. 783 784 If additional parsers are specified then the test is also scanned for the 785 keywords they specify and all matches are passed to the custom parser. 786 787 If 'require_script' is False an empty script 788 may be returned. This can be used for test formats where the actual script 789 is optional or ignored. 790 """ 791 # Collect the test lines from the script. 792 sourcepath = test.getSourcePath() 793 script = [] 794 requires = [] 795 requires_any = [] 796 unsupported = [] 797 builtin_parsers = [ 798 IntegratedTestKeywordParser('RUN:', ParserKind.COMMAND, 799 initial_value=script), 800 IntegratedTestKeywordParser('XFAIL:', ParserKind.LIST, 801 initial_value=test.xfails), 802 IntegratedTestKeywordParser('REQUIRES:', ParserKind.LIST, 803 initial_value=requires), 804 IntegratedTestKeywordParser('REQUIRES-ANY:', ParserKind.LIST, 805 initial_value=requires_any), 806 IntegratedTestKeywordParser('UNSUPPORTED:', ParserKind.LIST, 807 initial_value=unsupported), 808 IntegratedTestKeywordParser('END.', ParserKind.TAG) 809 ] 810 keyword_parsers = {p.keyword: p for p in builtin_parsers} 811 for parser in additional_parsers: 812 if not isinstance(parser, IntegratedTestKeywordParser): 813 raise ValueError('additional parser must be an instance of ' 814 'IntegratedTestKeywordParser') 815 if parser.keyword in keyword_parsers: 816 raise ValueError("Parser for keyword '%s' already exists" 817 % parser.keyword) 818 keyword_parsers[parser.keyword] = parser 819 820 for line_number, command_type, ln in \ 821 parseIntegratedTestScriptCommands(sourcepath, 822 keyword_parsers.keys()): 823 parser = keyword_parsers[command_type] 824 parser.parseLine(line_number, ln) 825 if command_type == 'END.' and parser.getValue() is True: 826 break 827 828 # Verify the script contains a run line. 829 if require_script and not script: 830 return lit.Test.Result(Test.UNRESOLVED, "Test has no run line!") 831 832 # Check for unterminated run lines. 833 if script and script[-1][-1] == '\\': 834 return lit.Test.Result(Test.UNRESOLVED, 835 "Test has unterminated run lines (with '\\')") 836 837 # Check that we have the required features: 838 missing_required_features = [f for f in requires 839 if f not in test.config.available_features] 840 if missing_required_features: 841 msg = ', '.join(missing_required_features) 842 return lit.Test.Result(Test.UNSUPPORTED, 843 "Test requires the following features: %s" 844 % msg) 845 requires_any_features = [f for f in requires_any 846 if f in test.config.available_features] 847 if requires_any and not requires_any_features: 848 msg = ' ,'.join(requires_any) 849 return lit.Test.Result(Test.UNSUPPORTED, 850 "Test requires any of the following features: " 851 "%s" % msg) 852 unsupported_features = [f for f in unsupported 853 if f in test.config.available_features] 854 if unsupported_features: 855 msg = ', '.join(unsupported_features) 856 return lit.Test.Result( 857 Test.UNSUPPORTED, 858 "Test is unsupported with the following features: %s" % msg) 859 860 unsupported_targets = [f for f in unsupported 861 if f in test.suite.config.target_triple] 862 if unsupported_targets: 863 return lit.Test.Result( 864 Test.UNSUPPORTED, 865 "Test is unsupported with the following triple: %s" % ( 866 test.suite.config.target_triple,)) 867 868 if test.config.limit_to_features: 869 # Check that we have one of the limit_to_features features in requires. 870 limit_to_features_tests = [f for f in test.config.limit_to_features 871 if f in requires] 872 if not limit_to_features_tests: 873 msg = ', '.join(test.config.limit_to_features) 874 return lit.Test.Result( 875 Test.UNSUPPORTED, 876 "Test requires one of the limit_to_features features %s" % msg) 877 return script 878 879 880 def _runShTest(test, litConfig, useExternalSh, script, tmpBase): 881 # Create the output directory if it does not already exist. 882 lit.util.mkdir_p(os.path.dirname(tmpBase)) 883 884 execdir = os.path.dirname(test.getExecPath()) 885 if useExternalSh: 886 res = executeScript(test, litConfig, tmpBase, script, execdir) 887 else: 888 res = executeScriptInternal(test, litConfig, tmpBase, script, execdir) 889 if isinstance(res, lit.Test.Result): 890 return res 891 892 out,err,exitCode,timeoutInfo = res 893 if exitCode == 0: 894 status = Test.PASS 895 else: 896 if timeoutInfo == None: 897 status = Test.FAIL 898 else: 899 status = Test.TIMEOUT 900 901 # Form the output log. 902 output = """Script:\n--\n%s\n--\nExit Code: %d\n""" % ( 903 '\n'.join(script), exitCode) 904 905 if timeoutInfo != None: 906 output += """Timeout: %s\n""" % (timeoutInfo,) 907 output += "\n" 908 909 # Append the outputs, if present. 910 if out: 911 output += """Command Output (stdout):\n--\n%s\n--\n""" % (out,) 912 if err: 913 output += """Command Output (stderr):\n--\n%s\n--\n""" % (err,) 914 915 return lit.Test.Result(status, output) 916 917 918 def executeShTest(test, litConfig, useExternalSh, 919 extra_substitutions=[]): 920 if test.config.unsupported: 921 return (Test.UNSUPPORTED, 'Test is unsupported') 922 923 script = parseIntegratedTestScript(test) 924 if isinstance(script, lit.Test.Result): 925 return script 926 if litConfig.noExecute: 927 return lit.Test.Result(Test.PASS) 928 929 tmpDir, tmpBase = getTempPaths(test) 930 substitutions = list(extra_substitutions) 931 substitutions += getDefaultSubstitutions(test, tmpDir, tmpBase, 932 normalize_slashes=useExternalSh) 933 script = applySubstitutions(script, substitutions) 934 935 # Re-run failed tests up to test_retry_attempts times. 936 attempts = 1 937 if hasattr(test.config, 'test_retry_attempts'): 938 attempts += test.config.test_retry_attempts 939 for i in range(attempts): 940 res = _runShTest(test, litConfig, useExternalSh, script, tmpBase) 941 if res.code != Test.FAIL: 942 break 943 # If we had to run the test more than once, count it as a flaky pass. These 944 # will be printed separately in the test summary. 945 if i > 0 and res.code == Test.PASS: 946 res.code = Test.FLAKYPASS 947 return res 948