"""
buildtest cli: include functions to build, get test configurations, and
interact with a global configuration for buildtest.
"""
import argparse
import datetime
import sys
from pygments.styles import STYLE_MAP
from rich.color import Color, ColorParseError
from buildtest import BUILDTEST_COPYRIGHT, BUILDTEST_VERSION
from buildtest.defaults import console
from buildtest.schemas.defaults import schema_table
[docs]
def handle_kv_string(val):
"""This method is used as type field in --filter argument in ``buildtest buildspec find``.
This method returns a dict of key,value pair where input is in format
key1=val1,key2=val2,key3=val3
Args:
val (str): Input string in ``key1=value1,key2=value2`` format that is processed into a dictionary type
Returns:
dict: A dict mapping of key=value pairs
"""
kv_dict = {}
if "," in val:
args = val.split(",")
for kv in args:
if "=" not in kv:
raise argparse.ArgumentTypeError("Must specify k=v")
key, value = kv.split("=")[0], kv.split("=")[1]
kv_dict[key] = value
return kv_dict
if "=" not in val:
raise argparse.ArgumentTypeError("Must specify in key=value format")
key, value = val.split("=")[0], val.split("=")[1]
kv_dict[key] = value
return kv_dict
[docs]
def positive_number(value):
"""Checks if input is positive number and returns value as an int type.
Args:
value (str or int): Specify an input number
Returns:
int: Return value as int type
Raises:
argparse.ArgumentTypeError will be raised if input is not positive number or input is not str or int type
>>> positive_number("1")
1
>>> positive_number(2)
2
"""
if not isinstance(value, (str, int)):
raise argparse.ArgumentTypeError(
f"Input must be an integer or string type, you have specified '{value}' which is of type {type(value)}"
)
try:
int_val = int(value)
except ValueError:
console.print(f"[red]Unable to convert {value} to int ")
console.print_exception()
raise ValueError
if int_val <= 0:
raise argparse.ArgumentTypeError(
f"Input: {value} converted to int: {int_val} must be a positive number"
)
return int_val
[docs]
def supported_color(input_color):
"""Checks if input is a supported color and returns value as an Color type.
Args:
input_color (str): Specify an input color
Returns:
str: Return value as rich.color.Color type
Raises:
argparse.ArgumentTypeError will be raised if input is not a supported color input or is not str type
>>> supported_color("red")
red
"""
if not isinstance(input_color, (str)):
raise argparse.ArgumentTypeError(
f"Input must be a string type, you have specified '{input_color}' which is of type {type(input_color)}"
)
try:
color_val = Color.parse(input_color)
except ColorParseError:
console.print(f"[red]Unable to convert {input_color} to a Color ")
console.print_exception()
return
return color_val
[docs]
def valid_time(value):
"""Checks if input is valid time and returns value as a str type.
Args:
value (str): Specify an input date in yyyy-mm-dd format
Returns:
int: Return value as str type in correct format
Raises:
argparse.ArgumentTypeError will be raised if input is not str or input is not in desired format
>>> valid_time("2022-01-01")
"2022-01-01"
>>> valid_time("2022-01-13")
"2022-01-13"
"""
if not isinstance(value, str):
raise argparse.ArgumentTypeError(
f"Input must be string type, you have specified '{value}' which is of type {type(value)}"
)
fmt = "%Y-%m-%d"
try:
dt_object = datetime.datetime.strptime(value, fmt)
except ValueError:
console.print(f"[red]Unable to convert {value} to correct date format")
console.print_exception()
raise ValueError
return dt_object
[docs]
def get_parser():
"""This method is used to simply return the parser for sphinx-argparse."""
bp = BuildTestParser()
return bp.parser
[docs]
class BuildTestParser:
"""This class implements the buildtest command line interface. This class
implements the following methods:
- :func:`get_parser`: This method builds the command line interface for buildtest
- :func:`parse`: This method parses arguments passed to buildtest command line interface
"""
_github = "https://github.com/buildtesters/buildtest"
_docs = "https://buildtest.readthedocs.io/en/latest/index.html"
_slack = "http://hpcbuildtest.slack.com/"
_issues = "https://github.com/buildtesters/buildtest/issues"
_progname = "buildtest"
_description = (
"buildtest is a HPC testing framework for building and running tests."
)
epilog_str = f"""
References
GitHub: {_github}
Documentation: {_docs}
Slack: {_slack}
Please report issues at {_issues}
{BUILDTEST_COPYRIGHT}
"""
_buildtest_show_commands = [
"bd",
"build",
"bc",
"buildspec",
"cdash",
"cg",
"config",
"hy",
"history",
"it",
"inspect",
"path",
"rt",
"report",
"schema",
"style",
"stylecheck",
"test",
"unittests",
]
def __init__(self):
self.parent_parser = self.get_parent_parser()
self.subcommands = {
"build": {"help": "Build and Run test", "aliases": ["bd"]},
"buildspec": {"help": "Buildspec Interface", "aliases": ["bc"]},
"config": {"help": "Query buildtest configuration", "aliases": ["cg"]},
"report": {
"help": "Query test report",
"aliases": ["rt"],
"parents": [
self.parent_parser["pager"],
self.parent_parser["row-count"],
self.parent_parser["terse"],
self.parent_parser["no-header"],
self.parent_parser["count"],
],
},
"inspect": {"help": "Inspect a test", "aliases": ["it"]},
"path": {"help": "Show path attributes for a given test", "aliases": ["p"]},
"history": {"help": "Query build history", "aliases": ["hy"]},
"schema": {"help": "List schema contents and examples"},
"cdash": {"help": "Upload test to CDASH server"},
"cd": {"help": "Change directory to root of test given a test name"},
"clean": {
"help": "Remove all generate files from buildtest including test directory, log files, report file, buildspec cache, history files"
},
"debugreport": {
"help": "Display system information and additional information for debugging purposes.",
"aliases": ["debug"],
},
"stats": {"help": "Show test statistics for given test"},
"info": {"help": " Show details regarding current buildtest setup"},
"show": {"help": "buildtest command guide"},
"commands": {"help": "List all buildtest commands", "aliases": ["cmds"]},
}
self.parser = argparse.ArgumentParser(
prog=self._progname,
formatter_class=argparse.RawDescriptionHelpFormatter,
description=self._description,
usage="%(prog)s [options] [COMMANDS]",
epilog=self.epilog_str,
)
self.subparsers = self.parser.add_subparsers(
title="COMMANDS", dest="subcommands", metavar=""
)
self._build_options()
self.hidden_subcommands = {
"docs": {},
"tutorial-examples": {},
"unittests": {"aliases": ["test"]},
"stylecheck": {"aliases": ["style"]},
}
# Variables needed to show all sub commands and their help message
show_all_help = any(arg in ["-H", "--help-all"] for arg in sys.argv)
if show_all_help:
self.hidden_subcommands = {
"tutorial-examples": {
"help": "Generate documentation examples for Buildtest Tutorial"
},
"docs": {"help": "Open buildtest docs in browser"},
"unittests": {"help": "Run buildtest unit tests", "aliases": ["test"]},
"stylecheck": {
"help": "Run buildtest style checks",
"aliases": ["style"],
},
}
self.buildtest_subcommands = list(self.subcommands.keys()) + list(
self.hidden_subcommands.keys()
)
self._build_subparsers()
self.build_menu()
self.buildspec_menu()
self.config_menu()
self.report_menu()
self.inspect_menu()
self.path_menu()
self.history_menu()
self.schema_menu()
self.cdash_menu()
self.unittest_menu()
self.stylecheck_menu()
self.tutorial_menu()
self.misc_menu()
[docs]
def parse(self):
"""This method parses arguments passed to buildtest command line interface."""
return self.parser.parse_args()
[docs]
def get_subparsers(self):
return self.subparsers
[docs]
def retrieve_main_options(self):
"""This method retrieves all options for buildtest command line interface. This is invoked by ``buildtest --listopts`` command and useful when user
wants to see all options."""
options_list = []
# Iterate over the actions of the parser to extract short and long options
for action in self.parser._actions:
option_strings = action.option_strings
options_list.extend(option_strings)
return sorted(options_list)
[docs]
def _build_subparsers(self):
"""This method builds subparsers for buildtest command line interface."""
for name, kwargs in self.subcommands.items():
self.subparsers.add_parser(name, **kwargs)
for name, kwargs in self.hidden_subcommands.items():
self.subparsers.add_parser(name, **kwargs)
[docs]
def _build_options(self):
"""This method builds the main options for buildtest command line interface."""
self.buildtest_options = [
(
["-V", "--version"],
{
"action": "version",
"version": f"%(prog)s version {BUILDTEST_VERSION}",
},
),
(["-c", "--configfile"], {"help": "Specify Path to Configuration File"}),
(
["-d", "--debug"],
{"action": "store_true", "help": "Stream log messages to stdout"},
),
(
["-l", "--loglevel"],
{
"help": "Filter log messages based on logging level",
"choices": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
"default": "DEBUG",
},
),
(
["--editor"],
{
"help": "Select your preferred editor when opening files.",
"choices": ["vi", "vim", "emacs", "nano"],
},
),
(
["--view-log"],
{"action": "store_true", "help": "Show content of last log"},
),
(
["--logpath"],
{"action": "store_true", "help": "Print full path to last log file"},
),
(
["--print-log"],
{
"action": "store_true",
"help": "Print content of last log without pagination",
},
),
(
["--color"],
{
"type": supported_color,
"metavar": "COLOR",
"help": "Print output of table with the selected color.",
},
),
(
["--no-color"],
{"help": "Disable colored output", "action": "store_true"},
),
(
["--helpcolor"],
{
"action": "store_true",
"help": "Print available color options in a table format.",
},
),
(["-r", "--report"], {"help": "Specify path to test report file"}),
(
["-H", "--help-all"],
{"help": "List all commands and options", "action": "help"},
),
(
["--listopts"],
{"action": "store_true", "help": "List all options for buildtest"},
),
(["--verbose"], {"action": "store_true", "help": "Enable verbose output"}),
]
for args, kwargs in self.buildtest_options:
self.parser.add_argument(*args, **kwargs)
[docs]
def get_subcommands(self):
"""Return a list of buildtest commands. This is useful for ``buildtest commands`` command to show a list of buildtest commands"""
return list(self.subcommands.keys()) + list(self.hidden_subcommands.keys())
[docs]
def get_parent_parser(self):
parent_parser = {}
parent_parser["pager"] = argparse.ArgumentParser(add_help=False)
parent_parser["pager"].add_argument(
"--pager", action="store_true", help="Enable PAGING when viewing result"
)
parent_parser["file"] = argparse.ArgumentParser(add_help=False)
parent_parser["file"].add_argument(
"--file", help="Write configuration file to a new file"
)
parent_parser["row-count"] = argparse.ArgumentParser(add_help=False)
parent_parser["row-count"].add_argument(
"--row-count",
action="store_true",
help="Display number of rows from query shown in table",
)
parent_parser["terse"] = argparse.ArgumentParser(add_help=False)
parent_parser["terse"].add_argument(
"--terse",
action="store_true",
help="Print output in machine readable format",
)
parent_parser["no-header"] = argparse.ArgumentParser(add_help=False)
parent_parser["no-header"].add_argument(
"-n",
"--no-header",
action="store_true",
help="Do not print header columns in terse output (--terse)",
)
parent_parser["count"] = argparse.ArgumentParser(add_help=False)
parent_parser["count"].add_argument(
"-c",
"--count",
type=int,
help="Retrieve limited number of rows that get printed",
)
parent_parser["theme"] = argparse.ArgumentParser(add_help=False)
parent_parser["theme"].add_argument(
"--theme",
metavar="Color Themes",
help="Specify a color theme, Pygments style to use when displaying output. See https://pygments.org/docs/styles/#getting-a-list-of-available-styles for available themese",
choices=list(STYLE_MAP.keys()),
)
return parent_parser