diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..1569bf5d --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +python 3.12.0 diff --git a/gatorgrade/input/checks.py b/gatorgrade/input/checks.py index 5333ba4e..42b2f2bd 100644 --- a/gatorgrade/input/checks.py +++ b/gatorgrade/input/checks.py @@ -1,12 +1,18 @@ """Define check classes.""" -from typing import List +from typing import List, Optional class ShellCheck: # pylint: disable=too-few-public-methods """Represent a shell check.""" - def __init__(self, command: str, description: str = None, json_info=None): # type: ignore + def __init__( + self, + command: str, + description: Optional[str] = None, + json_info=None, + gg_args: Optional[List[str]] = None, + ): # type: ignore """Construct a ShellCheck. Args: @@ -14,11 +20,17 @@ def __init__(self, command: str, description: str = None, json_info=None): # ty description: The description to use in output. If no description is given, the command is used as the description. json_info: The all-encompassing check information to include in json output. - If none is given, command is used + If none is given, command is used. + options: Additional options for the shell check as a dictionary. """ self.command = command self.description = description if description is not None else command self.json_info = json_info + self.gg_args = gg_args if gg_args is not None else [] + + def __str__(self): + """Return a string representation of the ShellCheck.""" + return f"ShellCheck(command={self.command}, description={self.description}, json_info={self.json_info}, gg_args={self.gg_args})" class GatorGraderCheck: # pylint: disable=too-few-public-methods diff --git a/gatorgrade/input/command_line_generator.py b/gatorgrade/input/command_line_generator.py index 364fbe78..3604ec13 100644 --- a/gatorgrade/input/command_line_generator.py +++ b/gatorgrade/input/command_line_generator.py @@ -24,44 +24,49 @@ def generate_checks( """ checks: List[Union[ShellCheck, GatorGraderCheck]] = [] for check_data in check_data_list: + gg_args = [] + # Add name of check if it exists in data, otherwise use default_check + check_name = check_data.check.get("check", "ConfirmFileExists") + gg_args.append(str(check_name)) + # Add any additional options + options = check_data.check.get("options") + if options is not None: + for option in options: + # If option should be a flag (i.e. its value is the `True` boolean), + # add only the option without a value + option_value = options[option] + if isinstance(option_value, bool): + if option_value: + gg_args.append(f"--{option}") + # Otherwise, add both the option and its value + else: + gg_args.extend([f"--{option}", str(option_value)]) + # Add directory and file if file context in data + if check_data.file_context is not None: + # Get the file and directory using os + dirname, filename = os.path.split(check_data.file_context) + if dirname == "": + dirname = "." + gg_args.extend(["--directory", dirname, "--file", filename]) + # If the check has a `command` key, then it is a shell check if "command" in check_data.check: + # Do not add GatorGrader-specific arguments to gg_args for shell checks + shell_gg_args = gg_args.copy() checks.append( ShellCheck( command=check_data.check.get("command"), description=check_data.check.get("description"), json_info=check_data.check, + gg_args=shell_gg_args, ) ) # Otherwise, it is a GatorGrader check else: - gg_args = [] - # Add description option if in data + # Add the description to gg_args for GatorGrader checks description = check_data.check.get("description") - if description is not None: - gg_args.extend(["--description", str(description)]) - # Always add name of check, which should be in data - gg_args.append(str(check_data.check.get("check"))) - # Add any additional options - options = check_data.check.get("options") - if options is not None: - for option in options: - # If option should be a flag (i.e. its value is the `True` boolean), - # add only the option without a value - option_value = options[option] - if isinstance(option_value, bool): - if option_value: - gg_args.append(f"--{option}") - # Otherwise, add both the option and its value - else: - gg_args.extend([f"--{option}", str(option_value)]) - # Add directory and file if file context in data - if check_data.file_context is not None: - # Get the file and directory using os - dirname, filename = os.path.split(check_data.file_context) - if dirname == "": - dirname = "." - gg_args.extend(["--directory", dirname, "--file", filename]) + if description: + gg_args.extend(["--description", description]) checks.append(GatorGraderCheck(gg_args=gg_args, json_info=check_data.check)) return checks diff --git a/gatorgrade/output/check_result.py b/gatorgrade/output/check_result.py index f6e88c29..de696220 100644 --- a/gatorgrade/output/check_result.py +++ b/gatorgrade/output/check_result.py @@ -1,6 +1,6 @@ """Define check result class.""" -from typing import Union +from typing import Optional import rich @@ -12,8 +12,9 @@ def __init__( passed: bool, description: str, json_info, - path: Union[str, None] = None, + path: Optional[str] = None, diagnostic: str = "No diagnostic message available", + weight: int = 1, ): """Construct a CheckResult. @@ -23,6 +24,7 @@ def __init__( description: The description to use in output. json_info: the overall information to be included in json output diagnostic: The message to use in output if the check has failed. + weight: The weight of the check. """ self.passed = passed self.description = description @@ -30,8 +32,11 @@ def __init__( self.diagnostic = diagnostic self.path = path self.run_command = "" + self.weight = weight - def display_result(self, show_diagnostic: bool = False) -> str: + def display_result( + self, show_diagnostic: bool = False, percentage: Optional[float] = None + ) -> str: """Print check's passed or failed status, description, and, optionally, diagnostic message. If no diagnostic message is available, then the output will say so. @@ -39,10 +44,15 @@ def display_result(self, show_diagnostic: bool = False) -> str: Args: show_diagnostic: If true, show the diagnostic message if the check has failed. Defaults to false. + percentage: The percentage weight of the check. """ icon = "✓" if self.passed else "✕" icon_color = "green" if self.passed else "red" - message = f"[{icon_color}]{icon}[/] {self.description}" + percentage_color = "green" if self.passed else "red" + message = f"[{icon_color}]{icon}[/]" + if percentage is not None: + message += f" [{percentage_color}]({percentage:.2f}%)[/]" + message += f" {self.description}" if not self.passed and show_diagnostic: message += f"\n[yellow] → {self.diagnostic}" return message @@ -62,7 +72,9 @@ def __str__(self, show_diagnostic: bool = False) -> str: message = self.display_result(show_diagnostic) return message - def print(self, show_diagnostic: bool = False) -> None: + def print( + self, show_diagnostic: bool = False, percentage: Optional[float] = None + ) -> None: """Print check's passed or failed status, description, and, optionally, diagnostic message. If no diagnostic message is available, then the output will say so. @@ -70,6 +82,7 @@ def print(self, show_diagnostic: bool = False) -> None: Args: show_diagnostic: If true, show the diagnostic message if the check has failed. Defaults to false. + percentage: The percentage weight of the check. """ - message = self.display_result(show_diagnostic) + message = self.display_result(show_diagnostic, percentage) rich.print(message) diff --git a/gatorgrade/output/output.py b/gatorgrade/output/output.py index 8fc94c5b..d8cb32ea 100644 --- a/gatorgrade/output/output.py +++ b/gatorgrade/output/output.py @@ -174,6 +174,9 @@ def create_markdown_report_file(json: dict) -> str: if "command" == i: val = check["options"]["command"] markdown_contents += f"\n\t- **command** {val}" + if "weight" == i: + val = check["options"]["weight"] + markdown_contents += f"\n\t- **weight:** {val}" if "fragment" == i: val = check["options"]["fragment"] markdown_contents += f"\n\t- **fragment:** {val}" @@ -284,6 +287,19 @@ def write_json_or_md_file(file_name, content_type, content): ) from e +def calculate_total_weight(checks: List[Union[ShellCheck, GatorGraderCheck]]) -> int: + """Calculate the total weight of all the checks.""" + total_weight = 0 + for check in checks: + weight = 1 # Default weight + if isinstance(check, (ShellCheck, GatorGraderCheck)): + if "--weight" in check.gg_args: + index_of_weight = check.gg_args.index("--weight") + weight = int(check.gg_args[index_of_weight + 1]) + total_weight += weight + return total_weight + + def run_checks( checks: List[Union[ShellCheck, GatorGraderCheck]], report: Tuple[str, str, str] ) -> bool: @@ -295,11 +311,11 @@ def run_checks( Args: checks: The list of shell and GatorGrader checks to run. """ + total_weight = calculate_total_weight(checks) results = [] - # run each of the checks for check in checks: result = None - command_ran = None + weight = 1 # run a shell check; this means # that it is going to run a command # in the shell as a part of a check; @@ -308,12 +324,30 @@ def run_checks( # inside of a CheckResult object but # not initialized in the constructor if isinstance(check, ShellCheck): + # Weighted Checks + if "--weight" in check.gg_args: + index_of_weight = check.gg_args.index("--weight") + weight = int(check.gg_args[index_of_weight + 1]) # Updated line + # Remove the hint from gg_args before passing to GatorGrader + check.gg_args = ( + check.gg_args[:index_of_weight] + + check.gg_args[index_of_weight + 2 :] + ) result = _run_shell_check(check) - command_ran = check.command - result.run_command = command_ran + result.weight = weight # run a check that GatorGrader implements elif isinstance(check, GatorGraderCheck): + # Weighted Checks + if "--weight" in check.gg_args: + index_of_weight = check.gg_args.index("--weight") + weight = int(check.gg_args[index_of_weight + 1]) # Updated line + # Remove the hint from gg_args before passing to GatorGrader + check.gg_args = ( + check.gg_args[:index_of_weight] + + check.gg_args[index_of_weight + 2 :] + ) result = _run_gg_check(check) + result.weight = weight # check to see if there was a command in the # GatorGraderCheck. This code finds the index of the # word "--command" in the check.gg_args list if it @@ -330,8 +364,16 @@ def run_checks( # there were results from running checks # and thus they must be displayed if result is not None: - result.print() + check_weight = int(weight) / total_weight + check_percent = round(check_weight * 100, 2) + result.print(percentage=check_percent) results.append(result) + # testing printed weights + # print(f"Weight = {weight}") + # total_weight = sum(getattr(result, 'weight', 1) for result in results) + # check_weight = (int(weight) / total_weight) + # print(check_weight * 100) + # determine if there are failures and then display them failed_results = list(filter(lambda result: not result.passed, results)) # print failures list if there are failures to print @@ -339,8 +381,6 @@ def run_checks( if len(failed_results) > 0: print("\n-~- FAILURES -~-\n") for result in failed_results: - # main.console.print("This is a result") - # main.console.print(result) result.print(show_diagnostic=True) # this result is an instance of CheckResult # that has a run_command field that is some @@ -356,11 +396,22 @@ def run_checks( # determine how many of the checks passed and then # compute the total percentage of checks passed passed_count = len(results) - len(failed_results) - # prevent division by zero if no results + # Math to calculate the % score if len(results) == 0: + total_weight = 0 + passed_weight = 0 percent = 0 else: - percent = round(passed_count / len(results) * 100) + total_weight = sum(getattr(result, "weight", 1) for result in results) + passed_weight = sum( + getattr(result, "weight", 1) for result in results if result.passed + ) + # prevent division by zero if no results + if total_weight == 0: + percent = 0 + else: + percent = round(passed_weight / total_weight * 100) + # if the report is wanted, create output in line with their specifications if all(report): report_output_data = create_report_json(passed_count, results, percent) diff --git a/tests/input/test_input_gg_checks.py b/tests/input/test_input_gg_checks.py index 6633fb8a..39369e7f 100644 --- a/tests/input/test_input_gg_checks.py +++ b/tests/input/test_input_gg_checks.py @@ -24,7 +24,7 @@ def test_parse_config_check_gg_matchfilefragment(): # When parse_config is run output = parse_config(config) # Then the description, check name, and options appear in the GatorGrader arguments - assert output[0].gg_args == [ + assert set(output[0].gg_args) == { "--description", "Complete all TODOs", "MatchFileFragment", @@ -37,7 +37,7 @@ def test_parse_config_check_gg_matchfilefragment(): "path/to", "--file", "file.py", - ] + } def test_parse_config_gg_check_no_file_context_contains_no_file(): @@ -49,13 +49,13 @@ def test_parse_config_gg_check_no_file_context_contains_no_file(): # When parse_config is run output = parse_config(config) # Then the GatorGrader arguments do not contain a file path - assert output[0].gg_args == [ + assert set(output[0].gg_args) == { "--description", "Have 8 commits", "CountCommits", "--count", "8", - ] + } def test_parse_config_parses_both_shell_and_gg_checks(): @@ -76,13 +76,13 @@ def test_parse_config_yml_file_runs_setup_shell_checks(): # When parse_config run output = parse_config(config) # Then the output should contain the GatorGrader check - assert output[0].gg_args == [ + assert set(output[0].gg_args) == { "--description", "Have 8 commits", "CountCommits", "--count", "8", - ] + } def test_parse_config_shell_check_contains_command():