Skip to content

Use new visualizers structure from 2023-07-draft #438

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

Closed
wants to merge 2 commits into from
Closed
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
52 changes: 22 additions & 30 deletions bin/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,14 @@ class VisualizerInvocation(Invocation):
def __init__(self, problem, string):
super().__init__(problem, string, allow_absolute=True, allow_relative=False)

# Run the visualizer, taking {name} as a command line argument.
# Stdin and stdout are not used.
# {name} is no longer used and hardcoded to `testcase` (see #273), and {seed} is also not used.
# Run the visualizer, passing the test case input to stdin.
def run(self, bar, cwd):
assert isinstance(self.program, program.Visualizer), "Visualizer program must be built!"

result = self.program.run(cwd, args=self._sub_args())
in_path = cwd / "testcase.in"

with in_path.open("rb") as in_file:
result = self.program.run(cwd, args=self._sub_args(), stdin=in_file)

if result.status == ExecStatus.TIMEOUT:
bar.debug(f"{Style.RESET_ALL}-> {shorten_path(self.problem, cwd)}")
Expand Down Expand Up @@ -325,7 +326,6 @@ def __init__(self, generator_config):
"generate",
"copy",
"solution",
"visualizer",
"random_salt",
"retries",
"count",
Expand All @@ -337,7 +337,6 @@ def __init__(self, generator_config):
"testdata.yaml",
"include",
"solution",
"visualizer",
"random_salt",
"retries",
]
Expand All @@ -348,7 +347,6 @@ def __init__(self, generator_config):

# Holds all inheritable configuration options. Currently:
# - config.solution
# - config.visualizer
# - config.random_salt
class Config:
# Used at each directory or testcase level.
Expand All @@ -360,13 +358,6 @@ def parse_solution(p, x, path):
return None
return SolutionInvocation(p, x)

@staticmethod
def parse_visualizer(p, x, path):
assert_type("Visualizer", x, [type(None), str], path)
if x is None:
return None
return VisualizerInvocation(p, x)

@staticmethod
def parse_random_salt(p, x, path):
assert_type("Random_salt", x, [type(None), str], path)
Expand All @@ -377,7 +368,6 @@ def parse_random_salt(p, x, path):
INHERITABLE_KEYS: Final[Sequence] = [
# True: use an AC submission by default when the solution: key is not present.
("solution", True, parse_solution),
("visualizer", None, parse_visualizer),
("random_salt", "", parse_random_salt),
# Non-portable keys only used by BAPCtools:
# The number of retries to run a generator when it fails, each time incrementing the {seed}
Expand All @@ -386,7 +376,6 @@ def parse_random_salt(p, x, path):
]

solution: SolutionInvocation
visualizer: Optional[VisualizerInvocation]
random_salt: str
retries: int

Expand Down Expand Up @@ -1053,21 +1042,21 @@ def generate_visualization():

if testcase.root in [*config.INVALID_CASE_DIRECTORIES, "valid_output"]:
return True
if not t.config.visualizer:
return True
if config.args.no_visualizer:
return True
if not generator_config.visualizer:
return True

visualizer_hash = {
"visualizer_hash": t.config.visualizer.hash(),
"visualizer": t.config.visualizer.cache_command(),
"visualizer_hash": generator_config.visualizer.hash(),
"visualizer": generator_config.visualizer.cache_command(),
}

if meta_yaml.get("visualizer_hash") == visualizer_hash:
return True

# Generate visualization
t.config.visualizer.run(bar, cwd)
generator_config.visualizer.run(bar, cwd)

meta_yaml["visualizer_hash"] = visualizer_hash
write_yaml(meta_yaml, meta_path, allow_yamllib=True)
Expand Down Expand Up @@ -1510,6 +1499,13 @@ def __init__(self, problem, restriction=None):
# Files that should be processed
self.restriction = restriction

# The input visualizer is shared between all test cases.
self.visualizer: Optional[VisualizerInvocation] = (
VisualizerInvocation(problem, "/input_visualizer")
if (problem.path / "input_visualizer").is_dir()
else None
)

