Skip to content

Commit

Permalink
Redesign cbicov to support commands
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
Pennycook committed Nov 26, 2024
1 parent 023c259 commit 5614a38
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 37 deletions.
Empty file added codebasin/coverage/__init__.py
Empty file.
129 changes: 92 additions & 37 deletions codebasin/coverage/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,64 @@
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="<path>",
dest="source_dir",
help=_help_string("Path to source directory.", is_long=True),
default=os.getcwd(),
)
parser.add_argument(
compute_parser.add_argument(
"-x",
"--exclude",
dest="excludes",
Expand All @@ -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="<output path>",
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="<input path>",
help=_help_string("Path to compilation database JSON file."),
)
parser.add_argument(
"ofile",
metavar="<output path>",
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]:
Expand All @@ -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
Expand Down Expand Up @@ -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:])
Expand Down

0 comments on commit 5614a38

Please sign in to comment.