From 25810b70f25190d20c46b2c2f5bfb4d37b55c63a Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sat, 25 Jan 2020 11:37:10 +0100 Subject: [PATCH] Update littlecheck This allows: - Running scripts via shebang (not important here) - Progress output (so we can ditch more of our run script) - Context (only after, for now) - this is important if there is a test failure --- build_tools/littlecheck.py | 63 +++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/build_tools/littlecheck.py b/build_tools/littlecheck.py index 19759745c..f6a3f0c01 100755 --- a/build_tools/littlecheck.py +++ b/build_tools/littlecheck.py @@ -27,6 +27,10 @@ class Config(object): self.verbose = False # Whether output gets ANSI colorization. self.colorize = False + # Whether to show which file was tested. + self.progress = False + # How many after lines to print + self.after = 5 def colors(self): """ Return a dictionary mapping color names to ANSI escapes """ @@ -112,13 +116,16 @@ class RunCmd(object): class TestFailure(object): - def __init__(self, line, check, testrun): + def __init__(self, line, check, testrun, after = None): self.line = line self.check = check self.testrun = testrun self.error_annotation_line = None + # The output that comes *after* the failure. + self.after = after def message(self): + afterlines = self.testrun.config.after fields = self.testrun.config.colors() fields["name"] = self.testrun.name fields["subbed_command"] = self.testrun.subbed_command @@ -139,7 +146,8 @@ class TestFailure(object): "check_type": self.check.type, } ) - fmtstrs = ["{RED}Failure{RESET} in {name}:", ""] + filemsg = "" if self.testrun.config.progress else " in {name}" + fmtstrs = ["{RED}Failure{RESET}" + filemsg + ":", ""] if self.line and self.check: fmtstrs += [ " The {check_type} on line {input_lineno} wants:", @@ -171,6 +179,12 @@ class TestFailure(object): " additional output on stderr:{error_annotation_lineno}:", " {BOLD}{error_annotation}{RESET}", ] + if self.after: + fields["additional_output"] = " ".join(self.after[:afterlines]) + fmtstrs += [ + " additional output:", + " {BOLD}{additional_output}{RESET}", + ] fmtstrs += [" when running command:", " {subbed_command}"] return "\n".join(fmtstrs).format(**fields) @@ -194,7 +208,9 @@ def perform_substitution(input_str, subs): for key, replacement in subs_ordered: if text.startswith(key): return replacement + text[len(key) :] - raise CheckerError("Unknown substitution: " + m.group(0)) + # No substitution found, so we default to running it as-is, + # which will end up running it via $PATH. + return text return re.sub(r"%(%|[a-zA-Z0-9_-]+)", subber, input_str) @@ -224,7 +240,9 @@ class TestRun(object): lineq.pop() else: # Failed to match. - return TestFailure(line, check, self) + lineq.pop() + # Add context, ignoring empty lines. + return TestFailure(line, check, self, after = [line.text for line in lineq[::-1] if not line.is_empty_space()]) # Drain empties. while lineq and lineq[-1].is_empty_space(): lineq.pop() @@ -258,6 +276,13 @@ class TestRun(object): close_fds=True, # For Python 2.6 as shipped on RHEL 6 ) stdout, stderr = proc.communicate() + # HACK: This is quite cheesy: POSIX specifies that sh should return 127 for a missing command. + # Technically it's also possible to return it in other conditions. + # Practically, that's *probably* not going to happen. + status = proc.returncode + if status == 127: + raise CheckerError("Command could not be found: " + self.subbed_command) + outlines = [ Line(text, idx + 1, "stdout") for idx, text in enumerate(split_by_newlines(stdout)) @@ -343,7 +368,12 @@ class Checker(object): # Find run commands. self.runcmds = [RunCmd.parse(sl) for sl in group1s(RUN_RE)] if not self.runcmds: - raise CheckerError("No runlines ('# RUN') found") + # If no RUN command has been given, fall back to the shebang. + if lines[0].text.startswith("#!"): + # Remove the "#!" at the beginning, and the newline at the end. + self.runcmds = [RunCmd(lines[0].text[2:-1] + " %s", lines[0])] + else: + raise CheckerError("No runlines ('# RUN') found") # Find check cmds. self.outchecks = [ @@ -406,7 +436,22 @@ def get_argparse(): action="append", default=[], ) + parser.add_argument( + "-p", + "--progress", + action='store_true', + dest='progress', + help="Show the files to be checked", + default=False, + ) parser.add_argument("file", nargs="+", help="File to check") + parser.add_argument( + "-A", "--after", + type=int, + help="How many non-empty lines of output after a failure to print (default: 5)", + action="store", + default=5, + ) return parser @@ -419,11 +464,19 @@ def main(): success = True config = Config() config.colorize = sys.stdout.isatty() + config.progress = args.progress + fields = config.colors() + config.after = args.after for path in args.file: + fields["path"] = path + if config.progress: + print("Testing file {path} ... ".format(**fields), end='') subs = def_subs.copy() subs["s"] = path if not check_path(path, subs, config, TestFailure.print_message): success = False + elif config.progress: + print("{GREEN}ok{RESET}".format(**fields)) sys.exit(0 if success else 1)