Index: llvm/utils/lit/lit/LitConfig.py =================================================================== --- llvm/utils/lit/lit/LitConfig.py +++ llvm/utils/lit/lit/LitConfig.py @@ -6,6 +6,7 @@ import lit.Test import lit.formats import lit.TestingConfig +import lit.TestRunner import lit.util # LitConfig must be a new style class for properties to work @@ -41,6 +42,7 @@ self.isWindows = bool(isWindows) self.params = dict(params) self.bashPath = None + self.additionalCommands = [] # Configuration files to look for when discovering test suites. self.config_prefix = config_prefix or 'lit' @@ -108,6 +110,9 @@ config.load_from_path(path, self) return config + def installKeywordCommand(self, keyword, command): + self.additionalCommands.append((keyword, command)) + def getBashPath(self): """getBashPath - Get the path to 'bash'""" if self.bashPath is not None: Index: llvm/utils/lit/lit/ShCommands.py =================================================================== --- llvm/utils/lit/lit/ShCommands.py +++ llvm/utils/lit/lit/ShCommands.py @@ -35,6 +35,12 @@ else: file.write("%s%s '%s'" % (r[0][1], r[0][0], r[1])) +class CustomCommand: + def __init__(self, command, location, parameter): + self.command = command + self.location = location + self.parameter = parameter + class GlobItem: def __init__(self, pattern): self.pattern = pattern Index: llvm/utils/lit/lit/TestRunner.py =================================================================== --- llvm/utils/lit/lit/TestRunner.py +++ llvm/utils/lit/lit/TestRunner.py @@ -20,6 +20,7 @@ from io import StringIO from lit.ShCommands import GlobItem +from lit.ShCommands import CustomCommand import lit.ShUtil as ShUtil import lit.Test as Test import lit.util @@ -1000,34 +1001,51 @@ def executeScriptInternal(test, litConfig, tmpBase, commands, cwd): cmds = [] - for i, ln in enumerate(commands): - ln = commands[i] = re.sub(kPdbgRegex, ": '\\1'; ", ln) - try: - cmds.append(ShUtil.ShParser(ln, litConfig.isWindows, - test.config.pipefail).parse()) - except: - return lit.Test.Result(Test.FAIL, "shell parser error on: %r" % ln) - - cmd = cmds[0] - for c in cmds[1:]: - cmd = ShUtil.Seq(cmd, '&&', c) + for i, (keyword, ln) in enumerate(commands): + if keyword == 'RUN:': + c = None + ln = re.sub(kPdbgRegex, ": '\\1'; ", ln) + try: + parser = ShUtil.ShParser(ln, litConfig.isWindows, test.config.pipefail) + c = parser.parse() + except: + return lit.Test.Result(Test.FAIL, "shell parser error on: %r" % ln) + if cmds and not isinstance(cmds[-1], CustomCommand): + cmds[-1] = ShUtil.Seq(cmds[-1], '&&', c) + else: + cmds.append(c) + else: + match = re.match('%dbg\((.* at line \\d+)\)(.*)', ln) + command = next(iter(filter(lambda x : x[0] == keyword, litConfig.additionalCommands))) + cmds.append(CustomCommand(command, match.group(1), match.group(2))) results = [] timeoutInfo = None - try: - shenv = ShellEnvironment(cwd, test.config.environment) - exitCode, timeoutInfo = executeShCmd(cmd, shenv, results, timeout=litConfig.maxIndividualTestTime) - except InternalShellError: - e = sys.exc_info()[1] - exitCode = 127 - results.append( - ShellCommandResult(e.command, '', e.message, exitCode, False)) + for cmd in cmds: + try: + if isinstance(cmd, CustomCommand): + fn = cmd.command[1] + stdout, stderr = fn(cmd.parameter) + result = ShellCommandResult(command=cmd, stdout=stdout, stderr=stderr, exitCode=0, timeoutReached=False) + results.append(result) + exitCode = 0 + else: + shenv = ShellEnvironment(cwd, test.config.environment) + exitCode, timeoutInfo = executeShCmd(cmd, shenv, results, timeout=litConfig.maxIndividualTestTime) + except InternalShellError: + e = sys.exc_info()[1] + exitCode = 127 + results.append( + ShellCommandResult(e.command, '', e.message, exitCode, False)) out = err = '' for i,result in enumerate(results): # Write the command line run. - out += '$ %s\n' % (' '.join('"%s"' % s - for s in result.command.args),) + if isinstance(result.command, CustomCommand): + temp = result.command.location + result.command.parameter + else: + temp = ' '.join('"%s"' % s for s in result.command.args) + out += '$ %s\n' % (temp,) # If nothing interesting happened, move on. if litConfig.maxIndividualTestTime == 0 and \ @@ -1246,6 +1264,7 @@ Replace each matching occurrence of regular expression pattern a with substitution b in line ln.""" def processLine(ln): + keyword, ln = ln # Apply substitutions for a,b in substitutions: if kIsWindows: @@ -1253,7 +1272,7 @@ ln = re.sub(a, b, ln) # Strip the trailing newline and any extra whitespace. - return ln.strip() + return (keyword, ln.strip()) # Note Python 3 map() gives an iterator rather than a list so explicitly # convert to list before returning. return list(map(processLine, script)) @@ -1327,9 +1346,7 @@ self.parser = parser if kind == ParserKind.COMMAND: - self.parser = lambda line_number, line, output: \ - self._handleCommand(line_number, line, output, - self.keyword) + self.parser = self._handleCommand elif kind == ParserKind.LIST: self.parser = self._handleList elif kind == ParserKind.BOOLEAN_EXPR: @@ -1343,10 +1360,10 @@ else: raise ValueError("Unknown kind '%s'" % kind) - def parseLine(self, line_number, line): + def parseLine(self, line_number, line, keyword): try: self.parsed_lines += [(line_number, line)] - self.value = self.parser(line_number, line, self.value) + self.value = self.parser(line_number, line, self.value, keyword) except ValueError as e: raise ValueError(str(e) + ("\nin %s directive on test line %d" % (self.keyword, line_number))) @@ -1355,7 +1372,7 @@ return self.value @staticmethod - def _handleTag(line_number, line, output): + def _handleTag(line_number, line, output, keyword): """A helper for parsing TAG type keywords""" return (not line.strip() or output) @@ -1374,8 +1391,8 @@ return str(line_number - int(match.group(2))) line = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, line) # Collapse lines with trailing '\\'. - if output and output[-1][-1] == '\\': - output[-1] = output[-1][:-1] + line + if output and output[-1][1][-1] == '\\': + output[-1] = (keyword, output[-1][1][:-1] + line) else: if output is None: output = [] @@ -1387,11 +1404,11 @@ line = "{pdbg} {real_command}".format( pdbg=pdbg, real_command=line) - output.append(line) + output.append((keyword, line)) return output @staticmethod - def _handleList(line_number, line, output): + def _handleList(line_number, line, output, keyword): """A parser for LIST type keywords""" if output is None: output = [] @@ -1399,7 +1416,7 @@ return output @staticmethod - def _handleBooleanExpr(line_number, line, output): + def _handleBooleanExpr(line_number, line, output, keyword): """A parser for BOOLEAN_EXPR type keywords""" if output is None: output = [] @@ -1412,17 +1429,17 @@ return output @staticmethod - def _handleRequiresAny(line_number, line, output): + def _handleRequiresAny(line_number, line, output, keyword): """A custom parser to transform REQUIRES-ANY: into REQUIRES:""" # Extract the conditions specified in REQUIRES-ANY: as written. conditions = [] - IntegratedTestKeywordParser._handleList(line_number, line, conditions) + IntegratedTestKeywordParser._handleList(line_number, line, conditions, keyword) # Output a `REQUIRES: a || b || c` expression in its place. expression = ' || '.join(conditions) IntegratedTestKeywordParser._handleBooleanExpr(line_number, - expression, output) + expression, output, keyword) return output def parseIntegratedTestScript(test, additional_parsers=[], @@ -1465,6 +1482,7 @@ if parser.keyword in keyword_parsers: raise ValueError("Parser for keyword '%s' already exists" % parser.keyword) + parser.value = script keyword_parsers[parser.keyword] = parser # Collect the test lines from the script. @@ -1473,7 +1491,9 @@ parseIntegratedTestScriptCommands(sourcepath, keyword_parsers.keys()): parser = keyword_parsers[command_type] - parser.parseLine(line_number, ln) + parser.parseLine(line_number, ln, parser.keyword) + if command_type != 'RUN:' and parser.kind == ParserKind.COMMAND: + has_custom_command_lines = True if command_type == 'END.' and parser.getValue() is True: break @@ -1482,7 +1502,7 @@ return lit.Test.Result(Test.UNRESOLVED, "Test has no run line!") # Check for unterminated run lines. - if script and script[-1][-1] == '\\': + if script and script[-1][-1][-1] == '\\': return lit.Test.Result(Test.UNRESOLVED, "Test has unterminated run lines (with '\\')") @@ -1512,14 +1532,18 @@ return script - def _runShTest(test, litConfig, useExternalSh, script, tmpBase): # Create the output directory if it does not already exist. lit.util.mkdir_p(os.path.dirname(tmpBase)) + disableExternalSh = any(x[0] != 'RUN:' for x in script) + if useExternalSh and disableExternalSh: + litConfig.note("External shell disabled since custom command was encountered.") + execdir = os.path.dirname(test.getExecPath()) - if useExternalSh: - res = executeScript(test, litConfig, tmpBase, script, execdir) + scriptCommands = [x[1] for x in script] + if useExternalSh and not disableExternalSh: + res = executeScript(test, litConfig, tmpBase, scriptCommands, execdir) else: res = executeScriptInternal(test, litConfig, tmpBase, script, execdir) if isinstance(res, lit.Test.Result): @@ -1536,7 +1560,7 @@ # Form the output log. output = """Script:\n--\n%s\n--\nExit Code: %d\n""" % ( - '\n'.join(script), exitCode) + '\n'.join(scriptCommands), exitCode) if timeoutInfo is not None: output += """Timeout: %s\n""" % (timeoutInfo,) @@ -1556,7 +1580,11 @@ if test.config.unsupported: return lit.Test.Result(Test.UNSUPPORTED, 'Test is unsupported') - script = parseIntegratedTestScript(test) + additional_parsers = [IntegratedTestKeywordParser(x[0], ParserKind.COMMAND) for x in litConfig.additionalCommands] + script = parseIntegratedTestScript(test, additional_parsers=additional_parsers) + + # If there are custom command lines we can't use an external shell because + # it won't understand them. if isinstance(script, lit.Test.Result): return script if litConfig.noExecute: Index: llvm/utils/lit/tests/Inputs/shtest-keyword-command/keyword-command.txt =================================================================== --- /dev/null +++ llvm/utils/lit/tests/Inputs/shtest-keyword-command/keyword-command.txt @@ -0,0 +1,4 @@ +MYCOMMAND: Command1 +MYCOMMAND: Command2 +MYCOMMAND: Multi-line \ +MYCOMMAND: Command Index: llvm/utils/lit/tests/Inputs/shtest-keyword-command/keyword_helper.py =================================================================== --- /dev/null +++ llvm/utils/lit/tests/Inputs/shtest-keyword-command/keyword_helper.py @@ -0,0 +1,3 @@ + +def customCommand(line): + return ('STDOUT: ' + line, 'STDERR: ' + line) Index: llvm/utils/lit/tests/Inputs/shtest-keyword-command/lit.cfg =================================================================== --- /dev/null +++ llvm/utils/lit/tests/Inputs/shtest-keyword-command/lit.cfg @@ -0,0 +1,18 @@ + +import os +import site + +site.addsitedir(os.path.dirname(__file__)) + + +import lit.formats +import keyword_helper + +config.name = 'shtest-keyword-command' +config.suffixes = ['.txt'] +config.test_format = lit.formats.ShTest() +config.test_source_root = None +config.test_exec_root = None +config.substitutions.append(('%{python}', '"%s"' % (sys.executable))) + +lit_config.installKeywordCommand('MYCOMMAND:', keyword_helper.customCommand) Index: llvm/utils/lit/tests/shtest-keyword-command.py =================================================================== --- /dev/null +++ llvm/utils/lit/tests/shtest-keyword-command.py @@ -0,0 +1,22 @@ +# Check the that keyword commands work as expected. +# +# RUN: %{lit} -j 1 -sav %{inputs}/shtest-keyword-command > %t.out +# RUN: FileCheck --input-file %t.out %s +# +# END. + +# CHECK: $ MYCOMMAND: at line 1 Command1 +# CHECK: command output: +# CHECK: STDOUT: Command1 +# CHECK: command stderr: +# CHECK: STDERR: Command1 +# CHECK: $ MYCOMMAND: at line 2 Command2 +# CHECK: command output: +# CHECK: STDOUT: Command2 +# CHECK: command stderr: +# CHECK: STDERR: Command2 +# CHECK: $ MYCOMMAND: at line 3 Multi-line Command +# CHECK: command output: +# CHECK: STDOUT: Multi-line Command +# CHECK: command stderr: +# CHECK: STDERR: Multi-line Command \ No newline at end of file