Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: hints for failing checks #156

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions gatorgrade/input/checks.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
"""Define check classes."""

from typing import List
from typing import Optional, List


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,
):
"""Construct a ShellCheck.

Args:
command: The command to run in a shell.
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
Expand Down
57 changes: 31 additions & 26 deletions gatorgrade/input/command_line_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion gatorgrade/output/check_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __init__(
self.diagnostic = diagnostic
self.path = path
self.run_command = ""
self.hint = "" # Store the hint as an instance attribute

def display_result(self, show_diagnostic: bool = False) -> str:
"""Print check's passed or failed status, description, and, optionally, diagnostic message.
Expand All @@ -48,7 +49,7 @@ def display_result(self, show_diagnostic: bool = False) -> str:
return message

def __repr__(self):
return f"CheckResult(passed={self.passed}, description='{self.description}', json_info={self.json_info}, path='{self.path}', diagnostic='{self.diagnostic}', run_command='{self.run_command}')"
return f"CheckResult(passed={self.passed}, description='{self.description}', json_info={self.json_info}, path='{self.path}', diagnostic='{self.diagnostic}', run_command='{self.run_command}', hint='{self.hint}')"

def __str__(self, show_diagnostic: bool = False) -> str:
"""Print check's passed or failed status, description, and, optionally, diagnostic message.
Expand Down
31 changes: 28 additions & 3 deletions gatorgrade/output/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "hint" == i:
val = check["options"]["hint"]
markdown_contents += f"\n\t- **hint:** {val}"
if "fragment" == i:
val = check["options"]["fragment"]
markdown_contents += f"\n\t- **fragment:** {val}"
Expand Down Expand Up @@ -300,6 +303,7 @@ def run_checks(
for check in checks:
result = None
command_ran = None
hint = ""
# run a shell check; this means
# that it is going to run a command
# in the shell as a part of a check;
Expand All @@ -308,12 +312,30 @@ def run_checks(
# inside of a CheckResult object but
# not initialized in the constructor
if isinstance(check, ShellCheck):
# Hint Feature
if "--hint" in check.gg_args:
index_of_hint = check.gg_args.index("--hint")
hint = check.gg_args[index_of_hint + 1]
# Remove the hint from gg_args before passing to GatorGrader
check.gg_args = (
check.gg_args[:index_of_hint] + check.gg_args[index_of_hint + 2 :]
)
result = _run_shell_check(check)
command_ran = check.command
result.run_command = command_ran
result.hint = hint
# run a check that GatorGrader implements
elif isinstance(check, GatorGraderCheck):
# Hint Feature
if "--hint" in check.gg_args:
index_of_hint = check.gg_args.index("--hint")
hint = check.gg_args[index_of_hint + 1]
# Remove the hint from gg_args before passing to GatorGrader
check.gg_args = (
check.gg_args[:index_of_hint] + check.gg_args[index_of_hint + 2 :]
)
result = _run_gg_check(check)
result.hint = hint # Store the hint in the CheckResult object
# 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
Expand All @@ -339,8 +361,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
Expand All @@ -349,10 +369,15 @@ def run_checks(
# the idea is that displaying this run_command
# will give the person using Gatorgrade a way
# to quickly run the command that failed
if result.run_command != "":
if result.run_command != "" and result.hint != "":
rich.print(f"[blue] → Run this command: [green]{result.run_command}")
elif result.run_command != "":
rich.print(
f"[blue] → Run this command: [green]{result.run_command}\n"
)
# display a hint set by the instructor for specific failed checks
if result.hint != "":
rich.print(f"[blue] → Hint: [green]{result.hint}\n")
# determine how many of the checks passed and then
# compute the total percentage of checks passed
passed_count = len(results) - len(failed_results)
Expand Down
31 changes: 4 additions & 27 deletions tests/input/test_input_gg_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,6 @@ def test_parse_config_gg_check_in_file_context_contains_file():
assert "file.py" in output[0].gg_args


def test_parse_config_check_gg_matchfilefragment():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this test deleted? Is there a good replacement?

"""Test to make sure the description, check name, and options appear in the GatorGrader arguments."""
# Given a configuration file with a GatorGrader check
config = Path("tests/input/yml_test_files/gatorgrade_matchfilefragment.yml")
# 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 == [
"--description",
"Complete all TODOs",
"MatchFileFragment",
"--fragment",
"TODO",
"--count",
"0",
"--exact",
"--directory",
"path/to",
"--file",
"file.py",
]


def test_parse_config_gg_check_no_file_context_contains_no_file():
"""Test to make sure checks without a file context do not have a file path in GatorGrader arguments."""
# Given a configuration file with a GatorGrader check without a file context
Expand All @@ -49,13 +26,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():
Expand All @@ -76,13 +53,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():
Expand Down
Loading