From 5614a388b8d92ce33978232ae2f7964d7e42de07 Mon Sep 17 00:00:00 2001 From: John Pennycook Date: Tue, 26 Nov 2024 13:26:47 +0000 Subject: [PATCH] Redesign cbicov to support commands Reimplements the cbicov script as one with support for multiple commands, exposing the previous default behavior of the cbicov prototype via the "cbicov compute" command. The new command also defaults to an output file called "coverage.json", since this filename was used by 99% of all cbicov invocations in our testing. Although slightly more complicated, switching to a command-based implementation will enable us to add more commands related to coverage (e.g., converting between coverage formats, visualizing coverage, etc) without needing to create dedicated command-line interfaces for each. Signed-off-by: John Pennycook --- codebasin/coverage/__init__.py | 0 codebasin/coverage/__main__.py | 129 +++++++++++++++++++++++---------- 2 files changed, 92 insertions(+), 37 deletions(-) create mode 100644 codebasin/coverage/__init__.py diff --git a/codebasin/coverage/__init__.py b/codebasin/coverage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codebasin/coverage/__main__.py b/codebasin/coverage/__main__.py index 63971b5..c7d5359 100755 --- a/codebasin/coverage/__main__.py +++ b/codebasin/coverage/__main__.py @@ -12,26 +12,56 @@ from codebasin import CodeBase, config, finder, util # TODO: Refactor to avoid imports from __main__ -from codebasin.__main__ import Formatter, WarningAggregator, _help_string +from codebasin.__main__ import ( + Formatter, + WarningAggregator, + _help_string, + version, +) from codebasin.preprocessor import CodeNode log = logging.getLogger("codebasin") -def cli(argv: list[str]): - # Read command-line arguments +def _build_parser() -> argparse.ArgumentParser: + """ + Build argument parser. + """ parser = argparse.ArgumentParser( - description="Code Base Investigator Coverage Tool", + description="CBI Coverage Tool " + version, formatter_class=argparse.RawTextHelpFormatter, add_help=False, ) + parser.set_defaults(func=None) parser.add_argument( "-h", "--help", action="help", - help=_help_string("Display help message and exit."), + help="Display help message and exit.", ) parser.add_argument( + "--version", + action="version", + version=f"CBI Coverage Tool {version}", + help="Display version information and exit.", + ) + + subparsers = parser.add_subparsers(title="commands") + + compute_parser = subparsers.add_parser( + "compute", + help="Compute coverage.", + formatter_class=argparse.RawTextHelpFormatter, + add_help=False, + ) + compute_parser.set_defaults(func=_compute) + compute_parser.add_argument( + "-h", + "--help", + action="help", + help=_help_string("Display help message and exit."), + ) + compute_parser.add_argument( "-S", "--source-dir", metavar="", @@ -39,7 +69,7 @@ def cli(argv: list[str]): help=_help_string("Path to source directory.", is_long=True), default=os.getcwd(), ) - parser.add_argument( + compute_parser.add_argument( "-x", "--exclude", dest="excludes", @@ -50,21 +80,34 @@ def cli(argv: list[str]): "Exclude files matching this pattern from the code base.", "May be specified multiple times.", is_long=True, + ), + ) + compute_parser.add_argument( + "-o", + "--output", + dest="ofile", + metavar="", + default="coverage.json", + help=_help_string( + "Path to coverage JSON file.", + "If not specified, defaults to 'coverage.json'.", + is_long=True, is_last=True, ), ) - parser.add_argument( + compute_parser.add_argument( "ifile", metavar="", - help=_help_string("Path to compilation database JSON file."), - ) - parser.add_argument( - "ofile", - metavar="", - help=_help_string("Path to coverage JSON file.", is_last=True), + help=_help_string( + "Path to compilation database JSON file.", + is_last=True, + ), ) - args = parser.parse_args(argv) + return parser + + +def _compute(args: argparse.Namespace): dbpath = os.path.realpath(args.ifile) covpath = os.path.realpath(args.ofile) for path in [dbpath, covpath]: @@ -75,29 +118,6 @@ def cli(argv: list[str]): source_dir = os.path.realpath(args.source_dir) - # Configure logging such that: - # - All messages are written to a log file - # - Only errors are written to the terminal - # - Meta-warnings and statistics are generated by a WarningAggregator - aggregator = WarningAggregator() - log.setLevel(logging.DEBUG) - - file_handler = logging.FileHandler("cbi.log", mode="w") - file_handler.setLevel(logging.INFO) - file_handler.setFormatter(Formatter()) - file_handler.addFilter(aggregator) - log.addHandler(file_handler) - - # Inform the user that a log file has been created. - # 'print' instead of 'log' to ensure the message is visible in the output. - log_path = os.path.abspath("cbi.log") - print(f"Log file created at {log_path}") - - stderr_handler = logging.StreamHandler(sys.stderr) - stderr_handler.setLevel(logging.ERROR) - stderr_handler.setFormatter(Formatter(colors=sys.stderr.isatty())) - log.addHandler(stderr_handler) - # Run CBI configured as-if: # - configuration contains a single (dummy) platform # - codebase contains all files in the specified compilation database @@ -135,6 +155,41 @@ def cli(argv: list[str]): sys.exit(0) +def cli(argv: list[str]) -> int: + parser = _build_parser() + args = parser.parse_args(argv) + command = args.func + + if command is None: + parser.print_help() + sys.exit(2) + + # Configure logging such that: + # - All messages are written to a log file + # - Only errors are written to the terminal + # - Meta-warnings and statistics are generated by a WarningAggregator + aggregator = WarningAggregator() + log.setLevel(logging.DEBUG) + + file_handler = logging.FileHandler("cbi.log", mode="w") + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(Formatter()) + file_handler.addFilter(aggregator) + log.addHandler(file_handler) + + # Inform the user that a log file has been created. + # 'print' instead of 'log' to ensure the message is visible in the output. + log_path = os.path.abspath("cbi.log") + print(f"Log file created at {log_path}") + + stderr_handler = logging.StreamHandler(sys.stderr) + stderr_handler.setLevel(logging.ERROR) + stderr_handler.setFormatter(Formatter(colors=sys.stderr.isatty())) + log.addHandler(stderr_handler) + + return command(args) + + def main(): try: cli(sys.argv[1:])