From a83ca75837a250ac75089c69cdd119160ce7c1d7 Mon Sep 17 00:00:00 2001 From: Maarten Sijm <9739541+mpsijm@users.noreply.github.com> Date: Sun, 16 Mar 2025 13:27:37 +0100 Subject: [PATCH 1/2] [generate] Use input_visualizer/ instead of `visualizer:` key in generators.yaml --- bin/generate.py | 52 +++++++++++++++++++++---------------------------- bin/program.py | 5 +++-- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/bin/generate.py b/bin/generate.py index bf923226a..9568df1af 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -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)}") @@ -325,7 +326,6 @@ def __init__(self, generator_config): "generate", "copy", "solution", - "visualizer", "random_salt", "retries", "count", @@ -337,7 +337,6 @@ def __init__(self, generator_config): "testdata.yaml", "include", "solution", - "visualizer", "random_salt", "retries", ] @@ -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. @@ -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) @@ -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} @@ -386,7 +376,6 @@ def parse_random_salt(p, x, path): ] solution: SolutionInvocation - visualizer: Optional[VisualizerInvocation] random_salt: str retries: int @@ -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) @@ -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 @@ -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. @@ -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: @@ -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) @@ -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) diff --git a/bin/program.py b/bin/program.py index db79b11e4..3e59f3336 100644 --- a/bin/program.py +++ b/bin/program.py @@ -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, ) From 2a8ef503f0e61c6dd0d5ec30908aa6ea74549e33 Mon Sep 17 00:00:00 2001 From: Maarten Sijm <9739541+mpsijm@users.noreply.github.com> Date: Sun, 16 Mar 2025 13:29:04 +0100 Subject: [PATCH 2/2] [validate] Add OutputVisualizer as special type of Validator --- bin/problem.py | 12 ++++++-- bin/run.py | 18 ++++++++++-- bin/validate.py | 73 +++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/bin/problem.py b/bin/problem.py index 4c3006493..1203658fe 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -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, "*")) @@ -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) @@ -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: diff --git a/bin/run.py b/bin/run.py index 297cb1fc5..1aa847be5 100644 --- a/bin/run.py +++ b/bin/run.py @@ -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 @@ -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() @@ -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 @@ -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): diff --git a/bin/validate.py b/bin/validate.py index 90ba120b5..d97cb8b92 100644 --- a/bin/validate.py +++ b/bin/validate.py @@ -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" @@ -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" @@ -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" @@ -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 ------- @@ -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() @@ -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