if yaml_path.is_file():
self.yaml = read_yaml(yaml_path)
self.has_yaml = True
Expand Down Expand Up @@ -1834,7 +1830,6 @@ def add_included_case(t: TestcaseRule):
def build(self, build_visualizers=True, skip_double_build_warning=False):
generators_used: set[Path] = set()
solutions_used: set[Path] = set()
visualizers_used: set[Path] = set()

# Collect all programs that need building.
# Also, convert the default submission into an actual Invocation.
Expand All @@ -1855,14 +1850,12 @@ def collect_programs(t):
default_solution = DefaultSolutionInvocation(self)
t.config.solution = default_solution
solutions_used.add(t.config.solution.program_path)
if build_visualizers and t.config.visualizer:
visualizers_used.add(t.config.visualizer.program_path)

self.root_dir.walk(collect_programs, dir_f=None)

def build_programs(
program_type: type[program.Generator | program.Visualizer | run.Submission],
program_paths: set[Path],
program_paths: Iterable[Path],
):
programs = list[program.Generator | program.Visualizer | run.Submission]()
for program_path in program_paths:
Expand Down Expand Up @@ -1898,7 +1891,10 @@ def build_program(p):
# TODO: Consider building all types of programs in parallel as well.
build_programs(program.Generator, generators_used)
build_programs(run.Submission, solutions_used)
build_programs(program.Visualizer, visualizers_used)
build_programs(
program.Visualizer,
[self.visualizer.program_path] if build_visualizers and self.visualizer else [],
)

self.problem.validators(validate.InputValidator)
self.problem.validators(validate.AnswerValidator)
Expand All @@ -1907,10 +1903,6 @@ def build_program(p):
def cleanup_build_failures(t):
if t.config.solution and t.config.solution.program is None:
t.config.solution = None
if not build_visualizers or (
t.config.visualizer and t.config.visualizer.program is None
):
t.config.visualizer = None

self.root_dir.walk(cleanup_build_failures, dir_f=None)

Expand Down
12 changes: 10 additions & 2 deletions bin/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,13 @@ def _validators(
paths = [problem.path / validate.OutputValidator.source_dir]
else:
paths = [config.TOOLS_ROOT / "support" / "default_output_validator.cpp"]
elif cls == validate.OutputVisualizer:
# TODO: if not config.args.no_output_visualizer:
paths = (
[problem.path / validate.OutputVisualizer.source_dir]
if (problem.path / validate.OutputVisualizer.source_dir).is_dir()
else []
)
else:
paths = list(glob(problem.path / cls.source_dir, "*"))

Expand Down Expand Up @@ -946,7 +953,7 @@ def has_constraints_checking(f):
)
for path in paths
]
bar = ProgressBar(f"Building {cls.validator_type} validator", items=validators)
bar = ProgressBar(f"Building {cls.validator_type}", items=validators)

def build_program(p):
localbar = bar.start(p)
Expand All @@ -965,9 +972,10 @@ def prepare_run(problem):
if not testcases:
return False

# Pre build the output validator to prevent nested ProgressBars.
# Pre build the output validator and visualizer to prevent nested ProgressBars.
if not problem.validators(validate.OutputValidator):
return False
problem.validators(validate.OutputVisualizer)

submissions = problem.submissions()
if not submissions:
Expand Down
5 changes: 3 additions & 2 deletions bin/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,10 +604,11 @@ def __init__(self, problem: "Problem", path: Path, **kwargs):
)

# Run the visualizer.
# Stdin and stdout are not used.
def run(self, cwd, args=[]):
# Stdout is not used.
def run(self, cwd, stdin, args=[]):
assert self.run_command is not None
return self._exec_command(
self.run_command + args,
cwd=cwd,
stdin=stdin,
)
18 changes: 15 additions & 3 deletions bin/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from colorama import Fore, Style
from pathlib import Path
from typing import cast
from typing import Optional, cast

import config
import interactive
Expand Down Expand Up @@ -174,7 +174,9 @@ def run(self, bar, *, interaction=None, submission_args=None):

result.duration = max_duration

# Delete .out files larger than 1MB.
self._visualize_output(bar)

