From b37cbfa90918fb6cd86c866f6fa18a62badb3899 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 5 Apr 2025 16:36:23 +0200 Subject: [PATCH 01/53] copied from mpsijm --- bin/generate.py | 49 ++++++++++++++++++++----------------------------- bin/program.py | 5 +++-- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/bin/generate.py b/bin/generate.py index 321774bb9..5e4039e12 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -193,13 +193,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)}") @@ -327,7 +328,6 @@ def __init__(self, generator_config): "generate", "copy", "solution", - "visualizer", "random_salt", "retries", "count", @@ -339,7 +339,6 @@ def __init__(self, generator_config): "testdata.yaml", "include", "solution", - "visualizer", "random_salt", "retries", ] @@ -350,7 +349,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. @@ -362,13 +360,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) @@ -379,7 +370,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} @@ -388,7 +378,6 @@ def parse_random_salt(p, x, path): ] solution: SolutionInvocation - visualizer: Optional[VisualizerInvocation] random_salt: str retries: int @@ -1049,21 +1038,21 @@ def generate_visualization(): if testcase.root in [*config.INVALID_CASE_DIRECTORIES, "valid_output"]: return True - if not t.config.visualizer: + if not generator_config.visualizer: return True if config.args.no_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) @@ -1504,6 +1493,12 @@ def __init__(self, problem, restriction=None): self.hashed_in = set() # 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) @@ -1829,7 +1824,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. @@ -1850,14 +1844,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: @@ -1893,7 +1885,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) @@ -1902,10 +1897,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 fd38bd3d8..b336b7117 100644 --- a/bin/program.py +++ b/bin/program.py @@ -605,10 +605,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 f647c2de733c8def70ecb8a7730397c4c4a3aca7 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 5 Apr 2025 16:48:00 +0200 Subject: [PATCH 02/53] update identity --- test/problems/identity/generators/generators.yaml | 3 --- test/problems/identity/input_visualizer_disabled/run | 4 ++++ .../visualize.asy | 0 test/problems/identity/visualizers/run | 6 ------ 4 files changed, 4 insertions(+), 9 deletions(-) create mode 100755 test/problems/identity/input_visualizer_disabled/run rename test/problems/identity/{visualizers => input_visualizer_disabled}/visualize.asy (100%) delete mode 100755 test/problems/identity/visualizers/run diff --git a/test/problems/identity/generators/generators.yaml b/test/problems/identity/generators/generators.yaml index d8e5c0f20..65afceab4 100644 --- a/test/problems/identity/generators/generators.yaml +++ b/test/problems/identity/generators/generators.yaml @@ -1,6 +1,4 @@ solution: /submissions/accepted/author.py -# The visualizer is disabled to speed up testing. -#visualizer: /visualizers random_salt: "abc" generators: @@ -133,7 +131,6 @@ data: solution: /generators/solution.c generate: random_gen.py {seed:7} testcase_dict_3: - visualizer: generate: random_gen.py {seed:8} unused_args_1: > # Spread arguments over multiple lines. random_gen.py diff --git a/test/problems/identity/input_visualizer_disabled/run b/test/problems/identity/input_visualizer_disabled/run new file mode 100755 index 000000000..53ead392e --- /dev/null +++ b/test/problems/identity/input_visualizer_disabled/run @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +set -e +asy -f png $(dirname $0)/visualize.asy -o testcase.png diff --git a/test/problems/identity/visualizers/visualize.asy b/test/problems/identity/input_visualizer_disabled/visualize.asy similarity index 100% rename from test/problems/identity/visualizers/visualize.asy rename to test/problems/identity/input_visualizer_disabled/visualize.asy diff --git a/test/problems/identity/visualizers/run b/test/problems/identity/visualizers/run deleted file mode 100755 index b82da4c8d..000000000 --- a/test/problems/identity/visualizers/run +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env sh - -set -e - -name=$1 -cat $name.in $name.ans | asy -f png $(dirname $0)/visualize.asy -o $name.png From b143e87e81320514a04663dca551999edec3e912 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 5 Apr 2025 16:57:31 +0200 Subject: [PATCH 03/53] update schemas --- support/schemas/generators.cue | 4 +-- support/schemas/generators_yaml_schema.json | 27 ------------------- .../invalid_yaml/bad_generators.yaml | 14 +++------- .../invalid_yaml/invalid.generators.yaml | 21 +++------------ .../valid_yaml/rich-generators.yaml | 1 - 5 files changed, 8 insertions(+), 59 deletions(-) diff --git a/support/schemas/generators.cue b/support/schemas/generators.cue index fa5f20b32..085d884a8 100644 --- a/support/schemas/generators.cue +++ b/support/schemas/generators.cue @@ -20,12 +20,10 @@ import "strings" _parts: [#path, ...#command_args] } -// Test cases and test groups allow configuration of solution, visualiser, and random salt. +// Test cases and test groups allow configuration of solution, and random salt. #config: { // Path to solution starts with slash, such as "/submissions/accepted/foo.py" solution?: #filepath & =~"^/" - // Visualiser can be omitted to disable visualisation, may not use {count} - visualizer?: #command & =~"^/" & !~"\\{count" | null random_salt?: string } diff --git a/support/schemas/generators_yaml_schema.json b/support/schemas/generators_yaml_schema.json index f75b294bb..6d3a5b09c 100644 --- a/support/schemas/generators_yaml_schema.json +++ b/support/schemas/generators_yaml_schema.json @@ -43,9 +43,6 @@ "solution": { "$ref": "#/$defs/solution" }, - "visualizer": { - "$ref": "#/$defs/visualizer" - } }, "additionalProperties": false }, @@ -221,9 +218,6 @@ "title": "Hint", "description": "Feedback shown to the solver about this test case given as a string" }, - "visualizer": { - "$ref": "#/$defs/visualizer" - }, "random_salt": { "$ref": "#/$defs/random_salt" }, @@ -235,24 +229,6 @@ } ] }, - "visualizer": { - "title": "Visualizer", - "description": "Absolute path to and arguments for a visualizer. Leave empty to disable visualizion.", - "examples": [ - "/visualizer", - "/visualizers/asy.py", - "/visualizers/vis --large" - ], - "oneOf": [ - { - "type": "string", - "pattern": "^\/([^{}]|\\{name\\})*(\\{seed(:[0-9]+)?\\})?([^{}]|\\{name\\})*$" - }, - { - "type": "null" - } - ] - }, "random_salt": { "title": "Random Salt", "type": "string", @@ -302,9 +278,6 @@ "solution": { "$ref": "#/$defs/solution" }, - "visualizer": { - "$ref": "#/$defs/visualizer" - }, "random_salt": { "$ref": "#/$defs/random_salt" }, diff --git a/test/yaml/generators/invalid_yaml/bad_generators.yaml b/test/yaml/generators/invalid_yaml/bad_generators.yaml index 7c1f24525..472baf0a0 100644 --- a/test/yaml/generators/invalid_yaml/bad_generators.yaml +++ b/test/yaml/generators/invalid_yaml/bad_generators.yaml @@ -23,8 +23,8 @@ solution: true --- solution: false --- -# visualizer must be null or string -visualizer: 0 +# visualizer is legacy +visualizer: "/foo/bar/baz" --- # random_salt must be null or string random_salt: 0 @@ -164,21 +164,13 @@ data: data: ab: /generators/dir/gen.py --- -# Solution ans visualizer must have an absolute path: +# Solution must have an absolute path: solution: a --- solution: a/b --- solution: a 1 2 --- -visualizer: a ---- -visualizer: a/b ---- -visualizer: a 1 2 ---- -visualizer: a {name} ---- # Directories may not have generate:. generate: xyz --- diff --git a/test/yaml/generators/invalid_yaml/invalid.generators.yaml b/test/yaml/generators/invalid_yaml/invalid.generators.yaml index b78f8e92a..835e424e8 100644 --- a/test/yaml/generators/invalid_yaml/invalid.generators.yaml +++ b/test/yaml/generators/invalid_yaml/invalid.generators.yaml @@ -29,8 +29,8 @@ data: {sample: {data: []}, secret: {data: []}} solution: false data: {sample: {data: []}, secret: {data: []}} --- -# visualizer must be null or string -visualizer: 0 +# visualizer is legacy +visualizer: "/foo/bar/baz" data: {sample: {data: []}, secret: {data: []}} --- # random_salt must be null or string @@ -266,7 +266,7 @@ data: a: generate: /generators/gen.py --- -# Solution and visualizer must have an absolute path: +# Solution must have an absolute path: solution: a data: {sample: {data: []}, secret: {data: []}} --- @@ -276,18 +276,6 @@ data: {sample: {data: []}, secret: {data: []}} solution: a 1 2 data: {sample: {data: []}, secret: {data: []}} --- -visualizer: a -data: {sample: {data: []}, secret: {data: []}} ---- -visualizer: a/b -data: {sample: {data: []}, secret: {data: []}} ---- -visualizer: a 1 2 -data: {sample: {data: []}, secret: {data: []}} ---- -visualizer: a {name} -data: {sample: {data: []}, secret: {data: []}} ---- ## No toplevel generate TODO #generate: xyz #data: {sample: {data: []}, secret: {data: []}} @@ -405,6 +393,5 @@ data: data: - '': in: '1 2' - visualizer: "/ab/c" # this is fine - testdata.yaml: # this is not + testdata.yaml: # this is not ok input_validator_args: "connected" diff --git a/test/yaml/generators/valid_yaml/rich-generators.yaml b/test/yaml/generators/valid_yaml/rich-generators.yaml index b2f5d743f..8341f8d09 100644 --- a/test/yaml/generators/valid_yaml/rich-generators.yaml +++ b/test/yaml/generators/valid_yaml/rich-generators.yaml @@ -30,7 +30,6 @@ data: 'group_with_testdata': testdata.yaml: input_validator_args: "--connected --max_n 2000" - visualizer: "/foo/bar/baz" data: 'a': my_generator invalid_input: From f876cc01750267d5bc37abf32b735d4540a7244e Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 5 Apr 2025 17:03:51 +0200 Subject: [PATCH 04/53] update doc --- doc/generators.md | 2 -- doc/generators.yaml | 16 ++-------------- skel/problem/generators/generators.yaml | 1 - 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/doc/generators.md b/doc/generators.md index edf424ecb..572ed2700 100644 --- a/doc/generators.md +++ b/doc/generators.md @@ -28,8 +28,6 @@ The two main object types are `directory` and `generator`. The root of `generato - `testdata.yaml`: Optional yaml configuration that will be copied to `testdata.yaml` in this directory. - `solution`: Optional invocation of a solution to be used to generate `.ans` files. Set to empty to disable generating `.ans`. (Useful for e.g. the `data/samples/` directory.) This must be an absolute path relative to the problem root. -- `visualizer`: Optional invocation of a visualizer to generate visualizations for each test case in this directory. - This must be an absolute path relative to the problem root. Set to empty to disable. - `random_salt`: Optional string that will be prepended to each command before computing its `{seed}`. May be used to regenerate all random cases and to prevent predictable seeds. - `data`: The test cases / test groups contained in this directory. This may take two forms: - A dictionary, each key is the name of a test case/test group, and each value must be a `directory` or `generator` object. diff --git a/doc/generators.yaml b/doc/generators.yaml index b6e13a7bd..4d3f101cb 100644 --- a/doc/generators.yaml +++ b/doc/generators.yaml @@ -11,17 +11,6 @@ # TOOLING: may pick a default if not specified, but should raise an error. solution: /submissions/accepted/sol.py -# The visualizer is used when no suitable image was generated already. -# This should read `testcase.in` and/or `testcase.ans` from the current working -# directory, and write `testcase.ext` for an extension in: -# .png, .jpg, .svg -# -# This must be the absolute path, starting in the problem root. -# -# TOOLING: may provide a flag to make running this optional, as it can be slow -# and usually isn't required. -visualizer: /visualizers/vis.py - # Optionally, a salt for generating the {seed} variables. Will be prepended to # the command being run. random_salt: abcd @@ -169,12 +158,11 @@ data: "13": write_in_and_ans.py # To override the global/testgroup configuration on a per-testcase basis, - # a dictionary may be used. This allows the solution: and visualizer: keys, + # a dictionary may be used. This allows the solution: key, # as well as the generate: key which contains the command to execute. - 14_no_visualizer: + 14_override: generate: large_case_generator.py 1000000 solution: /generators/gnu_multi_precision.cpp - visualizer: # Empty to disable the visualizer here. random_salt: "123" # An entry must include *some* key that produces an in-file, diff --git a/skel/problem/generators/generators.yaml b/skel/problem/generators/generators.yaml index 2c3a3fe72..964c91934 100644 --- a/skel/problem/generators/generators.yaml +++ b/skel/problem/generators/generators.yaml @@ -1,5 +1,4 @@ #solution: /submissions/accepted/submission.py -#visualizer: /visualizers/asy.sh version: 2025-02 # use this version of the generators framework {%testdata_yaml_comment%}testdata.yaml: From 8dc03ca97e70bb85a45b65a889ddd8ede3003cf6 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 5 Apr 2025 22:23:18 +0200 Subject: [PATCH 05/53] default value for action --- bin/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/config.py b/bin/config.py index eba8a1341..6e3cc3fe9 100644 --- a/bin/config.py +++ b/bin/config.py @@ -108,6 +108,7 @@ "jobs": (os.cpu_count() or 1) // 2, "time": 600, # Used for `bt fuzz` "verbose": 0, + "action": None, } From 6c80df91023311251790a9e032e36fb0d5538e9d Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 5 Apr 2025 22:45:45 +0200 Subject: [PATCH 06/53] move visualizer --- bin/generate.py | 19 ++++++++++++------- bin/program.py | 22 ---------------------- bin/tools.py | 7 +++++++ bin/visualize.py | 29 +++++++++++++++++++++++++++++ doc/commands.md | 1 + 5 files changed, 49 insertions(+), 29 deletions(-) create mode 100644 bin/visualize.py diff --git a/bin/generate.py b/bin/generate.py index 5e4039e12..4ee3da33b 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -16,6 +16,7 @@ import program import run import validate +import visualize from testcase import Testcase from verdicts import Verdict from problem import Problem @@ -121,7 +122,9 @@ def __init__(self, problem: Problem, string: str, *, allow_absolute: bool, allow raise ParseException("{seed(:[0-9]+)} may appear at most once.") # Automatically set self.program when that program has been built. - self.program: Optional[program.Generator | program.Visualizer | run.Submission] = None + self.program: Optional[program.Generator | visualize.InputVisualizer | run.Submission] = ( + None + ) def callback(program): self.program = program @@ -195,7 +198,9 @@ def __init__(self, problem, string): # 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!" + assert isinstance(self.program, visualize.InputVisualizer), ( + "Input Visualizer program must be built!" + ) in_path = cwd / "testcase.in" @@ -204,10 +209,10 @@ def run(self, bar, cwd): if result.status == ExecStatus.TIMEOUT: bar.debug(f"{Style.RESET_ALL}-> {shorten_path(self.problem, cwd)}") - bar.error(f"Visualizer TIMEOUT after {result.duration}s") + bar.error(f"Input Visualizer TIMEOUT after {result.duration}s") elif not result.status: bar.debug(f"{Style.RESET_ALL}-> {shorten_path(self.problem, cwd)}") - bar.error("Visualizer failed", result.err) + bar.error("Input Visualizer failed", result.err) if result.status and config.args.error and result.err: bar.log("stderr", result.err) @@ -1848,10 +1853,10 @@ def collect_programs(t): self.root_dir.walk(collect_programs, dir_f=None) def build_programs( - program_type: type[program.Generator | program.Visualizer | run.Submission], + program_type: type[program.Generator | visualize.InputVisualizer | run.Submission], program_paths: Iterable[Path], ): - programs = list[program.Generator | program.Visualizer | run.Submission]() + programs = list[program.Generator | visualize.InputVisualizer | run.Submission]() for program_path in program_paths: path = self.problem.path / program_path if program_type is program.Generator and program_path in self.generators: @@ -1886,7 +1891,7 @@ def build_program(p): build_programs(program.Generator, generators_used) build_programs(run.Submission, solutions_used) build_programs( - program.Visualizer, + visualize.InputVisualizer, [self.visualizer.program_path] if build_visualizers and self.visualizer else [], ) diff --git a/bin/program.py b/bin/program.py index b336b7117..1b399fcbe 100644 --- a/bin/program.py +++ b/bin/program.py @@ -591,25 +591,3 @@ def run(self, bar, cwd, name, args=[]): return result return result - - -class Visualizer(Program): - def __init__(self, problem: "Problem", path: Path, **kwargs): - super().__init__( - problem, - path, - "visualizers", - limits={"timeout": problem.limits.visualizer_time}, - substitute_constants=True, - **kwargs, - ) - - # Run the visualizer. - # 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, - ) diff --git a/bin/tools.py b/bin/tools.py index 50a6c1ea6..c63ccbedc 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -693,6 +693,13 @@ def build_parser(): action="store_true", help="Do not run `generate` before running submissions.", ) + runparser.add_argument( + "--visualizer", + default=True, + dest="no_visualizer", + action="store_false", + help="Also run the output visualizer.", + ) runparser.add_argument( "--all", "-a", diff --git a/bin/visualize.py b/bin/visualize.py new file mode 100644 index 000000000..da5939000 --- /dev/null +++ b/bin/visualize.py @@ -0,0 +1,29 @@ +from pathlib import Path +from typing import TYPE_CHECKING + +import program + +if TYPE_CHECKING: # Prevent circular import: https://stackoverflow.com/a/39757388 + from problem import Problem + + +class InputVisualizer(program.Program): + def __init__(self, problem: "Problem", path: Path, **kwargs): + super().__init__( + problem, + path, + "input_visualizer", + limits={"timeout": problem.limits.visualizer_time}, + substitute_constants=True, + **kwargs, + ) + + # Run the visualizer. + # 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, + ) diff --git a/doc/commands.md b/doc/commands.md index 8ae849ed4..102f03603 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -112,6 +112,7 @@ Use `bt run -v` to show results for all testcases. - `--overview`/`-o`: Print a live overview of the received verdicts for all submissions and testcases. If combined with `--no-bar` only the final table is printed. - `--no-testcase-sanity-checks`: when passed, all sanity checks on the testcases are skipped. You might want to set this in `.bapctools.yaml`. - `--sanitizer`: when passed, run submissions with additional sanitizer flags (currently only C++). Note that this removes all memory limits for submissions. +- `--visualizer`: when passed, run the output visualizer. ## `test` From c493fd33036463a76bd6b4fc21b5bbf0cc6855e8 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 5 Apr 2025 22:59:17 +0200 Subject: [PATCH 07/53] remove testcases --- test/yaml/generators/invalid_yaml/bad_generators.yaml | 3 --- test/yaml/generators/invalid_yaml/invalid.generators.yaml | 4 ---- 2 files changed, 7 deletions(-) diff --git a/test/yaml/generators/invalid_yaml/bad_generators.yaml b/test/yaml/generators/invalid_yaml/bad_generators.yaml index 472baf0a0..2ce4851af 100644 --- a/test/yaml/generators/invalid_yaml/bad_generators.yaml +++ b/test/yaml/generators/invalid_yaml/bad_generators.yaml @@ -23,9 +23,6 @@ solution: true --- solution: false --- -# visualizer is legacy -visualizer: "/foo/bar/baz" ---- # random_salt must be null or string random_salt: 0 --- diff --git a/test/yaml/generators/invalid_yaml/invalid.generators.yaml b/test/yaml/generators/invalid_yaml/invalid.generators.yaml index 835e424e8..4204d23d0 100644 --- a/test/yaml/generators/invalid_yaml/invalid.generators.yaml +++ b/test/yaml/generators/invalid_yaml/invalid.generators.yaml @@ -29,10 +29,6 @@ data: {sample: {data: []}, secret: {data: []}} solution: false data: {sample: {data: []}, secret: {data: []}} --- -# visualizer is legacy -visualizer: "/foo/bar/baz" -data: {sample: {data: []}, secret: {data: []}} ---- # random_salt must be null or string random_salt: 0 data: {sample: {data: []}, secret: {data: []}} From 0e150f4fb27ca1766355514fbd5a3cc53017b294 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 5 Apr 2025 23:32:42 +0200 Subject: [PATCH 08/53] add warning for outdated visualizer --- bin/upgrade.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/upgrade.py b/bin/upgrade.py index e9b6bff37..f37dd7a3a 100644 --- a/bin/upgrade.py +++ b/bin/upgrade.py @@ -91,6 +91,11 @@ def upgrade_generators_yaml(problem_path: Path, bar: ProgressBar) -> None: changed = False + if "visualizer" in yaml_data: + warn( + "Cannot automatically upgrade 'visualizer'.\n - move visualizer to 'input_visualizer/'\n - use stdin to read testcase" + ) + if "data" in yaml_data and isinstance(yaml_data["data"], dict): data = cast(CommentedMap, yaml_data["data"]) From 59a596543db5946d936b0d75b6ad9fe5cb9bada4 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 6 Apr 2025 00:07:14 +0200 Subject: [PATCH 09/53] add output visualizer (and made open more consistent) --- bin/generate.py | 4 ++-- bin/problem.py | 2 +- bin/util.py | 6 +++--- bin/validate.py | 11 +++++----- bin/visualize.py | 56 +++++++++++++++++++++++++++++++++++++++++------- 5 files changed, 59 insertions(+), 20 deletions(-) diff --git a/bin/generate.py b/bin/generate.py index 4ee3da33b..b7e0ac144 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -204,8 +204,8 @@ def run(self, bar, cwd): 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) + # TODO should the input validator get any args? + result = self.program.run(in_path, cwd) if result.status == ExecStatus.TIMEOUT: bar.debug(f"{Style.RESET_ALL}-> {shorten_path(self.problem, cwd)}") diff --git a/bin/problem.py b/bin/problem.py index db8524dcf..18da553c7 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -383,7 +383,7 @@ def _determine_statement_languages(self): unnormalised_yamlname = self.settings.name[lang] yamlname = " ".join(unnormalised_yamlname.split()) texpath = self.path / latex.PdfType.PROBLEM.path(lang) - with open(texpath) as texfile: + with texpath.open() as texfile: match texname := latex.get_argument_for_command(texfile, "problemname"): case None: error(rf"No \problemname found in {texpath.name}") diff --git a/bin/util.py b/bin/util.py index f86911338..bea2cc4bb 100644 --- a/bin/util.py +++ b/bin/util.py @@ -791,7 +791,7 @@ def write_yaml( exit(1) if path is None: return yamllib.dump(data) - with open(path, "w") as stream: + with path.open("w") as stream: yamllib.dump(data, stream) return None with write_yaml_lock: @@ -1478,7 +1478,7 @@ def hash_file_content(file: Path, buffer_size: int = 65536) -> str: raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(file)) sha = hashlib.sha512(usedforsecurity=False) - with open(file, "rb") as f: + with file.open("rb") as f: while True: data = f.read(buffer_size) if not data: @@ -1496,7 +1496,7 @@ def hash_file(file: Path, buffer_size: int = 65536) -> str: sha.update(len(name).to_bytes(8, "big")) sha.update(name) - with open(file, "rb") as f: + with file.open("rb") as f: while True: data = f.read(buffer_size) if not data: diff --git a/bin/validate.py b/bin/validate.py index 0eda6c1f7..748a91e59 100644 --- a/bin/validate.py +++ b/bin/validate.py @@ -174,7 +174,7 @@ def format_exec_code_map(returncode): return ExecStatus.ERROR if self.language == "checktestdata": - with main_path.open() as main_file: + with main_path.open("rb") as main_file: return self._exec_command( self.run_command, exec_code_map=format_exec_code_map, @@ -263,7 +263,7 @@ def run( invocation = self.run_command.copy() - with testcase.in_path.open() as in_file: + with testcase.in_path.open("rb") as in_file: ret = self._exec_helper( invocation + arglist, exec_code_map=validator_exec_code_map, @@ -316,7 +316,7 @@ def run( invocation = self.run_command + [testcase.in_path.resolve()] - with testcase.ans_path.open() as ans_file: + with testcase.ans_path.open("rb") as ans_file: ret = self._exec_helper( invocation + arglist, exec_code_map=validator_exec_code_map, @@ -388,7 +388,6 @@ def run( assert mode != Mode.INPUT # mode is actually a Run path = mode.out_path - in_path = mode.in_path if self.language in Validator.FORMAT_VALIDATOR_LANGUAGES: raise ValueError("Invalid output validator language") @@ -398,7 +397,7 @@ def run( cwd = mode.feedbackdir invocation = self.run_command + [in_path, ans_path, cwd] - with path.open() as file: + with path.open("rb") as file: ret = self._exec_helper( invocation + arglist, exec_code_map=validator_exec_code_map, @@ -460,7 +459,7 @@ def sanity_check(problem, path, bar, strict_whitespace=True): if not path.exists(): fatal(f"{path} not found during sanity check") - with open(path, "rb") as file: + with path.open("rb") as file: name = { ".in": "Input", ".ans": "Answer", diff --git a/bin/visualize.py b/bin/visualize.py index da5939000..1dde5db40 100644 --- a/bin/visualize.py +++ b/bin/visualize.py @@ -1,7 +1,11 @@ from pathlib import Path -from typing import TYPE_CHECKING +from typing import Optional, TYPE_CHECKING import program +import testcase +import run + +from util import * if TYPE_CHECKING: # Prevent circular import: https://stackoverflow.com/a/39757388 from problem import Problem @@ -18,12 +22,48 @@ def __init__(self, problem: "Problem", path: Path, **kwargs): **kwargs, ) - # Run the visualizer. + # Run the visualizer (should create a testcase. file). # 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, + def run(self, in_path: Path, cwd: Path, args: Optional[list[str]] = None) -> ExecResult: + assert self.run_command is not None, "Input Visualizer should be built before running it" + + with in_path.open("rb") as in_file: + return self._exec_command( + self.run_command + (args or []), + cwd=cwd, + stdin=in_file, + ) + + +class OutputVisualizer(program.Program): + def __init__(self, problem: "Problem", path: Path, **kwargs): + super().__init__( + problem, + path, + "output_visualizer", + limits={"timeout": problem.limits.visualizer_time}, + substitute_constants=True, + **kwargs, ) + + # Run the visualizer. + # Stdout is not used. + def run( + self, + testcase: testcase.Testcase, + run: run.Run, + args: Optional[list[str]] = None, + ) -> ExecResult: + assert self.run_command is not None, "Output Visualizer should be built before running it" + + in_path = testcase.in_path.resolve() + ans_path = testcase.ans_path.resolve() + out_path = run.out_path + cwd = run.feedbackdir + + with out_path.open("rb") as out_file: + return self._exec_command( + self.run_command + [in_path, ans_path, cwd] + (args or []), + stdin=out_file, + cwd=cwd, + ) From c720c20ad0a71c96c3849c52ede9febe925d3366 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 6 Apr 2025 00:21:13 +0200 Subject: [PATCH 10/53] add comments --- bin/validate.py | 1 + bin/visualize.py | 34 ++++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/bin/validate.py b/bin/validate.py index 748a91e59..a91c22e3e 100644 --- a/bin/validate.py +++ b/bin/validate.py @@ -388,6 +388,7 @@ def run( assert mode != Mode.INPUT # mode is actually a Run path = mode.out_path + in_path = mode.in_path # relevant for multipass if self.language in Validator.FORMAT_VALIDATOR_LANGUAGES: raise ValueError("Invalid output validator language") diff --git a/bin/visualize.py b/bin/visualize.py index 1dde5db40..8cff705b0 100644 --- a/bin/visualize.py +++ b/bin/visualize.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Optional, TYPE_CHECKING +from typing import Final, Optional, TYPE_CHECKING import program import testcase @@ -12,17 +12,28 @@ class InputVisualizer(program.Program): + """ + Visualizes an input file (such as "testcase.in"), called as: + + ./visualizer [args] < input + + """ + + validator_type: Final[str] = "input" + + source_dir: Final[str] = "input_visualizer" + def __init__(self, problem: "Problem", path: Path, **kwargs): super().__init__( problem, path, - "input_visualizer", + InputVisualizer.source_dir, limits={"timeout": problem.limits.visualizer_time}, substitute_constants=True, **kwargs, ) - # Run the visualizer (should create a testcase. file). + # Run the visualizer (should create a testcase. file). # Stdout is not used. def run(self, in_path: Path, cwd: Path, args: Optional[list[str]] = None) -> ExecResult: assert self.run_command is not None, "Input Visualizer should be built before running it" @@ -36,18 +47,29 @@ def run(self, in_path: Path, cwd: Path, args: Optional[list[str]] = None) -> Exe class OutputVisualizer(program.Program): + """ + Visualizes the output of a submission + + ./visualizer input answer feedbackdir [args] < output + + """ + + validator_type: Final[str] = "output" + + source_dir: Final[str] = "output_visualizer" + def __init__(self, problem: "Problem", path: Path, **kwargs): super().__init__( problem, path, - "output_visualizer", + OutputVisualizer.source_dir, limits={"timeout": problem.limits.visualizer_time}, substitute_constants=True, **kwargs, ) # Run the visualizer. - # Stdout is not used. + # should write to feedbackdir/judgeimage. def run( self, testcase: testcase.Testcase, @@ -56,7 +78,7 @@ def run( ) -> ExecResult: assert self.run_command is not None, "Output Visualizer should be built before running it" - in_path = testcase.in_path.resolve() + in_path = run.in_path # relevant for multipass ans_path = testcase.ans_path.resolve() out_path = run.out_path cwd = run.feedbackdir From 5394cb76cb634a29a86322febf55d4be3fd96605 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 6 Apr 2025 16:07:14 +0200 Subject: [PATCH 11/53] implemented output visualizer --- bin/config.py | 1 + bin/export.py | 3 + bin/generate.py | 73 +++++++------------ bin/problem.py | 40 +++++++++- bin/run.py | 28 +++++-- bin/tools.py | 2 +- bin/upgrade.py | 2 +- bin/validate.py | 4 +- bin/visualize.py | 29 ++++---- .../identity/input_visualizer_disabled/run | 3 +- .../identity/output_visualizer_disabled/run | 5 ++ .../output_visualizer_disabled/visualize.asy | 5 ++ 12 files changed, 123 insertions(+), 72 deletions(-) create mode 100755 test/problems/identity/output_visualizer_disabled/run create mode 100644 test/problems/identity/output_visualizer_disabled/visualize.asy diff --git a/bin/config.py b/bin/config.py index 6e3cc3fe9..1d5660633 100644 --- a/bin/config.py +++ b/bin/config.py @@ -109,6 +109,7 @@ "time": 600, # Used for `bt fuzz` "verbose": 0, "action": None, + "no_visualizer": True, } diff --git a/bin/export.py b/bin/export.py index a336ed2b1..f1c1097a3 100644 --- a/bin/export.py +++ b/bin/export.py @@ -13,6 +13,7 @@ from latex import PdfType from problem import Problem from validate import InputValidator, AnswerValidator, OutputValidator +from visualize import InputVisualizer, OutputVisualizer def select_languages(problems: list[Problem]) -> list[str]: @@ -125,6 +126,8 @@ def build_problem_zip(problem: Problem, output: Path) -> bool: ("submissions/accepted/**/*", True), ("submissions/*/**/*", False), ("attachments/**/*", problem.interactive or problem.multi_pass), + (f"{InputVisualizer.source_dir}/**/*", False), + (f"{OutputVisualizer.source_dir}/**/*", False), ] # Do not include PDFs for kattis. diff --git a/bin/generate.py b/bin/generate.py index b7e0ac144..76bb801ad 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -89,7 +89,6 @@ def resolve_path(path, *, allow_absolute, allow_relative): # The following classes inherit from Invocation: # - GeneratorInvocation # - SolutionInvocation -# - VisualizerInvocation class Invocation: SEED_REGEX: Final[re.Pattern[str]] = re.compile(r"\{seed(:[0-9]+)?\}") NAME_REGEX: Final[re.Pattern[str]] = re.compile(r"\{name\}") @@ -192,33 +191,6 @@ def run(self, bar, cwd, name, seed, retries=1): return result -class VisualizerInvocation(Invocation): - def __init__(self, problem, string): - super().__init__(problem, string, allow_absolute=True, allow_relative=False) - - # Run the visualizer, passing the test case input to stdin. - def run(self, bar, cwd): - assert isinstance(self.program, visualize.InputVisualizer), ( - "Input Visualizer program must be built!" - ) - - in_path = cwd / "testcase.in" - - # TODO should the input validator get any args? - result = self.program.run(in_path, cwd) - - if result.status == ExecStatus.TIMEOUT: - bar.debug(f"{Style.RESET_ALL}-> {shorten_path(self.problem, cwd)}") - bar.error(f"Input Visualizer TIMEOUT after {result.duration}s") - elif not result.status: - bar.debug(f"{Style.RESET_ALL}-> {shorten_path(self.problem, cwd)}") - bar.error("Input Visualizer failed", result.err) - - if result.status and config.args.error and result.err: - bar.log("stderr", result.err) - return result - - class SolutionInvocation(Invocation): def __init__(self, problem, string): super().__init__(problem, string, allow_absolute=True, allow_relative=False) @@ -1043,24 +1015,43 @@ def generate_visualization(): if testcase.root in [*config.INVALID_CASE_DIRECTORIES, "valid_output"]: return True - if not generator_config.visualizer: - return True if config.args.no_visualizer: return True + visualizer = problem.visualizer(visualize.InputVisualizer) + if visualizer is None: + return True + # TODO if the input visualizer gets args these need to be hashed as well visualizer_hash = { - "visualizer_hash": generator_config.visualizer.hash(), - "visualizer": generator_config.visualizer.cache_command(), + "visualizer_hash": visualizer.hash, } if meta_yaml.get("visualizer_hash") == visualizer_hash: return True # Generate visualization - generator_config.visualizer.run(bar, cwd) + in_path = cwd / "testcase.in" + ans_path = cwd / "testcase.ans" + assert in_path.is_file() + assert ans_path.is_file() + + result = visualizer.run(in_path, ans_path, cwd) - meta_yaml["visualizer_hash"] = visualizer_hash - write_yaml(meta_yaml, meta_path, allow_yamllib=True) + if result.status == ExecStatus.TIMEOUT: + bar.debug(f"{Style.RESET_ALL}-> {shorten_path(problem, cwd)}") + bar.error(f"Input Visualizer TIMEOUT after {result.duration}s") + elif not result.status: + bar.debug(f"{Style.RESET_ALL}-> {shorten_path(problem, cwd)}") + bar.error("Input Visualizer failed", result.err) + + if result.status and config.args.error and result.err: + bar.log("stderr", result.err) + + if result.status: + meta_yaml["visualizer_hash"] = visualizer_hash + write_yaml(meta_yaml, meta_path, allow_yamllib=True) + + # errors in the visualizer are not critical return True def copy_generated(): @@ -1498,12 +1489,6 @@ def __init__(self, problem, restriction=None): self.hashed_in = set() # 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) @@ -1890,10 +1875,8 @@ 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( - visualize.InputVisualizer, - [self.visualizer.program_path] if build_visualizers and self.visualizer else [], - ) + if build_visualizers: + self.problem.visualizer(visualize.InputVisualizer) self.problem.validators(validate.InputValidator) self.problem.validators(validate.AnswerValidator) diff --git a/bin/problem.py b/bin/problem.py index 18da553c7..43c804610 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Sequence from pathlib import Path -from typing import Any, Final, Literal, Optional, TYPE_CHECKING +from typing import Any, Final, Literal, Optional, overload, TYPE_CHECKING if TYPE_CHECKING: # Prevent circular import: https://stackoverflow.com/a/39757388 from program import Program @@ -20,6 +20,7 @@ import validate import validator_tests import verdicts +import visualize from util import * from colorama import Fore, Style @@ -337,6 +338,9 @@ def __init__(self, path: Path, tmpdir: Path, label: Optional[str] = None): tuple[type[validate.AnyValidator], bool], list[validate.AnyValidator] ]() self._validators_warn_cache = set[tuple[type[validate.AnyValidator], bool]]() + self._visualizer_cache = dict[ + type[visualize.AnyVisualizer], Optional[visualize.AnyVisualizer] + ]() self._programs = dict[Path, "Program"]() self._program_callbacks = dict[Path, list[Callable[["Program"], None]]]() # Dictionary from path to parsed file contents. @@ -857,6 +861,34 @@ def build_program(p): assert isinstance(problem._submissions, list) return problem._submissions.copy() + @overload + def visualizer( + problem, cls: type[visualize.InputVisualizer] + ) -> Optional[visualize.InputVisualizer]: ... + @overload + def visualizer( + problem, cls: type[visualize.OutputVisualizer] + ) -> Optional[visualize.OutputVisualizer]: ... + def visualizer( + problem, cls: type[visualize.AnyVisualizer] + ) -> Optional[visualize.AnyVisualizer]: + path = problem.path / cls.source_dir + if not path.is_dir(): + return None + if cls not in problem._visualizer_cache: + if cls == visualize.OutputVisualizer and problem.interactive: + problem._visualizer_cache[cls] = None + warn("Output Visualizer is not supported for interactive problem. IGNORED.") + else: + visualizer = cls(problem, path) + bar = ProgressBar(f"Building {cls.visualizer_type} visualizer", items=[visualizer]) + localbar = bar.start(visualizer) + visualizer.build(localbar) + localbar.done() + bar.finalize(print_done=False) + problem._visualizer_cache[cls] = visualizer if visualizer.ok else None + return problem._visualizer_cache[cls] + def validators( problem, cls: type[validate.AnyValidator], @@ -960,7 +992,7 @@ def build_program(p): problem._validators_cache[key] = validators return validators - # get all testcses and submissions and prepare the output validator + # get all testcases and submissions and prepare the output validator and visualizer def prepare_run(problem): testcases = problem.testcases() if not testcases: @@ -970,6 +1002,10 @@ def prepare_run(problem): if not problem.validators(validate.OutputValidator): return False + # Pre build the output visualizer to prevent nested ProgressBars. + if not config.args.no_visualizer: + problem.visualizer(visualize.OutputVisualizer) + submissions = problem.submissions() if not submissions: return False diff --git a/bin/run.py b/bin/run.py index 297cb1fc5..a9cceea7f 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 cast, Optional import config import interactive @@ -13,6 +13,7 @@ import problem import program import validate +import visualize from testcase import Testcase from util import ( crop_output, @@ -23,6 +24,7 @@ is_bsd, is_windows, ProgressBar, + shorten_path, warn, ) from verdicts import from_string, from_string_domjudge, RunUntil, Verdict, Verdicts @@ -174,7 +176,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 +219,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 +231,15 @@ def _validate_output(self, bar): args=self.testcase.testdata_yaml_validator_args(output_validator, bar), ) + def _visualize_output(self, bar: ProgressBar) -> Optional[ExecResult]: + if config.args.no_visualizer: + return None + output_visualizer = self.problem.visualizer(visualize.OutputVisualizer) + if output_visualizer is None: + return None + # TODO args? + return output_visualizer.run(self.testcase, self) + class Submission(program.Program): def __init__(self, problem, path, skip_double_build_warning=False): @@ -420,14 +433,15 @@ def process_run(run: Run): if result.out: data = crop_output(result.out) - judgemessage = run.feedbackdir / "judgemessage.txt" - judgeerror = run.feedbackdir / "judgeerror.txt" # Add data from feedbackdir. for f in run.feedbackdir.iterdir(): - if f in [judgemessage, judgeerror]: - continue if f.name.startswith("."): continue # skip "hidden" files + if f.name in ["judgemessage.txt", "judgeerror.txt"]: + continue + if f.name.startswith("judgeimage.") or f.name.startswith("teamimage."): + data += f"{f.name}: {shorten_path(self.problem, f.parent) / f.name}\n" + continue if not f.is_file(): localbar.warn(f"Validator wrote to {f} but it's not a file.") continue diff --git a/bin/tools.py b/bin/tools.py index c63ccbedc..06193e223 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -647,6 +647,7 @@ def build_parser(): ) genparser.add_argument( "--no-visualizer", + default=False, action="store_true", help="Skip generating graphics with the visualizer.", ) @@ -695,7 +696,6 @@ def build_parser(): ) runparser.add_argument( "--visualizer", - default=True, dest="no_visualizer", action="store_false", help="Also run the output visualizer.", diff --git a/bin/upgrade.py b/bin/upgrade.py index f37dd7a3a..d3c25dd3b 100644 --- a/bin/upgrade.py +++ b/bin/upgrade.py @@ -93,7 +93,7 @@ def upgrade_generators_yaml(problem_path: Path, bar: ProgressBar) -> None: if "visualizer" in yaml_data: warn( - "Cannot automatically upgrade 'visualizer'.\n - move visualizer to 'input_visualizer/'\n - use stdin to read testcase" + "Cannot automatically upgrade 'visualizer'.\n - move visualizer to 'input_visualizer/'\n - first argument is the in_file\n - second argument is the ans_file" ) if "data" in yaml_data and isinstance(yaml_data["data"], dict): diff --git a/bin/validate.py b/bin/validate.py index a91c22e3e..d44169047 100644 --- a/bin/validate.py +++ b/bin/validate.py @@ -358,7 +358,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 ------- @@ -368,7 +368,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() diff --git a/bin/visualize.py b/bin/visualize.py index 8cff705b0..10186c316 100644 --- a/bin/visualize.py +++ b/bin/visualize.py @@ -3,23 +3,23 @@ import program import testcase -import run from util import * if TYPE_CHECKING: # Prevent circular import: https://stackoverflow.com/a/39757388 from problem import Problem + import run class InputVisualizer(program.Program): """ - Visualizes an input file (such as "testcase.in"), called as: + Visualizes a testcase, called as: - ./visualizer [args] < input + ./visualizer input answer [args] """ - validator_type: Final[str] = "input" + visualizer_type: Final[str] = "input" source_dir: Final[str] = "input_visualizer" @@ -35,15 +35,15 @@ def __init__(self, problem: "Problem", path: Path, **kwargs): # Run the visualizer (should create a testcase. file). # Stdout is not used. - def run(self, in_path: Path, cwd: Path, args: Optional[list[str]] = None) -> ExecResult: + def run( + self, in_path: Path, ans_path: Path, cwd: Path, args: Optional[list[str]] = None + ) -> ExecResult: assert self.run_command is not None, "Input Visualizer should be built before running it" - with in_path.open("rb") as in_file: - return self._exec_command( - self.run_command + (args or []), - cwd=cwd, - stdin=in_file, - ) + return self._exec_command( + self.run_command + [in_path, ans_path] + (args or []), + cwd=cwd, + ) class OutputVisualizer(program.Program): @@ -54,7 +54,7 @@ class OutputVisualizer(program.Program): """ - validator_type: Final[str] = "output" + visualizer_type: Final[str] = "output" source_dir: Final[str] = "output_visualizer" @@ -73,7 +73,7 @@ def __init__(self, problem: "Problem", path: Path, **kwargs): def run( self, testcase: testcase.Testcase, - run: run.Run, + run: "run.Run", args: Optional[list[str]] = None, ) -> ExecResult: assert self.run_command is not None, "Output Visualizer should be built before running it" @@ -89,3 +89,6 @@ def run( stdin=out_file, cwd=cwd, ) + + +AnyVisualizer = InputVisualizer | OutputVisualizer diff --git a/test/problems/identity/input_visualizer_disabled/run b/test/problems/identity/input_visualizer_disabled/run index 53ead392e..ecc84b0c4 100755 --- a/test/problems/identity/input_visualizer_disabled/run +++ b/test/problems/identity/input_visualizer_disabled/run @@ -1,4 +1,5 @@ #!/usr/bin/env sh set -e -asy -f png $(dirname $0)/visualize.asy -o testcase.png + +cat $1 $2 | asy -f png $(dirname $0)/visualize.asy -o testcase.png diff --git a/test/problems/identity/output_visualizer_disabled/run b/test/problems/identity/output_visualizer_disabled/run new file mode 100755 index 000000000..ac445a73e --- /dev/null +++ b/test/problems/identity/output_visualizer_disabled/run @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +set -e + +asy -f png $(dirname $0)/visualize.asy -o $3/judgeimage.png diff --git a/test/problems/identity/output_visualizer_disabled/visualize.asy b/test/problems/identity/output_visualizer_disabled/visualize.asy new file mode 100644 index 000000000..6d0d328c7 --- /dev/null +++ b/test/problems/identity/output_visualizer_disabled/visualize.asy @@ -0,0 +1,5 @@ +defaultpen(1); + +string n = stdin; +label(scale(5)*n, (0,0)); +shipout(bbox(xmargin=5, white, Fill)); From f4d7c6774c7d7e963b546ec21fd1cd8bbea8735a Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 6 Apr 2025 17:54:37 +0200 Subject: [PATCH 12/53] change visualizer interface --- bin/generate.py | 35 +++++++++++++++++++++++-- bin/program.py | 25 +++++++++--------- bin/run.py | 4 ++- bin/skel.py | 9 ++++++- bin/visualize.py | 16 ++++------- skel/problem/input_visualizer/readme.md | 2 ++ skel/problem/output_visualizer/.gitkeep | 0 7 files changed, 64 insertions(+), 27 deletions(-) create mode 100644 skel/problem/input_visualizer/readme.md create mode 100644 skel/problem/output_visualizer/.gitkeep diff --git a/bin/generate.py b/bin/generate.py index 76bb801ad..66924960e 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -1017,7 +1017,9 @@ def generate_visualization(): return True if config.args.no_visualizer: return True - visualizer = problem.visualizer(visualize.InputVisualizer) + visualizer = problem.visualizer(visualize.InputVisualizer) or problem.visualizer( + visualize.OutputVisualizer + ) if visualizer is None: return True @@ -1035,7 +1037,35 @@ def generate_visualization(): assert in_path.is_file() assert ans_path.is_file() - result = visualizer.run(in_path, ans_path, cwd) + for ext in config.KNOWN_VISUALIZER_EXTENSIONS: + in_path.with_suffix(ext).unlink(True) + + if isinstance(visualizer, visualize.InputVisualizer): + result = visualizer.run(in_path, ans_path, cwd) + elif not problem.multi_pass: + feedbackdir = in_path.with_suffix(".feedbackdir") + feedbackdir.mkdir(parents=True, exist_ok=True) + teamimage = feedbackdir / "teamimage" + judgeimage = feedbackdir / "judgeimage" + + for ext in config.KNOWN_VISUALIZER_EXTENSIONS: + teamimage.with_suffix(ext).unlink(True) + judgeimage.with_suffix(ext).unlink(True) + + result = visualizer.run(in_path, ans_path, ans_path, feedbackdir) + if result.status: + found = None + for ext in config.KNOWN_VISUALIZER_EXTENSIONS: + file = teamimage.with_suffix(ext) + if file.is_file(): + found = file + for ext in config.KNOWN_VISUALIZER_EXTENSIONS: + file = judgeimage.with_suffix(ext) + if file.is_file(): + found = file + if found is not None: + found.rename(in_path.with_suffix(found.suffix)) + bar.log(f"Using {found.name} as testcase visualization") if result.status == ExecStatus.TIMEOUT: bar.debug(f"{Style.RESET_ALL}-> {shorten_path(problem, cwd)}") @@ -1877,6 +1907,7 @@ def build_program(p): build_programs(run.Submission, solutions_used) if build_visualizers: self.problem.visualizer(visualize.InputVisualizer) + self.problem.visualizer(visualize.OutputVisualizer) self.problem.validators(validate.InputValidator) self.problem.validators(validate.AnswerValidator) diff --git a/bin/program.py b/bin/program.py index 1b399fcbe..8d0144bfb 100644 --- a/bin/program.py +++ b/bin/program.py @@ -141,18 +141,19 @@ def __init__( # Set self.name and self.tmpdir. # Ideally they are the same as the path inside the problem, but fallback to just the name. - try: - # Only resolve the parent of the program. This preserves programs that are symlinks to other directories. - relpath = (path.parent.resolve() / path.name).relative_to( - problem.path.resolve() / self.subdir - ) - self.short_path = relpath - self.name: str = str(relpath) - self.tmpdir = problem.tmpdir / self.subdir / relpath - except ValueError: - self.short_path = Path(path.name) - self.name = str(path.name) - self.tmpdir = problem.tmpdir / self.subdir / path.name + relpath = Path(path.name) + if path.parent != problem.path: + try: + # Only resolve the parent of the program. This preserves programs that are symlinks to other directories. + relpath = (path.parent.resolve() / path.name).relative_to( + problem.path.resolve() / self.subdir + ) + except ValueError: + pass + + self.short_path = relpath + self.name: str = str(relpath) + self.tmpdir = problem.tmpdir / self.subdir / self.name self.compile_command: Optional[list[str]] = None self.run_command: Optional[list[str]] = None diff --git a/bin/run.py b/bin/run.py index a9cceea7f..699b2c968 100644 --- a/bin/run.py +++ b/bin/run.py @@ -238,7 +238,9 @@ def _visualize_output(self, bar: ProgressBar) -> Optional[ExecResult]: if output_visualizer is None: return None # TODO args? - return output_visualizer.run(self.testcase, self) + return output_visualizer.run( + self.in_path, self.testcase.ans_path.resolve(), self.out_path, self.feedbackdir + ) class Submission(program.Program): diff --git a/bin/skel.py b/bin/skel.py index 4427a69c8..9eff5b82c 100644 --- a/bin/skel.py +++ b/bin/skel.py @@ -11,6 +11,7 @@ from problem import Problem from util import * from validate import OutputValidator +from visualize import OutputVisualizer # Returns the alphanumeric version of a string: @@ -180,13 +181,19 @@ def new_problem() -> None: else: error("ruamel.yaml library not found. Please update problems.yaml manually.") + skip = [] + if custom_output: + skip.append(skeldir / OutputValidator.source_dir) + if "interactive" in problem_type: + skip.append(skeldir / OutputVisualizer.source_dir) + copytree_and_substitute( skeldir, target_dir / dirname, variables, exist_ok=True, preserve_symlinks=preserve_symlinks, - skip=[skeldir / OutputValidator.source_dir] if not custom_output else None, + skip=skip, ) # Warn about missing problem statement skeletons for non-en languages diff --git a/bin/visualize.py b/bin/visualize.py index 10186c316..86a5f2163 100644 --- a/bin/visualize.py +++ b/bin/visualize.py @@ -2,13 +2,11 @@ from typing import Final, Optional, TYPE_CHECKING import program -import testcase from util import * if TYPE_CHECKING: # Prevent circular import: https://stackoverflow.com/a/39757388 from problem import Problem - import run class InputVisualizer(program.Program): @@ -34,7 +32,6 @@ def __init__(self, problem: "Problem", path: Path, **kwargs): ) # Run the visualizer (should create a testcase. file). - # Stdout is not used. def run( self, in_path: Path, ans_path: Path, cwd: Path, args: Optional[list[str]] = None ) -> ExecResult: @@ -69,20 +66,17 @@ def __init__(self, problem: "Problem", path: Path, **kwargs): ) # Run the visualizer. - # should write to feedbackdir/judgeimage. + # should write to feedbackdir/judgeimage. and/or feedbackdir/teamimage. def run( self, - testcase: testcase.Testcase, - run: "run.Run", + in_path: Path, + ans_path: Path, + out_path: Path, + cwd: Path, args: Optional[list[str]] = None, ) -> ExecResult: assert self.run_command is not None, "Output Visualizer should be built before running it" - in_path = run.in_path # relevant for multipass - ans_path = testcase.ans_path.resolve() - out_path = run.out_path - cwd = run.feedbackdir - with out_path.open("rb") as out_file: return self._exec_command( self.run_command + [in_path, ans_path, cwd] + (args or []), diff --git a/skel/problem/input_visualizer/readme.md b/skel/problem/input_visualizer/readme.md new file mode 100644 index 000000000..c983127a4 --- /dev/null +++ b/skel/problem/input_visualizer/readme.md @@ -0,0 +1,2 @@ +This input visualizer is intended for use with BAPCtools `bt generate`. +The visualizer should be invoked as `./visualizer input answer` and writes a `testcase.`. diff --git a/skel/problem/output_visualizer/.gitkeep b/skel/problem/output_visualizer/.gitkeep new file mode 100644 index 000000000..e69de29bb From 08721a7cc90f17b159757a7b3f689dccd311cb5e Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 6 Apr 2025 22:27:47 +0200 Subject: [PATCH 13/53] handle args --- bin/export.py | 2 +- bin/generate.py | 1 + bin/interactive.py | 2 +- bin/problem.py | 45 ++++++++++++++++++++++++++------------------- bin/run.py | 9 ++++++--- bin/skel.py | 6 +++--- bin/testcase.py | 26 +++++++++++++------------- bin/upgrade.py | 16 ++++++++-------- bin/validate.py | 6 ++++++ bin/visualize.py | 4 ++++ 10 files changed, 69 insertions(+), 48 deletions(-) diff --git a/bin/export.py b/bin/export.py index f1c1097a3..a22c16567 100644 --- a/bin/export.py +++ b/bin/export.py @@ -295,7 +295,7 @@ def add_testcase(in_file: Path) -> None: validator_flags = " ".join( problem.get_testdata_yaml( problem.path / "data", - "output_validator_args", + OutputValidator.args_key, PrintBar("Getting validator_flags for legacy export"), ) ) diff --git a/bin/generate.py b/bin/generate.py index 66924960e..e4f1a0dc3 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -1026,6 +1026,7 @@ def generate_visualization(): # TODO if the input visualizer gets args these need to be hashed as well visualizer_hash = { "visualizer_hash": visualizer.hash, + "visualizer_args": testcase.testdata_yaml_args(visualizer, PrintBar()), } if meta_yaml.get("visualizer_hash") == visualizer_hash: diff --git a/bin/interactive.py b/bin/interactive.py index 8936afb1e..eb1a85418 100644 --- a/bin/interactive.py +++ b/bin/interactive.py @@ -56,7 +56,7 @@ def get_validator_command(): run.testcase.ans_path.resolve(), run.feedbackdir.resolve(), ] - + run.testcase.testdata_yaml_validator_args( + + run.testcase.testdata_yaml_args( output_validator, bar or PrintBar("Run interactive test case"), ) diff --git a/bin/problem.py b/bin/problem.py index 43c804610..970b977c0 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -267,7 +267,7 @@ def __init__( self.limits = ProblemLimits(parse_setting(yaml_data, "limits", {}), problem, self) parse_deprecated_setting( - yaml_data, "validator_flags", "output_validator_args' in 'testdata.yaml" + yaml_data, "validator_flags", f"{validate.OutputValidator.args_key}' in 'testdata.yaml" ) self.keywords: list[str] = parse_optional_list_setting(yaml_data, "keywords", str) @@ -460,17 +460,19 @@ def _parse_testdata_yaml(p, path, bar): p._testdata_yamls[f] = flags = parse_yaml(raw, path=f, plain=True) parse_deprecated_setting( - flags, "output_validator_flags", "output_validator_args" + flags, "output_validator_flags", validate.OutputValidator.args_key + ) + parse_deprecated_setting( + flags, "input_validator_flags", validate.InputValidator.args_key ) - parse_deprecated_setting(flags, "input_validator_flags", "input_validator_args") # Verify testdata.yaml for k in flags: match k: - case "output_validator_args": + case validate.OutputValidator.args_key: if not isinstance(flags[k], str): bar.error(f"{k} must be string", resume=True, print_item=False) - case "input_validator_args": + case validate.InputValidator.args_key: if not isinstance(flags[k], (str, dict)): bar.error( f"{k} must be string or map", @@ -508,8 +510,8 @@ def _parse_testdata_yaml(p, path, bar): def get_testdata_yaml( p, path: Path, - key: Literal["input_validator_args"] | Literal["output_validator_args"], - bar: ProgressBar | PrintBar, + key: str, + bar: BAR_TYPE, name: Optional[str] = None, ) -> list[str]: """ @@ -521,8 +523,7 @@ def get_testdata_yaml( Arguments --------- path: absolute path (a file or a directory) - key: The testdata.yaml key to look for, either of 'input_validator_args', 'output_validator_args', or 'grading'. - TODO: 'grading' is not yet implemented. + key: The testdata.yaml key to look for (TODO: 'grading' is not yet implemented) name: If key == 'input_validator_args', optionally the name of the input validator. Returns: @@ -530,9 +531,16 @@ def get_testdata_yaml( A list of string arguments, which is empty if no testdata.yaml is found. TODO: when 'grading' is supported, it also can return dict """ - if key not in ["input_validator_args", "output_validator_args"]: + known_args_keys = [ + validate.InputValidator.args_key, + validate.OutputValidator.args_key, + validate.AnswerValidator.args_key, + visualize.InputVisualizer.args_key, + visualize.OutputVisualizer.args_key, + ] + if key not in known_args_keys: raise NotImplementedError(key) - if key != "input_validator_args" and name is not None: + if key != validate.InputValidator.args_key and name is not None: raise ValueError( f"Only input validators support flags by validator name, got {key} and {name}" ) @@ -551,20 +559,19 @@ def get_testdata_yaml( continue flags = p._testdata_yamls[f] if key in flags: - if key == "output_validator_args": - if not isinstance(flags[key], str): - bar.error("output_validator_args must be string") - return [] - return flags[key].split() - - if key == "input_validator_args": + if key == validate.InputValidator.args_key: if not isinstance(flags[key], (str, dict)): - bar.error("input_validator_args must be string or map") + bar.error(f"{key} must be string or map") return [] if isinstance(flags[key], str): return flags[key].split() elif name in flags[key]: return flags[key][name].split() + elif key in known_args_keys: + if not isinstance(flags[key], str): + bar.error(f"{key} must be string") + return [] + return flags[key].split() return [] diff --git a/bin/run.py b/bin/run.py index 699b2c968..28d220bac 100644 --- a/bin/run.py +++ b/bin/run.py @@ -228,7 +228,7 @@ def _validate_output(self, bar: ProgressBar) -> Optional[ExecResult]: return output_validator.run( self.testcase, self, - args=self.testcase.testdata_yaml_validator_args(output_validator, bar), + args=self.testcase.testdata_yaml_args(output_validator, bar), ) def _visualize_output(self, bar: ProgressBar) -> Optional[ExecResult]: @@ -237,9 +237,12 @@ def _visualize_output(self, bar: ProgressBar) -> Optional[ExecResult]: output_visualizer = self.problem.visualizer(visualize.OutputVisualizer) if output_visualizer is None: return None - # TODO args? return output_visualizer.run( - self.in_path, self.testcase.ans_path.resolve(), self.out_path, self.feedbackdir + self.in_path, + self.testcase.ans_path.resolve(), + self.out_path, + self.feedbackdir, + args=self.testcase.testdata_yaml_args(output_visualizer, bar), ) diff --git a/bin/skel.py b/bin/skel.py index 9eff5b82c..8a6cc09f3 100644 --- a/bin/skel.py +++ b/bin/skel.py @@ -94,7 +94,7 @@ def new_problem() -> None: ) author = config.args.author if config.args.author else ask_variable_string("author") - output_validator_args = "#output_validator_args:" + output_validator_args = f"#{OutputValidator.args_key}:" custom_output = False if config.args.type: problem_type = config.args.type @@ -106,7 +106,7 @@ def new_problem() -> None: # The validation type `float` is not official, it only helps setting the `output_validator_args`. if problem_type == "float": problem_type = "pass-fail" - output_validator_args = "output_validator_args: float_tolerance 1e-6" + output_validator_args = f"{OutputValidator.args_key}: float_tolerance 1e-6" log("Using default float tolerance of 1e-6") # Since version 2023-07-draft of the spec, the `custom` validation type is no longer explicit. # The mere existence of the output_validator(s)/ folder signals non-default output validation. @@ -122,7 +122,7 @@ def new_problem() -> None: "dirname": dirname, "author": author, "type": problem_type, - "output_validator_args": output_validator_args, + OutputValidator.args_key: output_validator_args, "testdata_yaml_comment": "#" if output_validator_args[0] == "#" else "", } diff --git a/bin/testcase.py b/bin/testcase.py index d07f2af79..e839c4005 100644 --- a/bin/testcase.py +++ b/bin/testcase.py @@ -2,9 +2,10 @@ from colorama import Fore, Style from pathlib import Path -from typing import cast, Literal, Optional +from typing import Optional, TYPE_CHECKING from util import ( + BAR_TYPE, ExecStatus, combine_hashes_dict, fatal, @@ -14,6 +15,10 @@ import config import validate +if TYPE_CHECKING: # Prevent circular import: https://stackoverflow.com/a/39757388 + import validate + import visualize + class Testcase: """ @@ -102,10 +107,10 @@ def __repr__(self): def with_suffix(self, ext): return self.in_path.with_suffix(ext) - def testdata_yaml_validator_args( + def testdata_yaml_args( self, - validator, # TODO #102: Fix circular import when setting type to validate.AnyValidator - bar, # TODO #102: Type should probably be ProgressBar | PrintBar or something + program: "validate.AnyValidator | visualize.AnyVisualizer", + bar: BAR_TYPE, ) -> list[str]: """ The flags specified in testdata.yaml for the given validator applying to this testcase. @@ -116,18 +121,13 @@ def testdata_yaml_validator_args( A nonempty list of strings, such as ["space_change_sensitive", "case_sensitive"] or ["--max_N", "50"] or even [""]. """ - key, name = ( - ("input_validator_args", validator.name) - if isinstance(validator, validate.InputValidator) - else ("output_validator_args", None) - ) path = self.problem.path / "data" / self.short_path return self.problem.get_testdata_yaml( path, - cast(Literal["input_validator_args", "output_validator_args"], key), + type(program).args_key, bar, - name=name, + name=program.name if isinstance(program, validate.InputValidator) else None, ) def validator_hashes(self, cls: type["validate.AnyValidator"], bar): @@ -147,7 +147,7 @@ def validator_hashes(self, cls: type["validate.AnyValidator"], bar): d = dict() for validator in validators: - flags = self.testdata_yaml_validator_args(validator, bar) + flags = self.testdata_yaml_args(validator, bar) if not flags: continue flags_string = " ".join(flags) if flags is not None else None @@ -278,7 +278,7 @@ def _run_validators( if isinstance(validator, validate.OutputValidator) and mode == validate.Mode.ANSWER: args += ["case_sensitive", "space_change_sensitive"] name = f"{name} (ans)" - flags = self.testdata_yaml_validator_args(validator, bar) + flags = self.testdata_yaml_args(validator, bar) if flags is False: continue flags = args if flags is None else flags + args diff --git a/bin/upgrade.py b/bin/upgrade.py index d3c25dd3b..4dd6e1b08 100644 --- a/bin/upgrade.py +++ b/bin/upgrade.py @@ -61,8 +61,8 @@ def rename_testcase(old_base: Path, new_dir: Path) -> None: def upgrade_testdata_yaml(problem_path: Path, bar: ProgressBar) -> None: rename = [ - ("output_validator_flags", "output_validator_args"), - ("input_validator_flags", "input_validator_args"), + ("output_validator_flags", OutputValidator.args_key), + ("input_validator_flags", InputValidator.args_key), ] for f in (problem_path / "data").rglob("testdata.yaml"): @@ -158,8 +158,8 @@ def upgrade_generated_testdata_yaml(data: dict[str, Any], path: str) -> bool: print_path = f" ({path[1:]})" if len(path) > 1 else "" rename = [ - ("output_validator_flags", "output_validator_args"), - ("input_validator_flags", "input_validator_args"), + ("output_validator_flags", OutputValidator.args_key), + ("input_validator_flags", InputValidator.args_key), ] for old, new in rename: if old in testdata: @@ -370,14 +370,14 @@ def upgrade_problem_yaml(problem_path: Path, bar: ProgressBar) -> None: ryaml_filter(data, "limits") def add_args(new_data: dict[str, Any]) -> bool: - if "output_validator_args" in new_data: + if OutputValidator.args_key in new_data: bar.error( - "can't change 'validator_flags', 'output_validator_args' already exists in testdata.yaml", + f"can't change 'validator_flags', '{OutputValidator.args_key}' already exists in testdata.yaml", resume=True, ) return False - bar.log("change 'validator_flags' to 'output_validator_args' in testdata.yaml") - new_data["output_validator_args"] = data["validator_flags"] + bar.log(f"change 'validator_flags' to '{OutputValidator.args_key}' in testdata.yaml") + new_data[OutputValidator.args_key] = data["validator_flags"] ryaml_filter(data, "validator_flags") return True diff --git a/bin/validate.py b/bin/validate.py index d44169047..3a6c75b3f 100644 --- a/bin/validate.py +++ b/bin/validate.py @@ -230,6 +230,8 @@ class InputValidator(Validator): source_dir: Final[str] = "input_validators" + args_key: Final[str] = "input_validator_args" + def __init__(self, problem, path, **kwargs): super().__init__(problem, path, InputValidator.source_dir, **kwargs) @@ -290,6 +292,8 @@ class AnswerValidator(Validator): source_dir: Final[str] = "answer_validators" + args_key: Final[str] = "answer_validator_args" + def __init__(self, problem, path, **kwargs): super().__init__(problem, path, AnswerValidator.source_dir, **kwargs) @@ -341,6 +345,8 @@ class OutputValidator(Validator): source_dir: Final[str] = "output_validator" + args_key: Final[str] = "output_validator_args" + def __init__(self, problem, path, **kwargs): super().__init__(problem, path, OutputValidator.source_dir, **kwargs) diff --git a/bin/visualize.py b/bin/visualize.py index 86a5f2163..efc148211 100644 --- a/bin/visualize.py +++ b/bin/visualize.py @@ -21,6 +21,8 @@ class InputVisualizer(program.Program): source_dir: Final[str] = "input_visualizer" + args_key: Final[str] = "input_visualizer_args" + def __init__(self, problem: "Problem", path: Path, **kwargs): super().__init__( problem, @@ -55,6 +57,8 @@ class OutputVisualizer(program.Program): source_dir: Final[str] = "output_visualizer" + args_key: Final[str] = "output_visualizer_args" + def __init__(self, problem: "Problem", path: Path, **kwargs): super().__init__( problem, From 880134d96c8b944c37bdf554eb3704552e913403 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 6 Apr 2025 22:58:38 +0200 Subject: [PATCH 14/53] dont use answer_validator_args --- bin/tools.py | 3 +++ bin/validate.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bin/tools.py b/bin/tools.py index 06193e223..5edac5b23 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -637,11 +637,13 @@ def build_parser(): ) genparser.add_argument( "--no-validators", + default=False, action="store_true", help="Ignore results of input and answer validation. Validators are still run.", ) genparser.add_argument( "--no-solution", + default=False, action="store_true", help="Skip generating .ans/.interaction files with the solution.", ) @@ -653,6 +655,7 @@ def build_parser(): ) genparser.add_argument( "--no-testcase-sanity-checks", + default=False, action="store_true", help="Skip sanity checks on testcases.", ) diff --git a/bin/validate.py b/bin/validate.py index 3a6c75b3f..00560c192 100644 --- a/bin/validate.py +++ b/bin/validate.py @@ -292,7 +292,8 @@ class AnswerValidator(Validator): source_dir: Final[str] = "answer_validators" - args_key: Final[str] = "answer_validator_args" + # use output_validator_args as well + args_key: Final[str] = "output_validator_args" def __init__(self, problem, path, **kwargs): super().__init__(problem, path, AnswerValidator.source_dir, **kwargs) From b6ca0b307484072a05c77eec281e4f882e35a7b5 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Mon, 7 Apr 2025 10:14:19 +0200 Subject: [PATCH 15/53] properly resolve problem paths --- bin/program.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bin/program.py b/bin/program.py index 8d0144bfb..db9c3162b 100644 --- a/bin/program.py +++ b/bin/program.py @@ -142,12 +142,9 @@ def __init__( # Set self.name and self.tmpdir. # Ideally they are the same as the path inside the problem, but fallback to just the name. relpath = Path(path.name) - if path.parent != problem.path: + if path.absolute().parent != problem.path.absolute(): try: - # Only resolve the parent of the program. This preserves programs that are symlinks to other directories. - relpath = (path.parent.resolve() / path.name).relative_to( - problem.path.resolve() / self.subdir - ) + relpath = path.absolute().relative_to(problem.path.absolute()) except ValueError: pass From 3ab0ba8a2318e80193598f9a271e1f8c7b1ffc5a Mon Sep 17 00:00:00 2001 From: mzuenni Date: Mon, 7 Apr 2025 10:24:12 +0200 Subject: [PATCH 16/53] fix --- bin/program.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/program.py b/bin/program.py index db9c3162b..e0ad2bb55 100644 --- a/bin/program.py +++ b/bin/program.py @@ -144,7 +144,7 @@ def __init__( relpath = Path(path.name) if path.absolute().parent != problem.path.absolute(): try: - relpath = path.absolute().relative_to(problem.path.absolute()) + relpath = path.absolute().relative_to(problem.path.absolute() / subdir) except ValueError: pass From c566e374f7677a7d1301d03e206ab998f16750af Mon Sep 17 00:00:00 2001 From: mzuenni Date: Mon, 7 Apr 2025 11:40:54 +0200 Subject: [PATCH 17/53] typing --- bin/contest.py | 2 +- bin/generate.py | 4 +-- bin/problem.py | 14 ++++----- bin/program.py | 6 +--- bin/run.py | 4 +-- bin/testcase.py | 75 +++++++++++++++++++++++++++--------------------- bin/visualize.py | 6 ++-- 7 files changed, 57 insertions(+), 54 deletions(-) diff --git a/bin/contest.py b/bin/contest.py index db00d2884..241301dd8 100644 --- a/bin/contest.py +++ b/bin/contest.py @@ -41,7 +41,7 @@ def problems_yaml() -> Optional[list[dict[str, Any]]]: def get_api() -> str: - api = config.args.api or contest_yaml().get("api") + api = config.args.api or cast(str, contest_yaml().get("api")) if not api: fatal( "Could not find key `api` in contest.yaml and it was not specified on the command line." diff --git a/bin/generate.py b/bin/generate.py index e4f1a0dc3..7ec861404 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -9,7 +9,7 @@ from collections.abc import Callable, Sequence from colorama import Fore, Style from pathlib import Path, PurePosixPath -from typing import Any, Final, Optional, overload +from typing import Any, Final, Iterable, Optional, overload import config import parallel @@ -174,7 +174,7 @@ def run(self, bar, cwd, name, seed, retries=1): ) if result.status: break - if not result.retry: + if result.status == ExecStatus.TIMEOUT: break if not result.status: diff --git a/bin/problem.py b/bin/problem.py index 970b977c0..1fc63a8d6 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -1334,7 +1334,7 @@ def validate_valid_extra_data(p) -> bool: def _validate_data( problem, mode: validate.Mode, - constraints: dict | Literal[True] | None, + constraints: validate.ConstraintsDict | Literal[True] | None, action: str, testcases: Sequence[testcase.Testcase], extra: bool = False, @@ -1343,13 +1343,11 @@ def _validate_data( if not testcases: return True - if constraints is True: - constraints = {} - assert constraints is None or isinstance(constraints, dict) + constraints_dict = {} if constraints is True else constraints + check_constraints = constraints_dict is not None # Pre-build the relevant Validators so as to avoid clash with ProgressBar bar below # Also, pick the relevant testcases - check_constraints = constraints is not None match mode: case validate.Mode.INPUT: problem.validators(validate.InputValidator, check_constraints=check_constraints) @@ -1395,7 +1393,7 @@ def process_testcase(testcase: testcase.Testcase): return ok = testcase.validate_format( - mode, bar=localbar, constraints=constraints, warn_instead_of_error=extra + mode, bar=localbar, constraints=constraints_dict, warn_instead_of_error=extra ) success &= ok localbar.done(ok) @@ -1405,8 +1403,8 @@ def process_testcase(testcase: testcase.Testcase): bar.finalize(print_done=True) # Make sure all constraints are satisfied. - if constraints: - for loc, value in sorted(constraints.items()): + if constraints_dict: + for loc, value in sorted(constraints_dict.items()): loc = Path(loc).name name, has_low, has_high, vmin, vmax, low, high = value if not has_low: diff --git a/bin/program.py b/bin/program.py index e0ad2bb55..40278acd7 100644 --- a/bin/program.py +++ b/bin/program.py @@ -513,7 +513,7 @@ def build(self, bar: ProgressBar): return True - def _exec_command(self, *args, **kwargs): + def _exec_command(self, *args, **kwargs) -> ExecResult: if "timeout" not in kwargs and "timeout" in self.limits: kwargs["timeout"] = self.limits["timeout"] if "memory" not in kwargs and "memory" in self.limits: @@ -565,16 +565,12 @@ def run(self, bar, cwd, name, args=[]): cwd=cwd, ) - result.retry = False - if result.status == ExecStatus.TIMEOUT: # Timeout -> stop retrying and fail. bar.log(f"TIMEOUT after {timeout}s", color=Fore.RED) return result if not result.status: - # Other error -> try again. - result.retry = True return result if stdout_path.read_text(): diff --git a/bin/run.py b/bin/run.py index 28d220bac..bc2655b10 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, Optional +from typing import Optional import config import interactive @@ -42,7 +42,7 @@ def __init__(self, problem: "problem.Problem", submission: "Submission", testcas self.problem.tmpdir / "runs" / self.submission.short_path - / cast(Path, self.testcase.short_path).with_suffix("") + / self.testcase.short_path.with_suffix("") ) self.in_path: Path = self.tmpdir / "testcase.in" diff --git a/bin/testcase.py b/bin/testcase.py index e839c4005..e535a2d2c 100644 --- a/bin/testcase.py +++ b/bin/testcase.py @@ -1,5 +1,6 @@ """Test case""" +from collections.abc import Sequence from colorama import Fore, Style from pathlib import Path from typing import Optional, TYPE_CHECKING @@ -10,14 +11,15 @@ combine_hashes_dict, fatal, print_name, + ProgressBar, shorten_path, ) import config import validate if TYPE_CHECKING: # Prevent circular import: https://stackoverflow.com/a/39757388 - import validate import visualize + import problem class Testcase: @@ -62,7 +64,14 @@ class Testcase: """ - def __init__(self, base_problem, path, *, short_path=None, print_warn=False): + def __init__( + self, + base_problem: "problem.Problem", + path: Path, + *, + short_path: Optional[Path] = None, + print_warn: bool = False, + ): """ Arguments --------- @@ -81,17 +90,17 @@ def __init__(self, base_problem, path, *, short_path=None, print_warn=False): # TODO add self.out_path if short_path is None: try: - self.short_path = path.relative_to(self.problem.path / "data") + self.short_path: Path = path.relative_to(self.problem.path / "data") except ValueError: fatal(f"Testcase {path} is not inside {self.problem.path / 'data'}.") else: self.short_path = short_path - self.root = self.short_path.parts[0] + self.root: str = self.short_path.parts[0] - self.in_path = path - self.ans_path = self.in_path.with_suffix(".ans") - self.out_path = ( + self.in_path: Path = path + self.ans_path: Path = self.in_path.with_suffix(".ans") + self.out_path: Optional[Path] = ( self.in_path.with_suffix(".out") if self.root in ["valid_output", "invalid_output"] or self.in_path.with_suffix(".out").is_file() @@ -99,12 +108,12 @@ def __init__(self, base_problem, path, *, short_path=None, print_warn=False): ) # Display name: everything after data/. - self.name = str(self.short_path.with_suffix("")) + self.name: str = str(self.short_path.with_suffix("")) - def __repr__(self): + def __repr__(self) -> str: return self.name - def with_suffix(self, ext): + def with_suffix(self, ext: str) -> Path: return self.in_path.with_suffix(ext) def testdata_yaml_args( @@ -130,7 +139,9 @@ def testdata_yaml_args( name=program.name if isinstance(program, validate.InputValidator) else None, ) - def validator_hashes(self, cls: type["validate.AnyValidator"], bar): + def validator_hashes( + self, cls: type["validate.AnyValidator"], bar: BAR_TYPE + ) -> dict[str, dict[str, str]]: """ Returns ------- @@ -148,18 +159,18 @@ def validator_hashes(self, cls: type["validate.AnyValidator"], bar): for validator in validators: flags = self.testdata_yaml_args(validator, bar) - if not flags: - continue - flags_string = " ".join(flags) if flags is not None else None - o = { + flags_string = " ".join(flags) + h = combine_hashes_dict( + { + "name": validator.name, + "flags": flags_string, + "hash": validator.hash, + } + ) + d[h] = { "name": validator.name, "flags": flags_string, - "hash": validator.hash, } - h = combine_hashes_dict(o) - # Don't actually store the somewhat useless validator hash. - del o["hash"] - d[h] = o return d @@ -167,9 +178,9 @@ def validate_format( self, mode: "validate.Mode", *, - bar, - constraints=None, - warn_instead_of_error=False, + bar: ProgressBar, + constraints: Optional[validate.ConstraintsDict] = None, + warn_instead_of_error: bool = False, ) -> bool: check_constraints = constraints is not None @@ -264,12 +275,12 @@ def validate_format( def _run_validators( self, mode: "validate.Mode", - validators, - expect_rejection, + validators: Sequence[validate.AnyValidator], + expect_rejection: bool, *, - bar, - constraints=None, - warn_instead_of_error=False, + bar: ProgressBar, + constraints: Optional[validate.ConstraintsDict] = None, + warn_instead_of_error: bool = False, ) -> bool: args = [] results = [] @@ -279,9 +290,7 @@ def _run_validators( args += ["case_sensitive", "space_change_sensitive"] name = f"{name} (ans)" flags = self.testdata_yaml_args(validator, bar) - if flags is False: - continue - flags = args if flags is None else flags + args + flags = flags + args ret = validator.run(self, mode=mode, constraints=constraints, args=flags) results.append(ret.status) @@ -325,7 +334,7 @@ def _run_validators( data += ( f"{Style.RESET_ALL}-> {shorten_path(self.problem, file.parent) / file.name}\n" ) - else: + elif ret.err: data = ret.err if expect_rejection: @@ -343,7 +352,7 @@ def _run_validators( ) else: bar.part_done( - ret.status, + bool(ret.status), message, data=data, warn_instead_of_error=warn_instead_of_error, diff --git a/bin/visualize.py b/bin/visualize.py index efc148211..1deaaff11 100644 --- a/bin/visualize.py +++ b/bin/visualize.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Final, Optional, TYPE_CHECKING +from typing import Any, Final, Optional, TYPE_CHECKING import program @@ -23,7 +23,7 @@ class InputVisualizer(program.Program): args_key: Final[str] = "input_visualizer_args" - def __init__(self, problem: "Problem", path: Path, **kwargs): + def __init__(self, problem: "Problem", path: Path, **kwargs: Any): super().__init__( problem, path, @@ -59,7 +59,7 @@ class OutputVisualizer(program.Program): args_key: Final[str] = "output_visualizer_args" - def __init__(self, problem: "Problem", path: Path, **kwargs): + def __init__(self, problem: "Problem", path: Path, **kwargs: Any): super().__init__( problem, path, From 899f2322fb6058ef9a90fcb0beff942999b03925 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Mon, 7 Apr 2025 11:49:01 +0200 Subject: [PATCH 18/53] typing --- bin/testcase.py | 8 ++++---- bin/validate.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/testcase.py b/bin/testcase.py index e535a2d2c..379cc4475 100644 --- a/bin/testcase.py +++ b/bin/testcase.py @@ -118,7 +118,7 @@ def with_suffix(self, ext: str) -> Path: def testdata_yaml_args( self, - program: "validate.AnyValidator | visualize.AnyVisualizer", + program: validate.AnyValidator | visualize.AnyVisualizer, bar: BAR_TYPE, ) -> list[str]: """ @@ -140,7 +140,7 @@ def testdata_yaml_args( ) def validator_hashes( - self, cls: type["validate.AnyValidator"], bar: BAR_TYPE + self, cls: type[validate.AnyValidator], bar: BAR_TYPE ) -> dict[str, dict[str, str]]: """ Returns @@ -176,7 +176,7 @@ def validator_hashes( def validate_format( self, - mode: "validate.Mode", + mode: validate.Mode, *, bar: ProgressBar, constraints: Optional[validate.ConstraintsDict] = None, @@ -274,7 +274,7 @@ def validate_format( def _run_validators( self, - mode: "validate.Mode", + mode: validate.Mode, validators: Sequence[validate.AnyValidator], expect_rejection: bool, *, diff --git a/bin/validate.py b/bin/validate.py index 00560c192..2dc48fe4c 100644 --- a/bin/validate.py +++ b/bin/validate.py @@ -209,7 +209,7 @@ def _exec_helper(self, *args, cwd, **kwargs): def run( self, - testcase: testcase.Testcase, + testcase: "testcase.Testcase", mode: Mode, constraints: Optional[ConstraintsDict] = None, args: Optional[list[str]] = None, @@ -237,7 +237,7 @@ def __init__(self, problem, path, **kwargs): def run( self, - testcase: testcase.Testcase, + testcase: "testcase.Testcase", mode: Mode = Mode.INPUT, constraints: Optional[ConstraintsDict] = None, args: Optional[list[str]] = None, @@ -300,7 +300,7 @@ def __init__(self, problem, path, **kwargs): def run( self, - testcase: testcase.Testcase, + testcase: "testcase.Testcase", mode: Mode = Mode.ANSWER, constraints: Optional[ConstraintsDict] = None, args: Optional[list[str]] = None, @@ -353,7 +353,7 @@ def __init__(self, problem, path, **kwargs): def run( self, - testcase: testcase.Testcase, + testcase: "testcase.Testcase", mode: "Mode | run.Run", constraints: Optional[ConstraintsDict] = None, args: Optional[list[str]] = None, From a4e0637ff7890b0988a6b6d1d38050f586f213ee Mon Sep 17 00:00:00 2001 From: mzuenni Date: Mon, 7 Apr 2025 11:53:28 +0200 Subject: [PATCH 19/53] typing --- bin/testcase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/testcase.py b/bin/testcase.py index 379cc4475..3d5e7982f 100644 --- a/bin/testcase.py +++ b/bin/testcase.py @@ -118,7 +118,7 @@ def with_suffix(self, ext: str) -> Path: def testdata_yaml_args( self, - program: validate.AnyValidator | visualize.AnyVisualizer, + program: "validate.AnyValidator | visualize.AnyVisualizer", bar: BAR_TYPE, ) -> list[str]: """ From b12d403f03c113c62569bee6cdb04f62a430fcea Mon Sep 17 00:00:00 2001 From: mzuenni Date: Mon, 7 Apr 2025 11:56:10 +0200 Subject: [PATCH 20/53] typing --- bin/validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/validate.py b/bin/validate.py index 2dc48fe4c..4027d5bac 100644 --- a/bin/validate.py +++ b/bin/validate.py @@ -7,10 +7,10 @@ import config import program -import testcase if TYPE_CHECKING: # Prevent circular import: https://stackoverflow.com/a/39757388 import run + import testcase class Mode(Enum): From c4458eb44ac9429fc0544d2b83257a7e7cde0eff Mon Sep 17 00:00:00 2001 From: mzuenni Date: Mon, 7 Apr 2025 13:11:10 +0200 Subject: [PATCH 21/53] allow empty .ans files for multipass --- bin/validate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/validate.py b/bin/validate.py index 4027d5bac..6ca13e3f8 100644 --- a/bin/validate.py +++ b/bin/validate.py @@ -482,7 +482,9 @@ def sanity_check(problem, path, bar, strict_whitespace=True): if _has_invalid_byte(file_bytes, other_whitespaces=not strict_whitespace): bar.warn(f"{name} contains unexpected characters but was accepted!") - elif len(file_bytes) == 0: + elif len(file_bytes) == 0 and not ( + path.suffix == ".ans" and problem.multi_pass + ): # explicitly allow empty .ans files for multipass bar.warn(f"{name} is empty but was accepted!") elif len(file_bytes) > 20_000_000: bar.warn(f"{name} is larger than 20MB!") @@ -498,7 +500,7 @@ def sanity_check(problem, path, bar, strict_whitespace=True): and 2 * len(file_bytes) > problem.limits.output * 1024 * 1024 ): bar.warn(f"{name} is close to output limit") - elif strict_whitespace: + elif strict_whitespace and len(file_bytes) > 0: if file_bytes[0] in [ord(" "), ord("\n")]: bar.warn(f"{name} starts with whitespace but was accepted!") elif file_bytes[-1] != ord("\n"): From 06c7d54159bc50eddbde9be91991155e2e6fdab4 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Mon, 7 Apr 2025 13:28:05 +0200 Subject: [PATCH 22/53] fix test --- test/problems/divsort/generators/generators.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/problems/divsort/generators/generators.yaml b/test/problems/divsort/generators/generators.yaml index 5f44912f6..152252732 100644 --- a/test/problems/divsort/generators/generators.yaml +++ b/test/problems/divsort/generators/generators.yaml @@ -94,6 +94,8 @@ data: ans: 3.333333333 abcd out: 3.33 abcd valid_output: + testdata.yaml: + output_validator_args: float_tolerance 1e-2 data: valid: in: 10.0 3.0 ab cd From ea83959567cf8ed9ba4c7825902f05f47e3433f2 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Mon, 7 Apr 2025 23:45:52 +0200 Subject: [PATCH 23/53] refactored movetree --- bin/upgrade.py | 78 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/bin/upgrade.py b/bin/upgrade.py index 4dd6e1b08..859f6b238 100644 --- a/bin/upgrade.py +++ b/bin/upgrade.py @@ -13,6 +13,58 @@ from ruamel.yaml.comments import CommentedMap, CommentedSeq +def _move_dir(src_base: Path, dst_base: Path) -> None: + assert src_base.is_dir() + assert not dst_base.exists() + + src_base = src_base.absolute() + dst_base = dst_base.absolute() + base = [a for a, b in zip(reversed(src_base.parents), reversed(dst_base.parents)) if a == b][-1] + + def movetree(src: Path, dst: Path, depth: int = 0) -> None: + dst.mkdir(parents=True) + for file in [*src.iterdir()]: + new_file = dst / file.name + if file.is_symlink(): + # create a new symlink and make sure that the destination is handled properly + destination = file.readlink() + if destination.is_absolute(): + # absolute links should stay absolute + # if their destination is inside the dir we move we have to change it + if destination.is_relative_to(src_base): + destination = dst_base / destination.relative_to(src_base) + new_file.symlink_to(destination) + file.unlink() + else: + delta = sum(map(lambda x: -1 if x == ".." else 1, destination.parts)) + if depth + delta > 0: + # the link is relative and points to another file we move + file.rename(new_file) + else: + # the link is relative but points to a fixed place + src_rel = src.relative_to(base) + dst_rel = dst.relative_to(base) + parts = (("..",) * len(dst_rel.parts)) + src_rel.parts + destination.parts + resolved: list[str] = [] + for part in parts: + if part == ".." and len(resolved) and resolved[-1] != "..": + resolved.pop() + else: + resolved.append(part) + new_file.symlink_to(Path(*resolved)) + file.unlink() + elif file.is_dir(): + # recursively move (and delete) dirs + movetree(file, new_file, depth + 1) + else: + # recursively copy and delete dirs + file.rename(new_file) + # delete now empty dir + src.rmdir() + + movetree(src_base, dst_base) + + def upgrade_data(problem_path: Path, bar: ProgressBar) -> None: rename = [ ("data/invalid_inputs", "data/invalid_input"), @@ -249,31 +301,7 @@ def upgrade_output_validators(problem_path: Path, bar: ProgressBar) -> None: bar.log( f"renaming 'output_validators/{content[0].name}' to '{OutputValidator.source_dir}/'" ) - - def move(src: str, dst: str) -> None: - if Path(src).is_symlink(): - src_dst = Path(src).resolve() - if src_dst.is_relative_to(content[0]): # local symlink - Path(src).rename(dst) - else: # link outside output_validators/ - dst_pos = Path(dst).resolve() - common = [ - a - for a, b in zip(reversed(src_dst.parents), reversed(dst_pos.parents)) - if a == b - ][-1] - link = Path( - "../" * (len(dst_pos.parents) - len(common.parts)) - ) / src_dst.relative_to(common) - Path(dst).symlink_to(link) - Path(src).unlink() - else: - Path(src).rename(dst) - - shutil.copytree( - content[0], problem_path / OutputValidator.source_dir, copy_function=move - ) - shutil.rmtree(problem_path / "output_validators") + _move_dir(content[0], problem_path / OutputValidator.source_dir) else: bar.log(f"renaming 'output_validators/' to '{OutputValidator.source_dir}/'") (problem_path / "output_validators").rename(problem_path / OutputValidator.source_dir) From 4d73c0cc19866bc54c52ced4ea1a5717ff577347 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Tue, 8 Apr 2025 11:12:35 +0200 Subject: [PATCH 24/53] allow moving sylinks --- bin/upgrade.py | 78 ++++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/bin/upgrade.py b/bin/upgrade.py index 859f6b238..7c92bde50 100644 --- a/bin/upgrade.py +++ b/bin/upgrade.py @@ -13,6 +13,9 @@ from ruamel.yaml.comments import CommentedMap, CommentedSeq +# src_base must be a dir (or symlink to dir) +# dst_base must not exists +# the parents of dst_base must exist def _move_dir(src_base: Path, dst_base: Path) -> None: assert src_base.is_dir() assert not dst_base.exists() @@ -22,45 +25,44 @@ def _move_dir(src_base: Path, dst_base: Path) -> None: base = [a for a, b in zip(reversed(src_base.parents), reversed(dst_base.parents)) if a == b][-1] def movetree(src: Path, dst: Path, depth: int = 0) -> None: - dst.mkdir(parents=True) - for file in [*src.iterdir()]: - new_file = dst / file.name - if file.is_symlink(): - # create a new symlink and make sure that the destination is handled properly - destination = file.readlink() - if destination.is_absolute(): - # absolute links should stay absolute - # if their destination is inside the dir we move we have to change it - if destination.is_relative_to(src_base): - destination = dst_base / destination.relative_to(src_base) - new_file.symlink_to(destination) - file.unlink() - else: - delta = sum(map(lambda x: -1 if x == ".." else 1, destination.parts)) - if depth + delta > 0: - # the link is relative and points to another file we move - file.rename(new_file) - else: - # the link is relative but points to a fixed place - src_rel = src.relative_to(base) - dst_rel = dst.relative_to(base) - parts = (("..",) * len(dst_rel.parts)) + src_rel.parts + destination.parts - resolved: list[str] = [] - for part in parts: - if part == ".." and len(resolved) and resolved[-1] != "..": - resolved.pop() - else: - resolved.append(part) - new_file.symlink_to(Path(*resolved)) - file.unlink() - elif file.is_dir(): - # recursively move (and delete) dirs - movetree(file, new_file, depth + 1) + if src.is_symlink(): + # create a new symlink and make sure that the destination is handled properly + destination = src.readlink() + if destination.is_absolute(): + # absolute links should stay absolute + # if their destination is inside the dir we move we have to change it + if destination.is_relative_to(src_base): + destination = dst_base / destination.relative_to(src_base) + dst.symlink_to(destination) + src.unlink() else: - # recursively copy and delete dirs - file.rename(new_file) - # delete now empty dir - src.rmdir() + delta = sum(map(lambda x: -1 if x == ".." else 1, destination.parts)) + if depth + delta > 1: + # the link is relative and points to another file we move + src.rename(dst) + else: + # the link is relative but points to a fixed place + src_rel = src.parent.relative_to(base) + dst_rel = dst.parent.relative_to(base) + parts = (("..",) * len(dst_rel.parts)) + src_rel.parts + destination.parts + resolved: list[str] = [] + for part in parts: + if part == ".." and len(resolved) and resolved[-1] != "..": + resolved.pop() + else: + resolved.append(part) + dst.symlink_to(Path(*resolved)) + src.unlink() + elif src.is_dir(): + # recursively move stuff inside dirs + dst.mkdir() + for file in [*src.iterdir()]: + movetree(file, dst / file.name, depth + 1) + # delete now empty dir + src.rmdir() + else: + # move file + src.rename(dst) movetree(src_base, dst_base) From 07a5125f5f2c7daa39cf55ed538c463c12f3869c Mon Sep 17 00:00:00 2001 From: mzuenni Date: Wed, 9 Apr 2025 01:21:15 +0200 Subject: [PATCH 25/53] improve _move_dir --- bin/upgrade.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/bin/upgrade.py b/bin/upgrade.py index 7c92bde50..3803bac2a 100644 --- a/bin/upgrade.py +++ b/bin/upgrade.py @@ -24,7 +24,18 @@ def _move_dir(src_base: Path, dst_base: Path) -> None: dst_base = dst_base.absolute() base = [a for a, b in zip(reversed(src_base.parents), reversed(dst_base.parents)) if a == b][-1] - def movetree(src: Path, dst: Path, depth: int = 0) -> None: + def resolve_up(parts: tuple[str, ...]) -> Path: + resolved: list[str] = [] + for part in parts: + if part == ".": + continue + if part == ".." and len(resolved) and resolved[-1] != "..": + resolved.pop() + else: + resolved.append(part) + return Path(*resolved) + + def movetree(src: Path, dst: Path) -> None: if src.is_symlink(): # create a new symlink and make sure that the destination is handled properly destination = src.readlink() @@ -36,8 +47,7 @@ def movetree(src: Path, dst: Path, depth: int = 0) -> None: dst.symlink_to(destination) src.unlink() else: - delta = sum(map(lambda x: -1 if x == ".." else 1, destination.parts)) - if depth + delta > 1: + if resolve_up(src.parent.parts + destination.parts).is_relative_to(src_base): # the link is relative and points to another file we move src.rename(dst) else: @@ -45,19 +55,13 @@ def movetree(src: Path, dst: Path, depth: int = 0) -> None: src_rel = src.parent.relative_to(base) dst_rel = dst.parent.relative_to(base) parts = (("..",) * len(dst_rel.parts)) + src_rel.parts + destination.parts - resolved: list[str] = [] - for part in parts: - if part == ".." and len(resolved) and resolved[-1] != "..": - resolved.pop() - else: - resolved.append(part) - dst.symlink_to(Path(*resolved)) + dst.symlink_to(resolve_up(parts)) src.unlink() elif src.is_dir(): # recursively move stuff inside dirs dst.mkdir() for file in [*src.iterdir()]: - movetree(file, dst / file.name, depth + 1) + movetree(file, dst / file.name) # delete now empty dir src.rmdir() else: From 64c0123899898d8cef7593c30ad9dc913b480aa1 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Thu, 10 Apr 2025 22:34:52 +0200 Subject: [PATCH 26/53] improved user experience --- bin/generate.py | 34 +++++++++++++++++-------- bin/problem.py | 4 +++ bin/run.py | 1 + bin/util.py | 67 ++++++++++++++++++++++++++----------------------- 4 files changed, 64 insertions(+), 42 deletions(-) diff --git a/bin/generate.py b/bin/generate.py index 7ec861404..68d2f8189 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -1017,13 +1017,31 @@ def generate_visualization(): return True if config.args.no_visualizer: return True - visualizer = problem.visualizer(visualize.InputVisualizer) or problem.visualizer( - visualize.OutputVisualizer + + # Generate visualization + in_path = cwd / "testcase.in" + ans_path = cwd / "testcase.ans" + out_path = cwd / "testcase.out" + assert in_path.is_file() + assert ans_path.is_file() + + visualizer: Optional[visualize.AnyVisualizer] = problem.visualizer( + visualize.InputVisualizer ) + output_visualizer = problem.visualizer(visualize.OutputVisualizer) + if output_visualizer is not None and ( + out_path.is_file() or problem.settings.ans_is_output + ): + if visualizer is None or out_path.is_file(): + visualizer = output_visualizer + + if not out_path.is_file(): + assert problem.settings.ans_is_output + out_path = ans_path + if visualizer is None: return True - # TODO if the input visualizer gets args these need to be hashed as well visualizer_hash = { "visualizer_hash": visualizer.hash, "visualizer_args": testcase.testdata_yaml_args(visualizer, PrintBar()), @@ -1032,18 +1050,12 @@ def generate_visualization(): if meta_yaml.get("visualizer_hash") == visualizer_hash: return True - # Generate visualization - in_path = cwd / "testcase.in" - ans_path = cwd / "testcase.ans" - assert in_path.is_file() - assert ans_path.is_file() - for ext in config.KNOWN_VISUALIZER_EXTENSIONS: in_path.with_suffix(ext).unlink(True) if isinstance(visualizer, visualize.InputVisualizer): result = visualizer.run(in_path, ans_path, cwd) - elif not problem.multi_pass: + else: feedbackdir = in_path.with_suffix(".feedbackdir") feedbackdir.mkdir(parents=True, exist_ok=True) teamimage = feedbackdir / "teamimage" @@ -1053,7 +1065,7 @@ def generate_visualization(): teamimage.with_suffix(ext).unlink(True) judgeimage.with_suffix(ext).unlink(True) - result = visualizer.run(in_path, ans_path, ans_path, feedbackdir) + result = visualizer.run(in_path, ans_path, out_path, feedbackdir) if result.status: found = None for ext in config.KNOWN_VISUALIZER_EXTENSIONS: diff --git a/bin/problem.py b/bin/problem.py index 1fc63a8d6..029e330e6 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -1046,6 +1046,10 @@ def run_submissions(problem): testcases, submissions = ts_pair ok, verdict_table = Problem.run_some(testcases, submissions) + if len(testcases) * len(submissions) > 1: + if not config.args.verbose and not config.args.no_visualizer: + log("use -v with --visualize to see the paths to the generated images") + if config.args.table: Problem._print_table(verdict_table.results, testcases) elif config.args.overview and not config.args.tree: diff --git a/bin/run.py b/bin/run.py index bc2655b10..43141b64a 100644 --- a/bin/run.py +++ b/bin/run.py @@ -446,6 +446,7 @@ def process_run(run: Run): continue if f.name.startswith("judgeimage.") or f.name.startswith("teamimage."): data += f"{f.name}: {shorten_path(self.problem, f.parent) / f.name}\n" + ensure_symlink(run.problem.path / f.name, f, output=True, relative=False) continue if not f.is_file(): localbar.warn(f"Validator wrote to {f} but it's not a file.") diff --git a/bin/util.py b/bin/util.py index bea2cc4bb..7475fd4e4 100644 --- a/bin/util.py +++ b/bin/util.py @@ -993,41 +993,46 @@ def strip_newline(s: str) -> str: # When output is True, copy the file when args.cp is true. -def ensure_symlink(link: Path, target: Path, output: bool = False, relative: bool = False) -> None: - # on windows copy if necessary - if is_windows() and not windows_can_symlink: - if link.exists() or link.is_symlink(): - link.unlink() - shutil.copyfile(target, link) - return +def ensure_symlink(link: Path, target: Path, output: bool = False, relative: bool = False) -> bool: + try: + # on windows copy if necessary + if is_windows() and not windows_can_symlink: + if link.exists() or link.is_symlink(): + link.unlink() + shutil.copyfile(target, link) + return True - # For output files: copy them on Windows, or when --cp is passed. - if output and config.args.cp: - if link.exists() or link.is_symlink(): - link.unlink() - shutil.copyfile(target, link) - return + # For output files: copy them on Windows, or when --cp is passed. + if output and config.args.cp: + if link.exists() or link.is_symlink(): + link.unlink() + shutil.copyfile(target, link) + return True - # Do nothing if link already points to the right target. - if link.is_symlink() and link.resolve() == target.resolve(): - is_absolute = os.readlink(link) - if not relative and is_absolute: - return - # if relative and not is_absolute: return + # Do nothing if link already points to the right target. + if link.is_symlink() and link.resolve() == target.resolve(): + is_absolute = os.readlink(link) + if not relative and is_absolute: + return True + # if relative and not is_absolute: return - if link.is_symlink() or link.exists(): - if link.is_dir() and not link.is_symlink(): - shutil.rmtree(link) - else: - link.unlink() + if link.is_symlink() or link.exists(): + if link.is_dir() and not link.is_symlink(): + shutil.rmtree(link) + else: + link.unlink() - # for windows the symlink needs to know if it points to a directory or file - if relative: - # Rewrite target to be relative to link. - # Use os.path.relpath instead of Path.relative_to for non-subdirectories. - link.symlink_to(os.path.relpath(target, link.parent), target.is_dir()) - else: - link.symlink_to(target.resolve(), target.is_dir()) + # for windows the symlink needs to know if it points to a directory or file + if relative: + # Rewrite target to be relative to link. + # Use os.path.relpath instead of Path.relative_to for non-subdirectories. + link.symlink_to(os.path.relpath(target, link.parent), target.is_dir()) + else: + link.symlink_to(target.resolve(), target.is_dir()) + return True + except (FileNotFoundError, FileExistsError): + # this must be a race condition + return False def has_substitute( From 2c801fdb6d885eb7f89982dc1a7992f5b48612f1 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Thu, 10 Apr 2025 22:44:35 +0200 Subject: [PATCH 27/53] add extra assert --- bin/generate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/generate.py b/bin/generate.py index 68d2f8189..443e4f570 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -1056,6 +1056,8 @@ def generate_visualization(): if isinstance(visualizer, visualize.InputVisualizer): result = visualizer.run(in_path, ans_path, cwd) else: + assert out_path.is_file() + feedbackdir = in_path.with_suffix(".feedbackdir") feedbackdir.mkdir(parents=True, exist_ok=True) teamimage = feedbackdir / "teamimage" From 0b8715594afaa319dc1d02fb7efbf04e7d72f99b Mon Sep 17 00:00:00 2001 From: mzuenni Date: Fri, 11 Apr 2025 11:10:50 +0200 Subject: [PATCH 28/53] substitute more constants in export --- bin/export.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/export.py b/bin/export.py index a22c16567..6ae8bf056 100644 --- a/bin/export.py +++ b/bin/export.py @@ -215,6 +215,8 @@ def add_testcase(in_file: Path) -> None: f"{OutputValidator.source_dir}/**/*", # "statement/*", "solution/*", "problem_slide/*", use \constant{} commands # "submissions/*/**/*", removed support? + f"{InputVisualizer.source_dir}/**/*", + f"{OutputVisualizer.source_dir}/**/*", ] for pattern in constants_supported: for f in export_dir.glob(pattern): From fd6c4debb2ceca9b84ff8d6bb43e9326923aee74 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 12 Apr 2025 10:34:51 +0200 Subject: [PATCH 29/53] implement visualizer for interactive problems --- bin/generate.py | 20 +++++++++----------- bin/interactive.py | 4 ++++ bin/run.py | 7 ++++--- bin/visualize.py | 17 ++++++++++------- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/bin/generate.py b/bin/generate.py index 443e4f570..0cf24b6c5 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -1029,15 +1029,13 @@ def generate_visualization(): visualize.InputVisualizer ) output_visualizer = problem.visualizer(visualize.OutputVisualizer) - if output_visualizer is not None and ( - out_path.is_file() or problem.settings.ans_is_output - ): - if visualizer is None or out_path.is_file(): - visualizer = output_visualizer + if output_visualizer is not None: + if out_path.is_file() or problem.settings.ans_is_output or problem.interactive: + if visualizer is None or out_path.is_file(): + visualizer = output_visualizer - if not out_path.is_file(): - assert problem.settings.ans_is_output - out_path = ans_path + if not out_path.is_file() and problem.settings.ans_is_output: + out_path = ans_path if visualizer is None: return True @@ -1056,8 +1054,6 @@ def generate_visualization(): if isinstance(visualizer, visualize.InputVisualizer): result = visualizer.run(in_path, ans_path, cwd) else: - assert out_path.is_file() - feedbackdir = in_path.with_suffix(".feedbackdir") feedbackdir.mkdir(parents=True, exist_ok=True) teamimage = feedbackdir / "teamimage" @@ -1067,7 +1063,9 @@ def generate_visualization(): teamimage.with_suffix(ext).unlink(True) judgeimage.with_suffix(ext).unlink(True) - result = visualizer.run(in_path, ans_path, out_path, feedbackdir) + result = visualizer.run( + in_path, ans_path, out_path if not problem.interactive else None, feedbackdir + ) if result.status: found = None for ext in config.KNOWN_VISUALIZER_EXTENSIONS: diff --git a/bin/interactive.py b/bin/interactive.py index eb1a85418..540c7255c 100644 --- a/bin/interactive.py +++ b/bin/interactive.py @@ -167,6 +167,8 @@ def get_validator_command(): verdict = Verdict.VALIDATOR_CRASH break + run._visualize_output(bar or PrintBar("Visualize interactive test case")) + if tle_result is None: # Set result.err to validator error and result.out to team error. return ExecResult( @@ -431,6 +433,8 @@ def kill_handler_function(): if interaction_file is not None: interaction_file.close() + run._visualize_output(bar or PrintBar("Visualize interactive test case")) + if tle_result is None: return ExecResult( None, diff --git a/bin/run.py b/bin/run.py index 43141b64a..51c05fe6f 100644 --- a/bin/run.py +++ b/bin/run.py @@ -16,6 +16,7 @@ import visualize from testcase import Testcase from util import ( + BAR_TYPE, crop_output, ensure_symlink, error, @@ -219,7 +220,7 @@ def _prepare_nextpass(self, nextpass): shutil.move(nextpass, self.in_path) return True - def _validate_output(self, bar: ProgressBar) -> Optional[ExecResult]: + def _validate_output(self, bar: BAR_TYPE) -> Optional[ExecResult]: output_validators = self.problem.validators(validate.OutputValidator) if not output_validators: return None @@ -231,7 +232,7 @@ def _validate_output(self, bar: ProgressBar) -> Optional[ExecResult]: args=self.testcase.testdata_yaml_args(output_validator, bar), ) - def _visualize_output(self, bar: ProgressBar) -> Optional[ExecResult]: + def _visualize_output(self, bar: BAR_TYPE) -> Optional[ExecResult]: if config.args.no_visualizer: return None output_visualizer = self.problem.visualizer(visualize.OutputVisualizer) @@ -240,7 +241,7 @@ def _visualize_output(self, bar: ProgressBar) -> Optional[ExecResult]: return output_visualizer.run( self.in_path, self.testcase.ans_path.resolve(), - self.out_path, + self.out_path if not self.problem.interactive else None, self.feedbackdir, args=self.testcase.testdata_yaml_args(output_visualizer, bar), ) diff --git a/bin/visualize.py b/bin/visualize.py index 1deaaff11..3a6c20a56 100644 --- a/bin/visualize.py +++ b/bin/visualize.py @@ -75,18 +75,21 @@ def run( self, in_path: Path, ans_path: Path, - out_path: Path, + out_path: Optional[Path], cwd: Path, args: Optional[list[str]] = None, ) -> ExecResult: assert self.run_command is not None, "Output Visualizer should be built before running it" + assert (out_path is None) == self.problem.interactive, ( + "out_path should be None if and only if problem is interactive" + ) - with out_path.open("rb") as out_file: - return self._exec_command( - self.run_command + [in_path, ans_path, cwd] + (args or []), - stdin=out_file, - cwd=cwd, - ) + command = self.run_command + [in_path, ans_path, cwd] + (args or []) + if out_path is not None: + with out_path.open("rb") as out_file: + return self._exec_command(command, stdin=out_file, cwd=cwd) + else: + return self._exec_command(command, cwd=cwd) AnyVisualizer = InputVisualizer | OutputVisualizer From 271a1604f2e10a8e91b26015600a8c356680c8e0 Mon Sep 17 00:00:00 2001 From: Thore Husfeldt Date: Sun, 13 Apr 2025 21:13:21 +0200 Subject: [PATCH 30/53] `identity` output-visualize also ans and in --- test/problems/identity/output_visualizer/run | 5 +++++ .../identity/output_visualizer/visualize.asy | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100755 test/problems/identity/output_visualizer/run create mode 100644 test/problems/identity/output_visualizer/visualize.asy diff --git a/test/problems/identity/output_visualizer/run b/test/problems/identity/output_visualizer/run new file mode 100755 index 000000000..9f66478c9 --- /dev/null +++ b/test/problems/identity/output_visualizer/run @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +set -e + +asy -f png $(dirname $0)/visualize.asy -u infilename="'${1}'" -u ansfilename="'${2}'" -o $3/judgeimage.png diff --git a/test/problems/identity/output_visualizer/visualize.asy b/test/problems/identity/output_visualizer/visualize.asy new file mode 100644 index 000000000..dc5bda3b0 --- /dev/null +++ b/test/problems/identity/output_visualizer/visualize.asy @@ -0,0 +1,20 @@ +defaultpen(1); + +string outvalue = stdin; + +string infilename; +string ansfilename; +usersetting(); +file fin=input(infilename); +file fans=input(infilename); +string invalue = fin; +string ansvalue = fans; + +string label = "\texttt{in}: " + invalue ; +label(scale(5)*label, (0,200)); +string label = "\texttt{ans}: " + ansvalue ; +label(scale(5)*label, (0,100)); +pen labelPen = (invalue == outvalue) ? green : red; +string label = "\texttt{out}: " + outvalue ; +label(scale(5)*label, (0,0), p=labelPen); +shipout(bbox(xmargin=5, white, Fill)); From 293e38184674f64c2596056c46c12aad3bb05084 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 13 Apr 2025 22:48:45 +0200 Subject: [PATCH 31/53] add example arg --- test/problems/identity/data/sample/1.png | Bin 0 -> 2083 bytes test/problems/identity/data/sample/2.png | Bin 0 -> 2653 bytes test/problems/identity/data/sample/3.png | Bin 0 -> 1841 bytes test/problems/identity/data/sample/4.png | Bin 0 -> 2585 bytes test/problems/identity/data/sample/5.png | Bin 0 -> 2112 bytes test/problems/identity/data/sample/testdata.yaml | 1 + .../problems/identity/generators/generators.yaml | 2 ++ test/problems/identity/output_visualizer/run | 10 +++++++++- .../identity/output_visualizer_disabled/run | 5 ----- .../output_visualizer_disabled/visualize.asy | 5 ----- 10 files changed, 12 insertions(+), 11 deletions(-) create mode 100644 test/problems/identity/data/sample/1.png create mode 100644 test/problems/identity/data/sample/2.png create mode 100644 test/problems/identity/data/sample/3.png create mode 100644 test/problems/identity/data/sample/4.png create mode 100644 test/problems/identity/data/sample/5.png create mode 100644 test/problems/identity/data/sample/testdata.yaml delete mode 100755 test/problems/identity/output_visualizer_disabled/run delete mode 100644 test/problems/identity/output_visualizer_disabled/visualize.asy diff --git a/test/problems/identity/data/sample/1.png b/test/problems/identity/data/sample/1.png new file mode 100644 index 0000000000000000000000000000000000000000..3c98427b743ba80d575d8b64de073d66d45f919d GIT binary patch literal 2083 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 literal 0 HcmV?d00001 diff --git a/test/problems/identity/data/sample/4.png b/test/problems/identity/data/sample/4.png new file mode 100644 index 0000000000000000000000000000000000000000..c2a5887de6380704989a9d42aca352f9ae8c9405 GIT binary patch literal 2585 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 LS8S=)x03z?L~I5s literal 0 HcmV?d00001 diff --git a/test/problems/identity/data/sample/testdata.yaml b/test/problems/identity/data/sample/testdata.yaml new file mode 100644 index 000000000..81bc18252 --- /dev/null +++ b/test/problems/identity/data/sample/testdata.yaml @@ -0,0 +1 @@ +output_visualizer_args: --draw-please diff --git a/test/problems/identity/generators/generators.yaml b/test/problems/identity/generators/generators.yaml index 65afceab4..0152709fb 100644 --- a/test/problems/identity/generators/generators.yaml +++ b/test/problems/identity/generators/generators.yaml @@ -72,6 +72,8 @@ data: "6": in.statement: "6" ans.statement: "6" + testdata.yaml: + output_visualizer_args: --draw-please secret: data: diff --git a/test/problems/identity/output_visualizer/run b/test/problems/identity/output_visualizer/run index 9f66478c9..d8d0ead16 100755 --- a/test/problems/identity/output_visualizer/run +++ b/test/problems/identity/output_visualizer/run @@ -2,4 +2,12 @@ set -e -asy -f png $(dirname $0)/visualize.asy -u infilename="'${1}'" -u ansfilename="'${2}'" -o $3/judgeimage.png +draw=false +for var in "$@" +do + [ "$var" = "--draw-please" ] && draw=true +done + +if [ "$draw" = true ]; then + asy -f png $(dirname $0)/visualize.asy -u infilename="'${1}'" -u ansfilename="'${2}'" -o $3/judgeimage.png +fi diff --git a/test/problems/identity/output_visualizer_disabled/run b/test/problems/identity/output_visualizer_disabled/run deleted file mode 100755 index ac445a73e..000000000 --- a/test/problems/identity/output_visualizer_disabled/run +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env sh - -set -e - -asy -f png $(dirname $0)/visualize.asy -o $3/judgeimage.png diff --git a/test/problems/identity/output_visualizer_disabled/visualize.asy b/test/problems/identity/output_visualizer_disabled/visualize.asy deleted file mode 100644 index 6d0d328c7..000000000 --- a/test/problems/identity/output_visualizer_disabled/visualize.asy +++ /dev/null @@ -1,5 +0,0 @@ -defaultpen(1); - -string n = stdin; -label(scale(5)*n, (0,0)); -shipout(bbox(xmargin=5, white, Fill)); From 960ec8ea286a8bd004625bcbe8c025b92e832739 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 13 Apr 2025 22:49:22 +0200 Subject: [PATCH 32/53] fix arg passing --- bin/generate.py | 18 ++++++++++++++---- bin/problem.py | 7 ++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/bin/generate.py b/bin/generate.py index 0cf24b6c5..46e31820e 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -1040,9 +1040,11 @@ def generate_visualization(): if visualizer is None: return True + visualizer_args = testcase.testdata_yaml_args(visualizer, PrintBar()) + visualizer_hash = { "visualizer_hash": visualizer.hash, - "visualizer_args": testcase.testdata_yaml_args(visualizer, PrintBar()), + "visualizer_args": visualizer_args, } if meta_yaml.get("visualizer_hash") == visualizer_hash: @@ -1064,7 +1066,11 @@ def generate_visualization(): judgeimage.with_suffix(ext).unlink(True) result = visualizer.run( - in_path, ans_path, out_path if not problem.interactive else None, feedbackdir + in_path, + ans_path, + out_path if not problem.interactive else None, + feedbackdir, + visualizer_args, ) if result.status: found = None @@ -1082,10 +1088,14 @@ def generate_visualization(): if result.status == ExecStatus.TIMEOUT: bar.debug(f"{Style.RESET_ALL}-> {shorten_path(problem, cwd)}") - bar.error(f"Input Visualizer TIMEOUT after {result.duration}s") + bar.error( + f"{type(visualizer).visualizer_type.capitalize()} Visualizer TIMEOUT after {result.duration}s" + ) elif not result.status: bar.debug(f"{Style.RESET_ALL}-> {shorten_path(problem, cwd)}") - bar.error("Input Visualizer failed", result.err) + bar.error( + f"{type(visualizer).visualizer_type.capitalize()} Visualizer failed", result.err + ) if result.status and config.args.error and result.err: bar.log("stderr", result.err) diff --git a/bin/problem.py b/bin/problem.py index 029e330e6..f3a67312d 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -469,7 +469,12 @@ def _parse_testdata_yaml(p, path, bar): # Verify testdata.yaml for k in flags: match k: - case validate.OutputValidator.args_key: + case ( + validate.OutputValidator.args_key + | validate.AnswerValidator.args_key + | visualize.InputVisualizer.args_key + | visualize.OutputVisualizer.args_key + ): if not isinstance(flags[k], str): bar.error(f"{k} must be string", resume=True, print_item=False) case validate.InputValidator.args_key: From b26ba5ea10e2eb413b479143c5d447a7335436af Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 13 Apr 2025 22:52:36 +0200 Subject: [PATCH 33/53] fix wsl --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7e9ceca75..f1f0a1946 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,5 +51,6 @@ jobs: texlive-science latexmk texlive-lang-german + asymptote - shell: wsl-bash {0} run: pytest From 69f93e60c3e1a7f4dbb37830fcf289a25f18c99a Mon Sep 17 00:00:00 2001 From: mzuenni Date: Mon, 14 Apr 2025 11:50:06 +0200 Subject: [PATCH 34/53] disable output visualizer again --- test/problems/identity/data/sample/1.png | Bin 2083 -> 0 bytes test/problems/identity/data/sample/2.png | Bin 2653 -> 0 bytes test/problems/identity/data/sample/3.png | Bin 1841 -> 0 bytes test/problems/identity/data/sample/4.png | Bin 2585 -> 0 bytes test/problems/identity/data/sample/5.png | Bin 2112 -> 0 bytes .../run | 0 .../visualize.asy | 0 7 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test/problems/identity/data/sample/1.png delete mode 100644 test/problems/identity/data/sample/2.png delete mode 100644 test/problems/identity/data/sample/3.png delete mode 100644 test/problems/identity/data/sample/4.png delete mode 100644 test/problems/identity/data/sample/5.png rename test/problems/identity/{output_visualizer => output_visualizer_disabled}/run (100%) rename test/problems/identity/{output_visualizer => output_visualizer_disabled}/visualize.asy (100%) diff --git a/test/problems/identity/data/sample/1.png b/test/problems/identity/data/sample/1.png deleted file mode 100644 index 3c98427b743ba80d575d8b64de073d66d45f919d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2083 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 diff --git a/test/problems/identity/data/sample/4.png b/test/problems/identity/data/sample/4.png deleted file mode 100644 index c2a5887de6380704989a9d42aca352f9ae8c9405..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2585 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 LS8S=)x03z?L~I5s diff --git a/test/problems/identity/output_visualizer/run b/test/problems/identity/output_visualizer_disabled/run similarity index 100% rename from test/problems/identity/output_visualizer/run rename to test/problems/identity/output_visualizer_disabled/run diff --git a/test/problems/identity/output_visualizer/visualize.asy b/test/problems/identity/output_visualizer_disabled/visualize.asy similarity index 100% rename from test/problems/identity/output_visualizer/visualize.asy rename to test/problems/identity/output_visualizer_disabled/visualize.asy From 5290aeb6226928f8164af414200edf1c0ef631ee Mon Sep 17 00:00:00 2001 From: mzuenni Date: Mon, 14 Apr 2025 15:21:05 +0200 Subject: [PATCH 35/53] removed outdated warning --- bin/problem.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/bin/problem.py b/bin/problem.py index f3a67312d..1a2a38107 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -888,17 +888,13 @@ def visualizer( if not path.is_dir(): return None if cls not in problem._visualizer_cache: - if cls == visualize.OutputVisualizer and problem.interactive: - problem._visualizer_cache[cls] = None - warn("Output Visualizer is not supported for interactive problem. IGNORED.") - else: - visualizer = cls(problem, path) - bar = ProgressBar(f"Building {cls.visualizer_type} visualizer", items=[visualizer]) - localbar = bar.start(visualizer) - visualizer.build(localbar) - localbar.done() - bar.finalize(print_done=False) - problem._visualizer_cache[cls] = visualizer if visualizer.ok else None + visualizer = cls(problem, path) + bar = ProgressBar(f"Building {cls.visualizer_type} visualizer", items=[visualizer]) + localbar = bar.start(visualizer) + visualizer.build(localbar) + localbar.done() + bar.finalize(print_done=False) + problem._visualizer_cache[cls] = visualizer if visualizer.ok else None return problem._visualizer_cache[cls] def validators( From 154805b7efcf0249f1a66d26ea61105bfb82b7a7 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Mon, 14 Apr 2025 15:24:47 +0200 Subject: [PATCH 36/53] always copy output visualizer from skel --- bin/skel.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bin/skel.py b/bin/skel.py index 8a6cc09f3..ba0c2a6ff 100644 --- a/bin/skel.py +++ b/bin/skel.py @@ -11,7 +11,6 @@ from problem import Problem from util import * from validate import OutputValidator -from visualize import OutputVisualizer # Returns the alphanumeric version of a string: @@ -184,8 +183,6 @@ def new_problem() -> None: skip = [] if custom_output: skip.append(skeldir / OutputValidator.source_dir) - if "interactive" in problem_type: - skip.append(skeldir / OutputVisualizer.source_dir) copytree_and_substitute( skeldir, From 9aa7cd47315b9610b9589780814c92c1c191e998 Mon Sep 17 00:00:00 2001 From: Thore Husfeldt Date: Mon, 14 Apr 2025 18:40:01 +0200 Subject: [PATCH 37/53] `guess` output visualizer for an interactive problem --- .../guess-visualizer.py | 32 +++++++++++++++ .../guess/output_visualizer_disabled/run | 41 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 test/problems/guess/output_visualizer_disabled/guess-visualizer.py create mode 100755 test/problems/guess/output_visualizer_disabled/run diff --git a/test/problems/guess/output_visualizer_disabled/guess-visualizer.py b/test/problems/guess/output_visualizer_disabled/guess-visualizer.py new file mode 100644 index 000000000..4f201edf2 --- /dev/null +++ b/test/problems/guess/output_visualizer_disabled/guess-visualizer.py @@ -0,0 +1,32 @@ +import sys +from pathlib import Path + + +with open(sys.argv[1]) as in_file, open(sys.argv[3] / Path("judgemessage.txt"), "r") as msg_file: + mode = in_file.read().split()[0] + assert mode in ("random", "fixed", "adaptive"), mode + judgemessages = iter(msg_file) + + print(r"""\documentclass[varwidth]{standalone} +\usepackage{tikz} +\usetikzlibrary{patterns} +\tikzset{every node/.style={font=\sffamily}} +\begin{document} +\begin{tikzpicture} + """) + if not mode == "adaptive": + secret = int(next(judgemessages).split()[-1]) + print(rf"\node at ({secret / 100},-1.5) {{ {secret} ({mode}) }};") + else: + next(judgemessages) + print(r"\node at (5,-.5) { adaptive };") + for line in judgemessages: + rnd, guess = int(line.split()[1]), int(line.split()[3]) + y = -1 - rnd + print(rf"\draw [very thick, blue!20] (0, {y}) -- (10, {y});") + print(rf"\node at ({guess / 100}, {y})[anchor=north]", r"{$\uparrow$};") + print(rf"\node at ({guess / 100}, {y - 0.5})[anchor=north] {{ {guess} }};") + if not mode == "adaptive": + print(rf"\draw [red] ({secret / 100}, {-rnd - 1}) -- ({secret / 100}, 0);") + + print(r"\end{tikzpicture}\end{document}") diff --git a/test/problems/guess/output_visualizer_disabled/run b/test/problems/guess/output_visualizer_disabled/run new file mode 100755 index 000000000..62229c730 --- /dev/null +++ b/test/problems/guess/output_visualizer_disabled/run @@ -0,0 +1,41 @@ +#!/bin/bash + +# Set script directory +SCRIPT_DIR="$(dirname "$0")" + +# Check if visualize.py exists +if [[ ! -f "$SCRIPT_DIR/guess-visualizer.py" ]]; then + echo "Error: guess-visualizer.py not found in $SCRIPT_DIR" >&2 + exit 1 +fi + +tmptexdir=$(mktemp -d) # Create a unique temporary directory +OUTPUT_FILE="$tmptexdir/judgeimage.tex" + +# Run visualize.py +python3 "$SCRIPT_DIR/guess-visualizer.py" $1 $2 $3 > "$OUTPUT_FILE" +if [[ $? -ne 0 ]]; then + echo "Error: guess-visualizer.py failed" >&2 + exit 1 +fi + +# Check if judgeimage.tex exists +if [[ ! -f "$OUTPUT_FILE" ]]; then + echo "Error: texfile not found in $SCRIPT_DIR" >&2 + exit 1 +fi + +# Run pdflatex +( + cd "$tmptexdir" && pdflatex judgeimage.tex +) +if [[ $? -ne 0 ]]; then + echo "Error: pdflatex failed" >&2 + exit 1 +fi + +mv "$tmptexdir/judgeimage.pdf" $3 +rm -r "$tmptexdir" + +echo "Script completed successfully." +exit 0 From 00bd79d3ed0ba3885fbd07136b75bf4a44da840f Mon Sep 17 00:00:00 2001 From: mzuenni Date: Tue, 15 Apr 2025 10:59:41 +0200 Subject: [PATCH 38/53] update type --- bin/generate.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bin/generate.py b/bin/generate.py index 46e31820e..30ba7c3e9 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -121,9 +121,7 @@ def __init__(self, problem: Problem, string: str, *, allow_absolute: bool, allow raise ParseException("{seed(:[0-9]+)} may appear at most once.") # Automatically set self.program when that program has been built. - self.program: Optional[program.Generator | visualize.InputVisualizer | run.Submission] = ( - None - ) + self.program: Optional[program.Generator | run.Submission] = None def callback(program): self.program = program From 5f705881fd80b11a0587e4379825a7a2be4db999 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Tue, 15 Apr 2025 11:02:28 +0200 Subject: [PATCH 39/53] more feedback --- bin/generate.py | 4 ++-- bin/interactive.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/generate.py b/bin/generate.py index 30ba7c3e9..15560ad81 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -1889,10 +1889,10 @@ def collect_programs(t): self.root_dir.walk(collect_programs, dir_f=None) def build_programs( - program_type: type[program.Generator | visualize.InputVisualizer | run.Submission], + program_type: type[program.Generator | run.Submission], program_paths: Iterable[Path], ): - programs = list[program.Generator | visualize.InputVisualizer | run.Submission]() + programs = list[program.Generator | run.Submission]() for program_path in program_paths: path = self.problem.path / program_path if program_type is program.Generator and program_path in self.generators: diff --git a/bin/interactive.py b/bin/interactive.py index 540c7255c..8f587b80f 100644 --- a/bin/interactive.py +++ b/bin/interactive.py @@ -167,7 +167,7 @@ def get_validator_command(): verdict = Verdict.VALIDATOR_CRASH break - run._visualize_output(bar or PrintBar("Visualize interactive test case")) + run._visualize_output(bar or PrintBar("Visualize interaction")) if tle_result is None: # Set result.err to validator error and result.out to team error. @@ -433,7 +433,7 @@ def kill_handler_function(): if interaction_file is not None: interaction_file.close() - run._visualize_output(bar or PrintBar("Visualize interactive test case")) + run._visualize_output(bar or PrintBar("Visualize interaction")) if tle_result is None: return ExecResult( From 32075e563a6db3921cceff93c8e4b29f6dce0371 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Tue, 15 Apr 2025 11:03:29 +0200 Subject: [PATCH 40/53] Update skel/problem/input_visualizer/readme.md Co-authored-by: Maarten Sijm <9739541+mpsijm@users.noreply.github.com> --- skel/problem/input_visualizer/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skel/problem/input_visualizer/readme.md b/skel/problem/input_visualizer/readme.md index c983127a4..b4b83c66c 100644 --- a/skel/problem/input_visualizer/readme.md +++ b/skel/problem/input_visualizer/readme.md @@ -1,2 +1,2 @@ -This input visualizer is intended for use with BAPCtools `bt generate`. +This input visualizer is intended for use with BAPCtools' `bt generate`. The visualizer should be invoked as `./visualizer input answer` and writes a `testcase.`. From 2c95159ff6c2a3d33a6cea7d8547abae97479d96 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Tue, 15 Apr 2025 11:04:25 +0200 Subject: [PATCH 41/53] Update skel/problem/input_visualizer/readme.md Co-authored-by: Maarten Sijm <9739541+mpsijm@users.noreply.github.com> --- skel/problem/input_visualizer/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skel/problem/input_visualizer/readme.md b/skel/problem/input_visualizer/readme.md index b4b83c66c..0ef426286 100644 --- a/skel/problem/input_visualizer/readme.md +++ b/skel/problem/input_visualizer/readme.md @@ -1,2 +1,2 @@ This input visualizer is intended for use with BAPCtools' `bt generate`. -The visualizer should be invoked as `./visualizer input answer` and writes a `testcase.`. +The visualizer should be invoked as `./visualizer <...input_visualizer_args>` and should write a `testcase.` file. From 8f7385f8867edbf475eb5c536ba8d7883317d793 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Tue, 15 Apr 2025 11:06:43 +0200 Subject: [PATCH 42/53] Update bin/generate.py Co-authored-by: Maarten Sijm <9739541+mpsijm@users.noreply.github.com> --- bin/generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/generate.py b/bin/generate.py index 15560ad81..8185c513b 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -1082,7 +1082,7 @@ def generate_visualization(): found = file if found is not None: found.rename(in_path.with_suffix(found.suffix)) - bar.log(f"Using {found.name} as testcase visualization") + bar.log(f"Using {found.name} from output_visualizer as test case visualization") if result.status == ExecStatus.TIMEOUT: bar.debug(f"{Style.RESET_ALL}-> {shorten_path(problem, cwd)}") From f3e54fa2dbd12a07ab5323d628c38c0fd305c8bc Mon Sep 17 00:00:00 2001 From: mzuenni Date: Tue, 15 Apr 2025 11:18:35 +0200 Subject: [PATCH 43/53] update version --- skel/problem/generators/generators.yaml | 2 +- support/schemas/generators.cue | 2 +- support/schemas/generators_yaml_schema.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skel/problem/generators/generators.yaml b/skel/problem/generators/generators.yaml index 964c91934..fa2e7209f 100644 --- a/skel/problem/generators/generators.yaml +++ b/skel/problem/generators/generators.yaml @@ -1,5 +1,5 @@ #solution: /submissions/accepted/submission.py -version: 2025-02 # use this version of the generators framework +version: 2025-04 # use this version of the generators framework {%testdata_yaml_comment%}testdata.yaml: # One or more of: diff --git a/support/schemas/generators.cue b/support/schemas/generators.cue index 085d884a8..5dce093ee 100644 --- a/support/schemas/generators.cue +++ b/support/schemas/generators.cue @@ -71,7 +71,7 @@ import "strings" valid_output?: #testgroup }) #testgroup_config - version: =~"^[0-9]{4}-[0-9]{2}$" | *"2025-02" + version: =~"^[0-9]{4}-[0-9]{2}$" | *"2025-04" ... // Do allow unknown_key at top level for tooling } diff --git a/support/schemas/generators_yaml_schema.json b/support/schemas/generators_yaml_schema.json index 6d3a5b09c..54a57b89f 100644 --- a/support/schemas/generators_yaml_schema.json +++ b/support/schemas/generators_yaml_schema.json @@ -273,7 +273,7 @@ } }, "additionalProperties": true, - "description": "Generate test data for this problem. Version 2025-02.", + "description": "Generate test data for this problem. Version 2025-04.", "properties": { "solution": { "$ref": "#/$defs/solution" From 92956c75ce66f16a88d412132ac100a1ada52132 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Tue, 15 Apr 2025 11:20:09 +0200 Subject: [PATCH 44/53] format --- bin/generate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/generate.py b/bin/generate.py index 8185c513b..72fc8b1d5 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -1082,7 +1082,9 @@ def generate_visualization(): found = file if found is not None: found.rename(in_path.with_suffix(found.suffix)) - bar.log(f"Using {found.name} from output_visualizer as test case visualization") + bar.log( + f"Using {found.name} from output_visualizer as test case visualization" + ) if result.status == ExecStatus.TIMEOUT: bar.debug(f"{Style.RESET_ALL}-> {shorten_path(problem, cwd)}") From 06193ac5c76ac2b6a53382c5637b0524685deffb Mon Sep 17 00:00:00 2001 From: mzuenni Date: Tue, 15 Apr 2025 11:20:31 +0200 Subject: [PATCH 45/53] format --- bin/generate.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bin/generate.py b/bin/generate.py index 72fc8b1d5..60ef5ed4a 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -1082,9 +1082,7 @@ def generate_visualization(): found = file if found is not None: found.rename(in_path.with_suffix(found.suffix)) - bar.log( - f"Using {found.name} from output_visualizer as test case visualization" - ) + bar.log(f"Using {found.name} from output_visualizer as visualization") if result.status == ExecStatus.TIMEOUT: bar.debug(f"{Style.RESET_ALL}-> {shorten_path(problem, cwd)}") From 3d28764ce1a96aa3fd08c1976ce29a45db914883 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Tue, 15 Apr 2025 11:31:38 +0200 Subject: [PATCH 46/53] added example --- .../generators/{example.py => example_generator.py} | 0 .../input_visualizer/example_input_visualizer.py | 9 +++++++++ skel/problem/output_visualizer/.gitkeep | 0 .../output_visualizer/example_output_visualizer.py | 10 ++++++++++ 4 files changed, 19 insertions(+) rename skel/problem/generators/{example.py => example_generator.py} (100%) create mode 100644 skel/problem/input_visualizer/example_input_visualizer.py delete mode 100644 skel/problem/output_visualizer/.gitkeep create mode 100644 skel/problem/output_visualizer/example_output_visualizer.py diff --git a/skel/problem/generators/example.py b/skel/problem/generators/example_generator.py similarity index 100% rename from skel/problem/generators/example.py rename to skel/problem/generators/example_generator.py diff --git a/skel/problem/input_visualizer/example_input_visualizer.py b/skel/problem/input_visualizer/example_input_visualizer.py new file mode 100644 index 000000000..df1162d22 --- /dev/null +++ b/skel/problem/input_visualizer/example_input_visualizer.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +import sys + +input_file = open(sys.argv[1]).read().strip() +answer_file = open(sys.argv[2]).read().strip() +args = sys.argv[3:] +with open("testcase.svg", "w") as f: + # this is unsafe since args could contain svg tags + print(f"args: {args}", file=f) diff --git a/skel/problem/output_visualizer/.gitkeep b/skel/problem/output_visualizer/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/skel/problem/output_visualizer/example_output_visualizer.py b/skel/problem/output_visualizer/example_output_visualizer.py new file mode 100644 index 000000000..0d27bcb5e --- /dev/null +++ b/skel/problem/output_visualizer/example_output_visualizer.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +import sys + +input_file = open(sys.argv[1]).read().strip() +answer_file = open(sys.argv[2]).read().strip() +# input yields the team output +args = sys.argv[4:] +with open(f"{sys.argv[3]}/judgeimage.svg", "w") as f: + # this is unsafe since args could contain svg tags + print(f"args: {args}", file=f) From 1284cd3a178d72c268ffb11284933370a307effa Mon Sep 17 00:00:00 2001 From: mzuenni Date: Thu, 17 Apr 2025 03:11:59 +0200 Subject: [PATCH 47/53] update visualizer logic --- bin/generate.py | 88 ++++++++++++++++++++++++++----------------------- bin/problem.py | 2 +- bin/testcase.py | 2 -- 3 files changed, 48 insertions(+), 44 deletions(-) diff --git a/bin/generate.py b/bin/generate.py index 60ef5ed4a..ed723df1a 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -220,8 +220,7 @@ def run(self, bar, cwd): def run_interaction(self, bar, cwd, t): in_path = cwd / "testcase.in" interaction_path = cwd / "testcase.interaction" - if interaction_path.is_file(): - return True + interaction_path.unlink(missing_ok=True) testcase = Testcase(self.problem, in_path, short_path=(t.path.parent / (t.name + ".in"))) assert isinstance(self.program, run.Submission) @@ -689,7 +688,6 @@ def validate_in(t, problem: Problem, testcase: Testcase, meta_yaml: dict, bar: P ) return True - # we assume .ans is a valid output and validate it as such def validate_ans_and_out( t, problem: Problem, testcase: Testcase, meta_yaml: dict, bar: ProgressBar ): @@ -709,18 +707,22 @@ def validate_ans_and_out( bar.error("No .out file was generated!") return False - answer_validator_hashes = {**testcase.validator_hashes(validate.AnswerValidator, bar)} - if all(h in meta_yaml["answer_validator_hashes"] for h in answer_validator_hashes): - return True + ans_out_validator_hashes = testcase.validator_hashes(validate.AnswerValidator, bar).copy() + output_validator_hashes = testcase.validator_hashes(validate.OutputValidator, bar) mode = validate.Mode.ANSWER - if testcase.root in config.INVALID_CASE_DIRECTORIES: + if testcase.root == ["invalid_answer"]: mode = validate.Mode.INVALID - elif testcase.root == "valid_output": + elif testcase.root in ["valid_output", "invalid_output"]: + ans_out_validator_hashes.update(output_validator_hashes) mode = validate.Mode.VALID_OUTPUT elif outfile.is_file(): + ans_out_validator_hashes.update(output_validator_hashes) mode = validate.Mode.VALID_OUTPUT + if all(h in meta_yaml["ans_out_validator_hashes"] for h in ans_out_validator_hashes): + return True + if not testcase.validate_format( mode, bar=bar, @@ -731,8 +733,9 @@ def validate_ans_and_out( bar.done(False) return False else: - for h in answer_validator_hashes: - meta_yaml["answer_validator_hashes"][h] = answer_validator_hashes[h] + for h in ans_out_validator_hashes: + meta_yaml["ans_out_validator_hashes"][h] = ans_out_validator_hashes[h] + meta_yaml["visualizer_hash"] = dict() write_yaml( meta_yaml, problem.tmpdir / "data" / t.hash / "meta_.yaml", @@ -784,7 +787,7 @@ def init_meta(): "generated_extensions": [], "input_validator_hashes": dict(), "solution_hash": dict(), - "answer_validator_hashes": dict(), + "ans_out_validator_hashes": dict(), "visualizer_hash": dict(), } meta_yaml["rule"] = t.rule @@ -868,7 +871,7 @@ def generate_from_rule(): if not infile.is_file() or meta_yaml.get("rule_hashes") != rule_hashes: # clear all generated files - shutil.rmtree(cwd) + shutil.rmtree(cwd, ignore_errors=True) cwd.mkdir(parents=True, exist_ok=True) meta_yaml = init_meta() @@ -921,7 +924,7 @@ def generate_from_rule(): assert t._has_required_in(infile), f"Failed to generate in file: {infile.name}" return True - def generate_from_solution(): + def generate_from_solution(testcase: Testcase): nonlocal meta_yaml if testcase.root in [*config.INVALID_CASE_DIRECTORIES, "valid_output"]: @@ -986,7 +989,7 @@ def needed(ext): if used_solution: meta_yaml["solution_hash"] = solution_hash if changed_ans: - meta_yaml["answer_validator_hashes"] = dict() + meta_yaml["ans_out_validator_hashes"] = dict() meta_yaml["visualizer_hash"] = dict() if changed_ans or used_solution: write_yaml(meta_yaml, meta_path, allow_yamllib=True) @@ -994,24 +997,10 @@ def needed(ext): assert ansfile.is_file(), f"Failed to generate ans file: {ansfile}" return True - def generate_empty_interactive_sample_ans(): - if not t.sample: - return True - if not problem.interactive and not problem.multi_pass: - return True - for ext in ["", ".statement", ".download"]: - ans_ext_file = infile.with_suffix(f".ans{ext}") - if ans_ext_file.exists(): - return True - if infile.with_suffix(f".in{ext}").exists(): - ans_ext_file.write_text("") - return True - return True - - def generate_visualization(): + def generate_visualization(testcase: Testcase, bar: ProgressBar): nonlocal meta_yaml - if testcase.root in [*config.INVALID_CASE_DIRECTORIES, "valid_output"]: + if testcase.root in config.INVALID_CASE_DIRECTORIES: return True if config.args.no_visualizer: return True @@ -1028,18 +1017,18 @@ def generate_visualization(): ) output_visualizer = problem.visualizer(visualize.OutputVisualizer) if output_visualizer is not None: - if out_path.is_file() or problem.settings.ans_is_output or problem.interactive: + if out_path.is_file() or problem.settings.ans_is_output: if visualizer is None or out_path.is_file(): visualizer = output_visualizer - - if not out_path.is_file() and problem.settings.ans_is_output: + if not out_path.is_file(): + assert problem.settings.ans_is_output out_path = ans_path if visualizer is None: + # copy potential teamimage/judgeimage from output validator? return True - visualizer_args = testcase.testdata_yaml_args(visualizer, PrintBar()) - + visualizer_args = testcase.testdata_yaml_args(visualizer, bar) visualizer_hash = { "visualizer_hash": visualizer.hash, "visualizer_args": visualizer_args, @@ -1055,9 +1044,11 @@ def generate_visualization(): result = visualizer.run(in_path, ans_path, cwd) else: feedbackdir = in_path.with_suffix(".feedbackdir") - feedbackdir.mkdir(parents=True, exist_ok=True) - teamimage = feedbackdir / "teamimage" - judgeimage = feedbackdir / "judgeimage" + feedbackcopy = in_path.with_suffix(".feedbackcopy") + shutil.rmtree(feedbackcopy) + shutil.copytree(feedbackdir, feedbackcopy) + teamimage = feedbackcopy / "teamimage" + judgeimage = feedbackcopy / "judgeimage" for ext in config.KNOWN_VISUALIZER_EXTENSIONS: teamimage.with_suffix(ext).unlink(True) @@ -1067,7 +1058,7 @@ def generate_visualization(): in_path, ans_path, out_path if not problem.interactive else None, - feedbackdir, + feedbackcopy, visualizer_args, ) if result.status: @@ -1083,6 +1074,7 @@ def generate_visualization(): if found is not None: found.rename(in_path.with_suffix(found.suffix)) bar.log(f"Using {found.name} from output_visualizer as visualization") + shutil.rmtree(feedbackcopy) if result.status == ExecStatus.TIMEOUT: bar.debug(f"{Style.RESET_ALL}-> {shorten_path(problem, cwd)}") @@ -1105,6 +1097,20 @@ def generate_visualization(): # errors in the visualizer are not critical return True + def generate_empty_interactive_sample_ans(): + if not t.sample: + return True + if not problem.interactive and not problem.multi_pass: + return True + for ext in ["", ".statement", ".download"]: + ans_ext_file = infile.with_suffix(f".ans{ext}") + if ans_ext_file.exists(): + return True + if infile.with_suffix(f".in{ext}").exists(): + ans_ext_file.write_text("") + return True + return True + def copy_generated(): identical_exts = set() @@ -1206,7 +1212,7 @@ def add_testdata_to_cache(): return # Step 4: generate .ans and .interaction if needed - if not generate_from_solution(): + if not generate_from_solution(testcase): return # Step 5: validate .ans (and .out if it exists) @@ -1214,7 +1220,7 @@ def add_testdata_to_cache(): return # Step 6: generate visualization if needed - if not generate_visualization(): + if not generate_visualization(testcase, bar): return # Step 7: for interactive and/or multi-pass samples, generate empty .ans if it does not exist diff --git a/bin/problem.py b/bin/problem.py index 1a2a38107..b57c311aa 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -627,7 +627,7 @@ def testcases( testcases = [] for f in in_paths: - t = testcase.Testcase(p, f, print_warn=True) + t = testcase.Testcase(p, f) if ( (p.interactive or p.multi_pass) and mode in [validate.Mode.INVALID, validate.Mode.VALID_OUTPUT] diff --git a/bin/testcase.py b/bin/testcase.py index 3d5e7982f..26f061566 100644 --- a/bin/testcase.py +++ b/bin/testcase.py @@ -70,7 +70,6 @@ def __init__( path: Path, *, short_path: Optional[Path] = None, - print_warn: bool = False, ): """ Arguments @@ -149,7 +148,6 @@ def validator_hashes( hash => - name - flags - - hash indicating which validators will be run for this testcase. """ assert cls in [validate.InputValidator, validate.AnswerValidator, validate.OutputValidator] From 07cd74da2c562ad98328ec617d71b5c95c8344a2 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Thu, 17 Apr 2025 11:15:25 +0200 Subject: [PATCH 48/53] fix mode --- bin/generate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/generate.py b/bin/generate.py index ed723df1a..ffd8bd27c 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -713,10 +713,10 @@ def validate_ans_and_out( mode = validate.Mode.ANSWER if testcase.root == ["invalid_answer"]: mode = validate.Mode.INVALID - elif testcase.root in ["valid_output", "invalid_output"]: + elif testcase.root == "invalid_output": ans_out_validator_hashes.update(output_validator_hashes) - mode = validate.Mode.VALID_OUTPUT - elif outfile.is_file(): + mode = validate.Mode.INVALID + elif testcase.root == "valid_output" or outfile.is_file(): ans_out_validator_hashes.update(output_validator_hashes) mode = validate.Mode.VALID_OUTPUT From 34b82ce0aee1cbc023b58285b3b0cd54f6d016a8 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Thu, 17 Apr 2025 11:30:59 +0200 Subject: [PATCH 49/53] fix mode --- bin/generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/generate.py b/bin/generate.py index ffd8bd27c..f24f46cec 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -711,7 +711,7 @@ def validate_ans_and_out( output_validator_hashes = testcase.validator_hashes(validate.OutputValidator, bar) mode = validate.Mode.ANSWER - if testcase.root == ["invalid_answer"]: + if testcase.root == "invalid_answer": mode = validate.Mode.INVALID elif testcase.root == "invalid_output": ans_out_validator_hashes.update(output_validator_hashes) From fd94e89eb7a9b902a8d7149d78aa1558dfeb3a58 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 19 Apr 2025 21:14:23 +0200 Subject: [PATCH 50/53] simplify code --- bin/generate.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/bin/generate.py b/bin/generate.py index f24f46cec..8ccb94e62 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -1012,6 +1012,19 @@ def generate_visualization(testcase: Testcase, bar: ProgressBar): assert in_path.is_file() assert ans_path.is_file() + feedbackdir = in_path.with_suffix(".feedbackdir") + image_files = [f"judgeimage{ext}" for ext in config.KNOWN_VISUALIZER_EXTENSIONS] + [ + f"teamimage{ext}" for ext in config.KNOWN_VISUALIZER_EXTENSIONS + ] + + def use_feedback_image(feedbackdir: Path, source: str) -> None: + for name in image_files: + path = feedbackdir / name + if path.exists(): + ensure_symlink(in_path.with_suffix(path.suffix), path) + bar.log(f"Using {name} from {source} as visualization") + return + visualizer: Optional[visualize.AnyVisualizer] = problem.visualizer( visualize.InputVisualizer ) @@ -1025,7 +1038,9 @@ def generate_visualization(testcase: Testcase, bar: ProgressBar): out_path = ans_path if visualizer is None: - # copy potential teamimage/judgeimage from output validator? + for ext in config.KNOWN_VISUALIZER_EXTENSIONS: + in_path.with_suffix(ext).unlink(True) + use_feedback_image(feedbackdir, "validator") return True visualizer_args = testcase.testdata_yaml_args(visualizer, bar) @@ -1043,16 +1058,13 @@ def generate_visualization(testcase: Testcase, bar: ProgressBar): if isinstance(visualizer, visualize.InputVisualizer): result = visualizer.run(in_path, ans_path, cwd) else: - feedbackdir = in_path.with_suffix(".feedbackdir") feedbackcopy = in_path.with_suffix(".feedbackcopy") shutil.rmtree(feedbackcopy) - shutil.copytree(feedbackdir, feedbackcopy) - teamimage = feedbackcopy / "teamimage" - judgeimage = feedbackcopy / "judgeimage" - for ext in config.KNOWN_VISUALIZER_EXTENSIONS: - teamimage.with_suffix(ext).unlink(True) - judgeimage.with_suffix(ext).unlink(True) + def skip_images(src: str, content: list[str]) -> list[str]: + return [] if src != str(feedbackdir) else image_files + + shutil.copytree(feedbackdir, feedbackcopy, ignore=skip_images) result = visualizer.run( in_path, @@ -1062,19 +1074,7 @@ def generate_visualization(testcase: Testcase, bar: ProgressBar): visualizer_args, ) if result.status: - found = None - for ext in config.KNOWN_VISUALIZER_EXTENSIONS: - file = teamimage.with_suffix(ext) - if file.is_file(): - found = file - for ext in config.KNOWN_VISUALIZER_EXTENSIONS: - file = judgeimage.with_suffix(ext) - if file.is_file(): - found = file - if found is not None: - found.rename(in_path.with_suffix(found.suffix)) - bar.log(f"Using {found.name} from output_visualizer as visualization") - shutil.rmtree(feedbackcopy) + use_feedback_image(feedbackdir, "output_visualizer") if result.status == ExecStatus.TIMEOUT: bar.debug(f"{Style.RESET_ALL}-> {shorten_path(problem, cwd)}") From 2c168ef0ef0014e96b205bcfc3bf45cf0363688f Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 20 Apr 2025 01:53:33 +0200 Subject: [PATCH 51/53] add warning for deprecated root key --- bin/generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/generate.py b/bin/generate.py index 8ccb94e62..c9edc98e1 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -318,7 +318,7 @@ def __init__(self, generator_config): ] RESERVED_DIRECTORY_KEYS: Final[Sequence[str]] = ["command"] KNOWN_ROOT_KEYS: Final[Sequence[str]] = ["generators", "parallel", "version"] -DEPRECATED_ROOT_KEYS: Final[Sequence[str]] = ["gitignore_generated"] +DEPRECATED_ROOT_KEYS: Final[Sequence[str]] = ["gitignore_generated", "visualizer"] # Holds all inheritable configuration options. Currently: From fab7cfd364aa32ab5f7f57d47aa754ad3df31547 Mon Sep 17 00:00:00 2001 From: Maarten Sijm <9739541+mpsijm@users.noreply.github.com> Date: Sun, 20 Apr 2025 23:41:21 +0200 Subject: [PATCH 52/53] Problem._parse_testdata_yaml: enforce validator/visualizer args to be lists of strings --- bin/generate.py | 2 +- bin/problem.py | 39 ++++++++++++------- bin/upgrade.py | 5 ++- doc/generators.yaml | 10 ++--- support/schemas/generators_yaml_schema.json | 37 +++++++++++------- support/schemas/problemformat.cue | 6 ++- .../divsort/generators/generators.yaml | 20 +++++----- test/problems/fltcmp/data/testdata.yaml | 2 +- .../generators/generators.yaml | 6 +-- .../identity/data/sample/testdata.yaml | 2 +- .../identity/generators/generators.yaml | 2 +- .../invalid_yaml/invalid.generators.yaml | 2 +- .../valid_yaml/rich-generators.yaml | 2 +- 13 files changed, 80 insertions(+), 55 deletions(-) diff --git a/bin/generate.py b/bin/generate.py index c9edc98e1..e11769d30 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -1056,7 +1056,7 @@ def use_feedback_image(feedbackdir: Path, source: str) -> None: in_path.with_suffix(ext).unlink(True) if isinstance(visualizer, visualize.InputVisualizer): - result = visualizer.run(in_path, ans_path, cwd) + result = visualizer.run(in_path, ans_path, cwd, visualizer_args) else: feedbackcopy = in_path.with_suffix(".feedbackcopy") shutil.rmtree(feedbackcopy) diff --git a/bin/problem.py b/bin/problem.py index b57c311aa..d9250ecfb 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -475,12 +475,16 @@ def _parse_testdata_yaml(p, path, bar): | visualize.InputVisualizer.args_key | visualize.OutputVisualizer.args_key ): - if not isinstance(flags[k], str): - bar.error(f"{k} must be string", resume=True, print_item=False) + if not isinstance(flags[k], list): + bar.error( + f"{k} must be a list of strings", + resume=True, + print_item=False, + ) case validate.InputValidator.args_key: - if not isinstance(flags[k], (str, dict)): + if not isinstance(flags[k], (list, dict)): bar.error( - f"{k} must be string or map", + f"{k} must be list or map", resume=True, print_item=False, ) @@ -564,19 +568,28 @@ def get_testdata_yaml( continue flags = p._testdata_yamls[f] if key in flags: + args = flags[key] if key == validate.InputValidator.args_key: - if not isinstance(flags[key], (str, dict)): - bar.error(f"{key} must be string or map") + if not isinstance(args, (list, dict)): + bar.error(f"{key} must be list of strings or map of lists") return [] - if isinstance(flags[key], str): - return flags[key].split() - elif name in flags[key]: - return flags[key][name].split() + if isinstance(args, list): + if any(not isinstance(arg, str) for arg in args): + bar.error(f"{key} must be list of strings or map of lists") + return [] + return args + elif name in args: + if not isinstance(args[name], list) or any( + not isinstance(arg, str) for arg in args[name] + ): + bar.error(f"{key} must be list of strings or map of lists") + return [] + return args[name] elif key in known_args_keys: - if not isinstance(flags[key], str): - bar.error(f"{key} must be string") + if not isinstance(args, list) or any(not isinstance(arg, str) for arg in args): + bar.error(f"{key} must be a list of strings") return [] - return flags[key].split() + return args return [] diff --git a/bin/upgrade.py b/bin/upgrade.py index 3803bac2a..0ad0e648b 100644 --- a/bin/upgrade.py +++ b/bin/upgrade.py @@ -411,7 +411,10 @@ def add_args(new_data: dict[str, Any]) -> bool: ) return False bar.log(f"change 'validator_flags' to '{OutputValidator.args_key}' in testdata.yaml") - new_data[OutputValidator.args_key] = data["validator_flags"] + validator_flags = data["validator_flags"] + new_data[OutputValidator.args_key] = ( + validator_flags.split() if isinstance(validator_flags, str) else validator_flags + ) ryaml_filter(data, "validator_flags") return True diff --git a/doc/generators.yaml b/doc/generators.yaml index 4d3f101cb..fb78d06d0 100644 --- a/doc/generators.yaml +++ b/doc/generators.yaml @@ -17,7 +17,7 @@ random_salt: abcd # The top level may contain a testdata.yaml that will be written to data/ as specified. testdata.yaml: - output_validator_args: "" + output_validator_args: [] # We support three types of generators: # - Standalone files, like generators/a.cpp, generators/b.py, ..., which will @@ -105,7 +105,7 @@ data: secret: include: # You can include other testcroups by there yaml name - - 'sample' + - "sample" # This will include "1", "2", "3", "4", and "5" from sample data: # Types of generator programs. @@ -136,8 +136,8 @@ data: 11-random-3: graph seed={seed:2} # Different seed, because command isn't the same. #11-random-4: graph {seed} {seed:2} # Not allowed because the regex matches twice. 12-counted: - generate: graph {seed:3} {count} - count: 2 # generate two testcases at once + generate: graph {seed:3} {count} + count: 2 # generate two testcases at once # No key (testcase or testgroup) may be a prefix of another key. #01-second: graph 6 # Collision with rule 01 above. @@ -177,7 +177,7 @@ data: hard_cases_group: # Directories may contain a testdata.yaml that will be written as specified. testdata.yaml: - output_validator_args: space_change_sensitive + output_validator_args: [space_change_sensitive] # To enable automatic numbering of testcases, data: may also contain a list of # single-element dictionaries instead of a single dictionary. In this case, diff --git a/support/schemas/generators_yaml_schema.json b/support/schemas/generators_yaml_schema.json index 54a57b89f..9a4cfc4a8 100644 --- a/support/schemas/generators_yaml_schema.json +++ b/support/schemas/generators_yaml_schema.json @@ -64,9 +64,6 @@ }, "input_validator_args": { "oneOf": [ - { - "type": "string" - }, { "type": "array", "items": { @@ -77,7 +74,10 @@ "type": "object", "patternProperties": { "^([A-Za-z0-9][A-Za-z0-9_-]*[A-Za-z0-9]|[A-Za-z0-9])$":{ - "type": "string" + "type": "array", + "items": { + "type": "string" + } } } } @@ -85,19 +85,26 @@ "description": "Defines arguments passed to each input validator for the test case/group. If a sequence of strings, then those are the arguments that will be passed to each input validator for this the case/group. If a map, then each key is the name of the input validator and the value is the arguments to pass to that input validator for the test case/group. Validators not present in the map are run without any arguments." }, "output_validator_args": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ], + "type": "array", + "items": { + "type": "string" + }, "description": "Defines arguments passed to the output validator for the test case/group." }, + "input_visualizer_args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Defines arguments passed to the input visualizer for the test case/group." + }, + "output_visualizer_args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Defines arguments passed to the output visualizer for the test case/group." + }, "input_validator_flags": { "type": "string", "deprecated": true, diff --git a/support/schemas/problemformat.cue b/support/schemas/problemformat.cue index e063dd16a..6387adba6 100644 --- a/support/schemas/problemformat.cue +++ b/support/schemas/problemformat.cue @@ -23,8 +23,10 @@ let filename = "[A-Za-z0-9][A-Za-z0-9_.-]{0,253}[A-Za-z0-9]" // Test data settings #testdata_settings: { - input_validator_args?: *"" | string | {[string]: string} - output_validator_args?: *"" | string + input_validator_args?: *[] | [string] | {[string]: [string]} + output_validator_args?: *[] | [string] + input_visualizer_args?: *[] | [string] + output_visualizer_args?: *[] | [string] grading?: { score?: >0 max_score?: >0 diff --git a/test/problems/divsort/generators/generators.yaml b/test/problems/divsort/generators/generators.yaml index 152252732..dd1d0f5b1 100644 --- a/test/problems/divsort/generators/generators.yaml +++ b/test/problems/divsort/generators/generators.yaml @@ -8,11 +8,11 @@ data: secret: testdata.yaml: input_validator_args: - integers: small + integers: [small] data: integers: testdata.yaml: - input_validator_args: --integer + input_validator_args: [--integer] #grading: foo data: - unsorted-integer: @@ -21,7 +21,7 @@ data: sorted: testdata.yaml: input_validator_args: - strings: --sorted + strings: [--sorted] data: - sorted-integer: in: 10.0 1.0 ab cd @@ -30,14 +30,14 @@ data: data: nested_1: testdata.yaml: - input_validator_args: --small + input_validator_args: [--small] data: small_floats: in: 10 3.5 ab cd nested_2: testdata.yaml: input_validator_args: - integers: "" # hides the input_validator_args in secret/testdata.yaml + integers: [] # hides the input_validator_args in secret/testdata.yaml data: - tiny_floats: in: 10.0 3.5 ab dc @@ -46,7 +46,7 @@ data: desc: Must validate, because `secret/testdata.yaml` hidden by `secret/general/nested_2/testdata.yaml` tolerant: testdata.yaml: - output_validator_args: float_tolerance 1e-2 + output_validator_args: [float_tolerance, "1e-2"] data: - tiny_floats: in: 10.0 3.0 ab dc @@ -62,14 +62,14 @@ data: too_many_tokens: {in: 10.0 2.5 ab cd ef} integers: testdata.yaml: - input_validator_args: --integer + input_validator_args: [--integer] data: ints_expected: {in: 10.0 2.5 ab cd} include: - small_floats sorted: testdata.yaml: - input_validator_args: --sorted + input_validator_args: [--sorted] include: - unsorted # invalid here because of --sorted flag (valid input in invalid_answers/no_output_validator_args) invalid_answer: @@ -84,7 +84,7 @@ data: ans: 5.0 Abccd with_output_validator_args: testdata.yaml: - output_validator_args: --forbid_abcd + output_validator_args: [--forbid_abcd] include: - imprecise # must reject because its ans includes abcd invalid_output: @@ -95,7 +95,7 @@ data: out: 3.33 abcd valid_output: testdata.yaml: - output_validator_args: float_tolerance 1e-2 + output_validator_args: [float_tolerance, "1e-2"] data: valid: in: 10.0 3.0 ab cd diff --git a/test/problems/fltcmp/data/testdata.yaml b/test/problems/fltcmp/data/testdata.yaml index af7ce1f19..ded323389 100644 --- a/test/problems/fltcmp/data/testdata.yaml +++ b/test/problems/fltcmp/data/testdata.yaml @@ -1 +1 @@ -output_validator_args: float_tolerance 1E-6 +output_validator_args: [float_tolerance, "1E-6"] diff --git a/test/problems/generatorincludes/generators/generators.yaml b/test/problems/generatorincludes/generators/generators.yaml index c82e8b97e..139deb187 100644 --- a/test/problems/generatorincludes/generators/generators.yaml +++ b/test/problems/generatorincludes/generators/generators.yaml @@ -11,10 +11,10 @@ data: data: - small: testdata.yaml: - output_validator_args: space_change_sensitive + output_validator_args: [space_change_sensitive] input_validator_args: - connected: --small - strongly-connected: --small + connected: [--small] + strongly-connected: [--small] data: - positive: data: diff --git a/test/problems/identity/data/sample/testdata.yaml b/test/problems/identity/data/sample/testdata.yaml index 81bc18252..cb6f96a79 100644 --- a/test/problems/identity/data/sample/testdata.yaml +++ b/test/problems/identity/data/sample/testdata.yaml @@ -1 +1 @@ -output_visualizer_args: --draw-please +output_visualizer_args: [--draw-please] diff --git a/test/problems/identity/generators/generators.yaml b/test/problems/identity/generators/generators.yaml index 0152709fb..e6090d53c 100644 --- a/test/problems/identity/generators/generators.yaml +++ b/test/problems/identity/generators/generators.yaml @@ -73,7 +73,7 @@ data: in.statement: "6" ans.statement: "6" testdata.yaml: - output_visualizer_args: --draw-please + output_visualizer_args: [--draw-please] secret: data: diff --git a/test/yaml/generators/invalid_yaml/invalid.generators.yaml b/test/yaml/generators/invalid_yaml/invalid.generators.yaml index 4204d23d0..d4618cc0c 100644 --- a/test/yaml/generators/invalid_yaml/invalid.generators.yaml +++ b/test/yaml/generators/invalid_yaml/invalid.generators.yaml @@ -390,4 +390,4 @@ data: - '': in: '1 2' testdata.yaml: # this is not ok - input_validator_args: "connected" + input_validator_args: [connected] diff --git a/test/yaml/generators/valid_yaml/rich-generators.yaml b/test/yaml/generators/valid_yaml/rich-generators.yaml index 8341f8d09..4e32ed274 100644 --- a/test/yaml/generators/valid_yaml/rich-generators.yaml +++ b/test/yaml/generators/valid_yaml/rich-generators.yaml @@ -29,7 +29,7 @@ data: count: 5 'group_with_testdata': testdata.yaml: - input_validator_args: "--connected --max_n 2000" + input_validator_args: [--connected, --max_n, "2000"] data: 'a': my_generator invalid_input: From 0d2333f428725007f1b292932c9759167bd8f690 Mon Sep 17 00:00:00 2001 From: Maarten Sijm <9739541+mpsijm@users.noreply.github.com> Date: Mon, 21 Apr 2025 00:00:09 +0200 Subject: [PATCH 53/53] [visualize] Rename InputVisualizer to TestCaseVisualizer --- bin/export.py | 6 +++--- bin/generate.py | 6 +++--- bin/problem.py | 8 ++++---- bin/upgrade.py | 2 +- bin/visualize.py | 18 ++++++++++-------- skel/problem/input_visualizer/readme.md | 2 -- .../example_test_case_visualizer.py} | 0 skel/problem/test_case_visualizer/readme.md | 2 ++ support/schemas/generators_yaml_schema.json | 6 +++--- support/schemas/problemformat.cue | 2 +- .../run | 0 .../visualize.asy | 0 12 files changed, 27 insertions(+), 25 deletions(-) delete mode 100644 skel/problem/input_visualizer/readme.md rename skel/problem/{input_visualizer/example_input_visualizer.py => test_case_visualizer/example_test_case_visualizer.py} (100%) create mode 100644 skel/problem/test_case_visualizer/readme.md rename test/problems/identity/{input_visualizer_disabled => test_case_visualizer_disabled}/run (100%) rename test/problems/identity/{input_visualizer_disabled => test_case_visualizer_disabled}/visualize.asy (100%) diff --git a/bin/export.py b/bin/export.py index 6ae8bf056..219c01a36 100644 --- a/bin/export.py +++ b/bin/export.py @@ -13,7 +13,7 @@ from latex import PdfType from problem import Problem from validate import InputValidator, AnswerValidator, OutputValidator -from visualize import InputVisualizer, OutputVisualizer +from visualize import TestCaseVisualizer, OutputVisualizer def select_languages(problems: list[Problem]) -> list[str]: @@ -126,7 +126,7 @@ def build_problem_zip(problem: Problem, output: Path) -> bool: ("submissions/accepted/**/*", True), ("submissions/*/**/*", False), ("attachments/**/*", problem.interactive or problem.multi_pass), - (f"{InputVisualizer.source_dir}/**/*", False), + (f"{TestCaseVisualizer.source_dir}/**/*", False), (f"{OutputVisualizer.source_dir}/**/*", False), ] @@ -215,7 +215,7 @@ def add_testcase(in_file: Path) -> None: f"{OutputValidator.source_dir}/**/*", # "statement/*", "solution/*", "problem_slide/*", use \constant{} commands # "submissions/*/**/*", removed support? - f"{InputVisualizer.source_dir}/**/*", + f"{TestCaseVisualizer.source_dir}/**/*", f"{OutputVisualizer.source_dir}/**/*", ] for pattern in constants_supported: diff --git a/bin/generate.py b/bin/generate.py index e11769d30..e1d5465d1 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -1026,7 +1026,7 @@ def use_feedback_image(feedbackdir: Path, source: str) -> None: return visualizer: Optional[visualize.AnyVisualizer] = problem.visualizer( - visualize.InputVisualizer + visualize.TestCaseVisualizer ) output_visualizer = problem.visualizer(visualize.OutputVisualizer) if output_visualizer is not None: @@ -1055,7 +1055,7 @@ def use_feedback_image(feedbackdir: Path, source: str) -> None: for ext in config.KNOWN_VISUALIZER_EXTENSIONS: in_path.with_suffix(ext).unlink(True) - if isinstance(visualizer, visualize.InputVisualizer): + if isinstance(visualizer, visualize.TestCaseVisualizer): result = visualizer.run(in_path, ans_path, cwd, visualizer_args) else: feedbackcopy = in_path.with_suffix(".feedbackcopy") @@ -1933,7 +1933,7 @@ def build_program(p): build_programs(program.Generator, generators_used) build_programs(run.Submission, solutions_used) if build_visualizers: - self.problem.visualizer(visualize.InputVisualizer) + self.problem.visualizer(visualize.TestCaseVisualizer) self.problem.visualizer(visualize.OutputVisualizer) self.problem.validators(validate.InputValidator) diff --git a/bin/problem.py b/bin/problem.py index d9250ecfb..1b98b0343 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -472,7 +472,7 @@ def _parse_testdata_yaml(p, path, bar): case ( validate.OutputValidator.args_key | validate.AnswerValidator.args_key - | visualize.InputVisualizer.args_key + | visualize.TestCaseVisualizer.args_key | visualize.OutputVisualizer.args_key ): if not isinstance(flags[k], list): @@ -544,7 +544,7 @@ def get_testdata_yaml( validate.InputValidator.args_key, validate.OutputValidator.args_key, validate.AnswerValidator.args_key, - visualize.InputVisualizer.args_key, + visualize.TestCaseVisualizer.args_key, visualize.OutputVisualizer.args_key, ] if key not in known_args_keys: @@ -888,8 +888,8 @@ def build_program(p): @overload def visualizer( - problem, cls: type[visualize.InputVisualizer] - ) -> Optional[visualize.InputVisualizer]: ... + problem, cls: type[visualize.TestCaseVisualizer] + ) -> Optional[visualize.TestCaseVisualizer]: ... @overload def visualizer( problem, cls: type[visualize.OutputVisualizer] diff --git a/bin/upgrade.py b/bin/upgrade.py index 0ad0e648b..cbf2a5413 100644 --- a/bin/upgrade.py +++ b/bin/upgrade.py @@ -151,7 +151,7 @@ def upgrade_generators_yaml(problem_path: Path, bar: ProgressBar) -> None: if "visualizer" in yaml_data: warn( - "Cannot automatically upgrade 'visualizer'.\n - move visualizer to 'input_visualizer/'\n - first argument is the in_file\n - second argument is the ans_file" + "Cannot automatically upgrade 'visualizer'.\n - move visualizer to 'test_case_visualizer/'\n - first argument is the in_file\n - second argument is the ans_file" ) if "data" in yaml_data and isinstance(yaml_data["data"], dict): diff --git a/bin/visualize.py b/bin/visualize.py index 3a6c20a56..074248cfd 100644 --- a/bin/visualize.py +++ b/bin/visualize.py @@ -9,25 +9,25 @@ from problem import Problem -class InputVisualizer(program.Program): +class TestCaseVisualizer(program.Program): """ - Visualizes a testcase, called as: + Visualizes a test case, called as: ./visualizer input answer [args] """ - visualizer_type: Final[str] = "input" + visualizer_type: Final[str] = "test case" - source_dir: Final[str] = "input_visualizer" + source_dir: Final[str] = "test_case_visualizer" - args_key: Final[str] = "input_visualizer_args" + args_key: Final[str] = "test_case_visualizer_args" def __init__(self, problem: "Problem", path: Path, **kwargs: Any): super().__init__( problem, path, - InputVisualizer.source_dir, + TestCaseVisualizer.source_dir, limits={"timeout": problem.limits.visualizer_time}, substitute_constants=True, **kwargs, @@ -37,7 +37,9 @@ def __init__(self, problem: "Problem", path: Path, **kwargs: Any): def run( self, in_path: Path, ans_path: Path, cwd: Path, args: Optional[list[str]] = None ) -> ExecResult: - assert self.run_command is not None, "Input Visualizer should be built before running it" + assert self.run_command is not None, ( + "Test Case Visualizer should be built before running it" + ) return self._exec_command( self.run_command + [in_path, ans_path] + (args or []), @@ -92,4 +94,4 @@ def run( return self._exec_command(command, cwd=cwd) -AnyVisualizer = InputVisualizer | OutputVisualizer +AnyVisualizer = TestCaseVisualizer | OutputVisualizer diff --git a/skel/problem/input_visualizer/readme.md b/skel/problem/input_visualizer/readme.md deleted file mode 100644 index 0ef426286..000000000 --- a/skel/problem/input_visualizer/readme.md +++ /dev/null @@ -1,2 +0,0 @@ -This input visualizer is intended for use with BAPCtools' `bt generate`. -The visualizer should be invoked as `./visualizer <...input_visualizer_args>` and should write a `testcase.` file. diff --git a/skel/problem/input_visualizer/example_input_visualizer.py b/skel/problem/test_case_visualizer/example_test_case_visualizer.py similarity index 100% rename from skel/problem/input_visualizer/example_input_visualizer.py rename to skel/problem/test_case_visualizer/example_test_case_visualizer.py diff --git a/skel/problem/test_case_visualizer/readme.md b/skel/problem/test_case_visualizer/readme.md new file mode 100644 index 000000000..88516a9c0 --- /dev/null +++ b/skel/problem/test_case_visualizer/readme.md @@ -0,0 +1,2 @@ +This test case visualizer is intended for use with BAPCtools' `bt generate`. +The visualizer should be invoked as `./visualizer <...test_case_visualizer_args>` and should write a `testcase.` file. diff --git a/support/schemas/generators_yaml_schema.json b/support/schemas/generators_yaml_schema.json index 9a4cfc4a8..3377488a3 100644 --- a/support/schemas/generators_yaml_schema.json +++ b/support/schemas/generators_yaml_schema.json @@ -91,12 +91,12 @@ }, "description": "Defines arguments passed to the output validator for the test case/group." }, - "input_visualizer_args": { + "test_case_visualizer_args": { "type": "array", "items": { "type": "string" }, - "description": "Defines arguments passed to the input visualizer for the test case/group." + "description": "Defines arguments passed to the test case visualizer for the test case/group." }, "output_visualizer_args": { "type": "array", @@ -113,7 +113,7 @@ "output_validator_flags": { "type": "string", "deprecated": true, - "description": "With 'problem_format_version: 2023-07-draft' in problem.yaml, use input_validator_args instead." + "description": "With 'problem_format_version: 2023-07-draft' in problem.yaml, use output_validator_args instead." }, "accept_score": { "type": "string" diff --git a/support/schemas/problemformat.cue b/support/schemas/problemformat.cue index 6387adba6..8f5fca3d5 100644 --- a/support/schemas/problemformat.cue +++ b/support/schemas/problemformat.cue @@ -25,7 +25,7 @@ let filename = "[A-Za-z0-9][A-Za-z0-9_.-]{0,253}[A-Za-z0-9]" #testdata_settings: { input_validator_args?: *[] | [string] | {[string]: [string]} output_validator_args?: *[] | [string] - input_visualizer_args?: *[] | [string] + test_case_visualizer_args?: *[] | [string] output_visualizer_args?: *[] | [string] grading?: { score?: >0 diff --git a/test/problems/identity/input_visualizer_disabled/run b/test/problems/identity/test_case_visualizer_disabled/run similarity index 100% rename from test/problems/identity/input_visualizer_disabled/run rename to test/problems/identity/test_case_visualizer_disabled/run diff --git a/test/problems/identity/input_visualizer_disabled/visualize.asy b/test/problems/identity/test_case_visualizer_disabled/visualize.asy similarity index 100% rename from test/problems/identity/input_visualizer_disabled/visualize.asy rename to test/problems/identity/test_case_visualizer_disabled/visualize.asy