# Delete .out files larger than 1GB.
if (
not config.args.error
and self.out_path.is_file()
Expand Down Expand Up @@ -215,7 +217,7 @@ def _prepare_nextpass(self, nextpass):
shutil.move(nextpass, self.in_path)
return True

def _validate_output(self, bar):
def _validate_output(self, bar: ProgressBar) -> Optional[ExecResult]:
output_validators = self.problem.validators(validate.OutputValidator)
if not output_validators:
return None
Expand All @@ -227,6 +229,16 @@ def _validate_output(self, bar):
args=self.testcase.testdata_yaml_validator_args(output_validator, bar),
)

def _visualize_output(self, bar: ProgressBar) -> Optional[ExecResult]:
output_validators = self.problem.validators(validate.OutputVisualizer)
if not output_validators:
return None
return output_validators[0].run(
self.testcase,
self,
args=self.testcase.testdata_yaml_validator_args(output_validators[0], bar),
)


class Submission(program.Program):
def __init__(self, problem, path, skip_double_build_warning=False):
Expand Down
73 changes: 67 additions & 6 deletions bin/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ class InputValidator(Validator):
Also supports checktestdata and viva files, with different invocation.
"""

validator_type: Final[str] = "input"
validator_type: Final[str] = "input validator"

source_dir: Final[str] = "input_validators"

Expand Down Expand Up @@ -284,7 +284,7 @@ class AnswerValidator(Validator):
Also supports checktestdata and viva files, with different invocation.
"""

validator_type: Final[str] = "answer"
validator_type: Final[str] = "answer validator"

source_dir: Final[str] = "answer_validators"

Expand Down Expand Up @@ -335,7 +335,7 @@ class OutputValidator(Validator):
./validator input answer feedbackdir [arguments from problem.yaml] < output
"""

validator_type: Final[str] = "output"
validator_type: Final[str] = "output validator"

source_dir: Final[str] = "output_validator"

Expand All @@ -356,7 +356,7 @@ def run(
---------

mode: either a run.Run (namely, when validating submission output) or a Mode
(namely, when validation a testcase)
(namely, when validating a testcase)

Returns
-------
Expand All @@ -366,7 +366,7 @@ def run(
assert self.run_command is not None, "Validator should be built before running it"

if mode == Mode.INPUT:
raise ValueError("OutputValidator do not support Mode.INPUT")
raise ValueError("OutputValidator does not support Mode.INPUT")

in_path = testcase.in_path.resolve()
ans_path = testcase.ans_path.resolve()
Expand Down Expand Up @@ -410,7 +410,68 @@ def run(
return ret


AnyValidator = InputValidator | AnswerValidator | OutputValidator
class OutputVisualizer(Validator):
"""
Visualize the output of a submission

./visualizer input answer feedbackdir [arguments from problem.yaml] < output
"""

validator_type: Final[str] = "output visualizer"

source_dir: Final[str] = "output_visualizer"

def __init__(self, problem, path, **kwargs):
super().__init__(problem, path, "output_visualizer", **kwargs)

def run(
self,
testcase, # TODO #102: fix type errors after setting type to Testcase
mode,
constraints: Optional[ConstraintsDict] = None,
args=None,
) -> ExecResult:
"""
Run this validator on the given testcase.

Arguments
---------

run: run.Run (namely, when visualizing submission output)

Returns
-------
The ExecResult
"""

assert self.run_command is not None, "Validator should be built before running it"

in_path = testcase.in_path.resolve()
ans_path = testcase.ans_path.resolve()
run = mode # mode is actually a run
path = run.out_path
in_path = run.in_path

if self.language in Validator.FORMAT_VALIDATOR_LANGUAGES:
raise ValueError("Invalid output validator language")

# Only get the output_validator_args
_, _, arglist = self._run_helper(testcase, constraints, args)
cwd = run.feedbackdir
invocation = self.run_command + [in_path, ans_path, cwd]

with path.open() as file:
ret = self._exec_helper(
invocation + arglist,
exec_code_map=validator_exec_code_map,
stdin=file,
cwd=cwd,
)

return ret


AnyValidator = InputValidator | AnswerValidator | OutputValidator | OutputVisualizer


# Checks if byte is printable or whitespace
Expand Down
Loading