From 79923754678a489b37095810e40569980237b80a Mon Sep 17 00:00:00 2001 From: mzuenni Date: Wed, 5 Mar 2025 22:58:39 +0100 Subject: [PATCH 01/10] use output_validator --- bin/export.py | 10 ++++++++-- bin/generate.py | 5 +---- bin/interactive.py | 2 +- bin/problem.py | 26 ++++++++++---------------- bin/run.py | 22 +++++++++------------- bin/skel.py | 2 +- bin/stats.py | 2 +- bin/testcase.py | 2 +- bin/upgrade.py | 19 ++++++++++++++++++- bin/validate.py | 5 ++--- 10 files changed, 52 insertions(+), 43 deletions(-) diff --git a/bin/export.py b/bin/export.py index 5b25b75a6..6abfacb77 100644 --- a/bin/export.py +++ b/bin/export.py @@ -121,7 +121,7 @@ def build_problem_zip(problem: Problem, output: Path): ] if problem.custom_output: - files.append(("output_validators/**/*", True)) + files.append(("output_validator/**/*", True)) if config.args.kattis: files.append(("input_validators/**/*", True)) @@ -224,7 +224,7 @@ def add_file(path, source): if problem.settings.constants: constants_supported = [ "data/**/testdata.yaml", - "output_validators/**/*", + "output_validator/**/*", "input_validators/**/*", # "problem_statement/*", uses \constants # "submissions/*/**/*", removed support? @@ -242,6 +242,12 @@ def add_file(path, source): f.unlink() f.write_text(text) + # TODO: Remove this if we know others use the output_validator dir + if (export_dir / "output_validator").exists(): + (export_dir / "output_validator").rename( + export_dir / "output_validators" / "output_validator" + ) + # Build .ZIP file. message("writing zip file", "Zip", output, color_type=MessageType.LOG) try: diff --git a/bin/generate.py b/bin/generate.py index a472c1d69..42ffa1e03 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -729,10 +729,7 @@ def validate_ans(t, problem: Problem, testcase: Testcase, meta_yaml: dict, bar: f".ans file is {size / 1024 / 1024:.3f}MiB, which is close to output limit (set limits.output to at least {(2 * size + 1024 * 1024 - 1) // 1024 // 1024}MiB in problem.yaml)" ) - answer_validator_hashes = { - **testcase.validator_hashes(validate.AnswerValidator, bar), - **testcase.validator_hashes(validate.OutputValidator, bar), - } + 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 diff --git a/bin/interactive.py b/bin/interactive.py index 83390c365..1180a1c2c 100644 --- a/bin/interactive.py +++ b/bin/interactive.py @@ -32,7 +32,7 @@ def run_interactive_testcase( bar: Optional[ProgressBar] = None, ): output_validators = run.problem.validators(validate.OutputValidator) - if len(output_validators) != 1: + if not output_validators: return None output_validator = output_validators[0] diff --git a/bin/problem.py b/bin/problem.py index de94046e2..203a80515 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -224,7 +224,7 @@ def __init__( self.interactive: bool = "interactive" in mode self.multi_pass: bool = "multi-pass" in mode self.custom_output: bool = ( - self.interactive or self.multi_pass or (problem.path / "output_validators").exists() + self.interactive or self.multi_pass or (problem.path / "output_validator").is_dir() ) self.name: dict[str, str] = parse_setting(yaml_data, "name", {"en": ""}) @@ -798,8 +798,6 @@ def validators( warn("No input validators found.") case validate.AnswerValidator, 0: warn("No answer validators found") - case validate.OutputValidator, l if l != 1: - error(f"Found {len(validators)} output validators, expected exactly one.") build_ok = all(v.ok for v in validators) @@ -815,15 +813,16 @@ def _validators( return problem._validators_cache[key] assert hasattr(cls, "source_dirs") - # TODO #424: We should not support multiple output validators inside output_validator/. - paths = [p for source_dir in cls.source_dirs for p in glob(problem.path / source_dir, "*")] - - # Handle default output validation if cls == validate.OutputValidator: - if not paths: - if problem.custom_output: - fatal("Problem validation type requires output_validators/") + assert len(cls.source_dirs) == 1 + if problem.custom_output: + paths = [problem.path / cls.source_dirs[0]] + else: paths = [config.TOOLS_ROOT / "support" / "default_output_validator.cpp"] + else: + paths = [ + p for source_dir in cls.source_dirs for p in glob(problem.path / source_dir, "*") + ] # TODO: Instead of checking file contents, maybe specify this in generators.yaml? def has_constraints_checking(f): @@ -875,13 +874,8 @@ def prepare_run(problem): if not testcases: return False - if problem.interactive or problem.multi_pass: - validators = problem.validators(validate.OutputValidator) - if not validators: - return False - # Pre build the output validator to prevent nested ProgressBars. - if problem.validators(validate.OutputValidator) is False: + if not problem.validators(validate.OutputValidator): return False submissions = problem.submissions() diff --git a/bin/run.py b/bin/run.py index 87d29b88d..308b6d670 100644 --- a/bin/run.py +++ b/bin/run.py @@ -217,12 +217,12 @@ def _prepare_nextpass(self, nextpass): def _validate_output(self, bar): output_validators = self.problem.validators(validate.OutputValidator) - if len(output_validators) != 1: + if not output_validators: return None - validator = output_validators[0] - - return validator.run( - self.testcase, self, args=self.testcase.testdata_yaml_validator_args(validator, bar) + return output_validators[0].run( + self.testcase, + self, + args=self.testcase.testdata_yaml_validator_args(output_validators[0], bar), ) @@ -522,10 +522,8 @@ def test(self): testcases = self.problem.testcases(needans=False) - if self.problem.interactive: - output_validators = self.problem.validators(validate.OutputValidator) - if output_validators is False: - return + if not self.problem.validators(validate.OutputValidator): + return for testcase in testcases: header = ProgressBar.action("Running " + str(self.name), testcase.name) @@ -589,10 +587,8 @@ def test(self): # Run the submission using stdin as input. def test_interactive(self): - if self.problem.interactive: - output_validators = self.problem.validators(validate.OutputValidator) - if output_validators is False: - return + if not self.problem.validators(validate.OutputValidator): + return bar = ProgressBar("Running " + str(self.name), max_len=1, count=1) bar.start() diff --git a/bin/skel.py b/bin/skel.py index 20feaab90..59de1ce74 100644 --- a/bin/skel.py +++ b/bin/skel.py @@ -242,7 +242,7 @@ def new_problem(): variables, exist_ok=True, preserve_symlinks=preserve_symlinks, - skip=[skeldir / "output_validators"] if not custom_output else None, + skip=[skeldir / "output_validator"] if not custom_output else None, ) # Warn about missing problem statement skeletons for non-en languages diff --git a/bin/stats.py b/bin/stats.py index 69eb8a638..e2f4d96f5 100644 --- a/bin/stats.py +++ b/bin/stats.py @@ -52,7 +52,7 @@ def problem_stats(problems): ("sol", "problem_statement/solution*.tex", 1), (" val: I", ["input_validators/*", "input_format_validators/*"]), ("A", ["answer_validators/*"]), - ("O", ["output_validators/*"]), + ("O", ["output_validator/"]), ( " sample", [lambda s: {x.stem for x in s if x.parts[2] == "sample"}], diff --git a/bin/testcase.py b/bin/testcase.py index d4bdb6255..d580bfe76 100644 --- a/bin/testcase.py +++ b/bin/testcase.py @@ -150,7 +150,7 @@ def validator_hashes(self, cls: type["validate.AnyValidator"], bar): indicating which validators will be run for this testcase. """ assert cls in [validate.InputValidator, validate.AnswerValidator, validate.OutputValidator] - validators = self.problem.validators(cls) or [] + validators = self.problem.validators(cls) d = dict() diff --git a/bin/upgrade.py b/bin/upgrade.py index 15c3b72ba..6798544f8 100644 --- a/bin/upgrade.py +++ b/bin/upgrade.py @@ -188,6 +188,23 @@ def upgrade_statement(problem_path: Path, bar: ProgressBar) -> None: shutil.move(f, dest) +def upgrade_output_validators(problem_path: Path, bar: ProgressBar) -> None: + if (problem_path / "output_validators").is_dir(): + if (problem_path / "output_validator").exists(): + bar.error( + "can't rename 'output_validators/', 'output_validator/' already exists", resume=True + ) + return + content = [*(problem_path / "output_validators").iterdir()] + if len(content) == 1 and content[0].is_dir(): + bar.log(f"renaming 'output_validators/{content[0].name}' to 'output_validator/'") + content[0].rename(problem_path / "output_validator") + (problem_path / "output_validators").rmdir() + else: + bar.log("renaming 'output_validators/' to 'output_validator/'") + (problem_path / "output_validators").rename(problem_path / "output_validator") + + def upgrade_problem_yaml(problem_path: Path, bar: ProgressBar) -> None: assert (problem_path / "problem.yaml").exists() data = cast(CommentedMap, read_yaml(problem_path / "problem.yaml")) @@ -364,7 +381,7 @@ def _upgrade(problem_path: Path, bar: ProgressBar) -> None: upgrade_testdata_yaml(problem_path, bar) upgrade_generators_yaml(problem_path, bar) # upgrade_statement(problem_path, bar) TODO: activate this when we support the new statement dirs - # TODO: output_validators -> output_validator + upgrade_output_validators(problem_path, bar) upgrade_problem_yaml(problem_path, bar) bar.done() diff --git a/bin/validate.py b/bin/validate.py index 527fc5ad6..1b4094925 100644 --- a/bin/validate.py +++ b/bin/validate.py @@ -333,12 +333,11 @@ class OutputValidator(Validator): """ def __init__(self, problem, path, **kwargs): - super().__init__(problem, path, "output_validators", **kwargs) + super().__init__(problem, path, "output_validator", **kwargs) validator_type = "output" - # TODO #424: We should not support multiple output validators inside output_validator/. - source_dirs = ["output_validator", "output_validators"] + source_dirs = ["output_validator"] def run( self, From 0e07aae0c3150e40d752713401d63a5bc8b80b27 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Wed, 5 Mar 2025 22:59:19 +0100 Subject: [PATCH 02/10] upgraded tests --- .../boolfind_run => output_validator}/build | 0 .../boolfind_run => output_validator}/run | 0 .../runjury_boolfind.c | 0 .../output_validator/output_validator.cpp | 0 .../output_validator/validation.h | 0 .../validate.cc | 0 .../validate.h | 0 .../guess_validator => output_validator}/validate.cc | 0 .../guess_validator => output_validator}/validate.h | 0 .../guess_validator => output_validator}/validate.cc | 0 .../guess_validator => output_validator}/validate.h | 0 .../interctive_multipass_validator.py | 0 .../multipass_validator.py | 0 .../{output_validators => output_validator}/.gitkeep | 0 test/test_default_output_validator.py | 10 +++++----- 15 files changed, 5 insertions(+), 5 deletions(-) rename test/problems/boolfind/{output_validators/boolfind_run => output_validator}/build (100%) rename test/problems/boolfind/{output_validators/boolfind_run => output_validator}/run (100%) rename test/problems/boolfind/{output_validators/boolfind_run => output_validator}/runjury_boolfind.c (100%) rename test/problems/constants/{output_validators => }/output_validator/output_validator.cpp (100%) rename test/problems/constants/{output_validators => }/output_validator/validation.h (100%) rename test/problems/different/{output_validators/different_validator => output_validator}/validate.cc (100%) rename test/problems/different/{output_validators/different_validator => output_validator}/validate.h (100%) rename test/problems/guess/{output_validators/guess_validator => output_validator}/validate.cc (100%) rename test/problems/guess/{output_validators/guess_validator => output_validator}/validate.h (100%) rename test/problems/guessnoeofcheck/{output_validators/guess_validator => output_validator}/validate.cc (100%) rename test/problems/guessnoeofcheck/{output_validators/guess_validator => output_validator}/validate.h (100%) rename test/problems/interactivemultipass/{output_validators/interactive_multipass_validator => output_validator}/interctive_multipass_validator.py (100%) rename test/problems/multipass/{output_validators/multipass_validator => output_validator}/multipass_validator.py (100%) rename test/problems/testproblemconfig/{output_validators => output_validator}/.gitkeep (100%) diff --git a/test/problems/boolfind/output_validators/boolfind_run/build b/test/problems/boolfind/output_validator/build similarity index 100% rename from test/problems/boolfind/output_validators/boolfind_run/build rename to test/problems/boolfind/output_validator/build diff --git a/test/problems/boolfind/output_validators/boolfind_run/run b/test/problems/boolfind/output_validator/run similarity index 100% rename from test/problems/boolfind/output_validators/boolfind_run/run rename to test/problems/boolfind/output_validator/run diff --git a/test/problems/boolfind/output_validators/boolfind_run/runjury_boolfind.c b/test/problems/boolfind/output_validator/runjury_boolfind.c similarity index 100% rename from test/problems/boolfind/output_validators/boolfind_run/runjury_boolfind.c rename to test/problems/boolfind/output_validator/runjury_boolfind.c diff --git a/test/problems/constants/output_validators/output_validator/output_validator.cpp b/test/problems/constants/output_validator/output_validator.cpp similarity index 100% rename from test/problems/constants/output_validators/output_validator/output_validator.cpp rename to test/problems/constants/output_validator/output_validator.cpp diff --git a/test/problems/constants/output_validators/output_validator/validation.h b/test/problems/constants/output_validator/validation.h similarity index 100% rename from test/problems/constants/output_validators/output_validator/validation.h rename to test/problems/constants/output_validator/validation.h diff --git a/test/problems/different/output_validators/different_validator/validate.cc b/test/problems/different/output_validator/validate.cc similarity index 100% rename from test/problems/different/output_validators/different_validator/validate.cc rename to test/problems/different/output_validator/validate.cc diff --git a/test/problems/different/output_validators/different_validator/validate.h b/test/problems/different/output_validator/validate.h similarity index 100% rename from test/problems/different/output_validators/different_validator/validate.h rename to test/problems/different/output_validator/validate.h diff --git a/test/problems/guess/output_validators/guess_validator/validate.cc b/test/problems/guess/output_validator/validate.cc similarity index 100% rename from test/problems/guess/output_validators/guess_validator/validate.cc rename to test/problems/guess/output_validator/validate.cc diff --git a/test/problems/guess/output_validators/guess_validator/validate.h b/test/problems/guess/output_validator/validate.h similarity index 100% rename from test/problems/guess/output_validators/guess_validator/validate.h rename to test/problems/guess/output_validator/validate.h diff --git a/test/problems/guessnoeofcheck/output_validators/guess_validator/validate.cc b/test/problems/guessnoeofcheck/output_validator/validate.cc similarity index 100% rename from test/problems/guessnoeofcheck/output_validators/guess_validator/validate.cc rename to test/problems/guessnoeofcheck/output_validator/validate.cc diff --git a/test/problems/guessnoeofcheck/output_validators/guess_validator/validate.h b/test/problems/guessnoeofcheck/output_validator/validate.h similarity index 100% rename from test/problems/guessnoeofcheck/output_validators/guess_validator/validate.h rename to test/problems/guessnoeofcheck/output_validator/validate.h diff --git a/test/problems/interactivemultipass/output_validators/interactive_multipass_validator/interctive_multipass_validator.py b/test/problems/interactivemultipass/output_validator/interctive_multipass_validator.py similarity index 100% rename from test/problems/interactivemultipass/output_validators/interactive_multipass_validator/interctive_multipass_validator.py rename to test/problems/interactivemultipass/output_validator/interctive_multipass_validator.py diff --git a/test/problems/multipass/output_validators/multipass_validator/multipass_validator.py b/test/problems/multipass/output_validator/multipass_validator.py similarity index 100% rename from test/problems/multipass/output_validators/multipass_validator/multipass_validator.py rename to test/problems/multipass/output_validator/multipass_validator.py diff --git a/test/problems/testproblemconfig/output_validators/.gitkeep b/test/problems/testproblemconfig/output_validator/.gitkeep similarity index 100% rename from test/problems/testproblemconfig/output_validators/.gitkeep rename to test/problems/testproblemconfig/output_validator/.gitkeep diff --git a/test/test_default_output_validator.py b/test/test_default_output_validator.py index 9c7286726..220b12d9f 100644 --- a/test/test_default_output_validator.py +++ b/test/test_default_output_validator.py @@ -13,7 +13,7 @@ RUN_DIR = Path.cwd().resolve() # Note: the python version isn't tested by default, because it's quite slow. -DEFAULT_OUTPUT_VALIDATORS = ["default_output_validator.cpp"] +DEFAULT_OUTPUT_VALIDATOR = ["default_output_validator.cpp"] config.args.verbose = 2 config.args.error = True @@ -42,7 +42,7 @@ def read_tests(): return tests -@pytest.fixture(scope="class", params=DEFAULT_OUTPUT_VALIDATORS) +@pytest.fixture(scope="class", params=DEFAULT_OUTPUT_VALIDATOR) def validator(request): problem_dir = RUN_DIR / "test/problems/identity" os.chdir(problem_dir) @@ -65,9 +65,9 @@ class MockRun: @pytest.mark.usefixtures("validator") -class TestDefaultOutputValidators: +class TestDefaultOutputValidator: @pytest.mark.parametrize("testdata", read_tests()) - def test_default_output_validators(self, validator, testdata): + def test_default_output_validator(self, validator, testdata): problem, validator = validator flags, ans, out, exp = testdata flags = flags.split() @@ -87,7 +87,7 @@ def test_default_output_validators(self, validator, testdata): r.out_path = out_path r.feedbackdir = problem.tmpdir / "data" - # TODO: the validators should probably be able to figure the flags out from the Problem config + # TODO: the validator should probably be able to figure the flags out from the Problem config result = validator.run(t, r, args=flags) if result.status != exp: From 4a85c453a7cc4ae9776af6ab9ef3d47a28d31d64 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Wed, 5 Mar 2025 22:59:33 +0100 Subject: [PATCH 03/10] updated skel --- .../output_validator/output_validator.cpp | 0 skel/problem/output_validator/validation.h | 1 + skel/problem/output_validators/output_validator/validation.h | 1 - 3 files changed, 1 insertion(+), 1 deletion(-) rename skel/problem/{output_validators => }/output_validator/output_validator.cpp (100%) create mode 120000 skel/problem/output_validator/validation.h delete mode 120000 skel/problem/output_validators/output_validator/validation.h diff --git a/skel/problem/output_validators/output_validator/output_validator.cpp b/skel/problem/output_validator/output_validator.cpp similarity index 100% rename from skel/problem/output_validators/output_validator/output_validator.cpp rename to skel/problem/output_validator/output_validator.cpp diff --git a/skel/problem/output_validator/validation.h b/skel/problem/output_validator/validation.h new file mode 120000 index 000000000..8394e5a44 --- /dev/null +++ b/skel/problem/output_validator/validation.h @@ -0,0 +1 @@ +../../../headers/validation.h \ No newline at end of file diff --git a/skel/problem/output_validators/output_validator/validation.h b/skel/problem/output_validators/output_validator/validation.h deleted file mode 120000 index 9a928c978..000000000 --- a/skel/problem/output_validators/output_validator/validation.h +++ /dev/null @@ -1 +0,0 @@ -../../../../headers/validation.h \ No newline at end of file From 6cb4db5b67b6e2184bffe3249079f1b9bd73c136 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Wed, 5 Mar 2025 23:22:52 +0100 Subject: [PATCH 04/10] fix symlink --- test/problems/constants/output_validator/validation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/problems/constants/output_validator/validation.h b/test/problems/constants/output_validator/validation.h index 2b74c5d6a..9a928c978 120000 --- a/test/problems/constants/output_validator/validation.h +++ b/test/problems/constants/output_validator/validation.h @@ -1 +1 @@ -../../../../../headers/validation.h \ No newline at end of file +../../../../headers/validation.h \ No newline at end of file From 97a1a34d2abd755fedeb3c3ec38449d563a9ed96 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Wed, 5 Mar 2025 23:30:59 +0100 Subject: [PATCH 05/10] create output_validators dir --- bin/export.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/export.py b/bin/export.py index 6abfacb77..74d547dc9 100644 --- a/bin/export.py +++ b/bin/export.py @@ -244,6 +244,7 @@ def add_file(path, source): # TODO: Remove this if we know others use the output_validator dir if (export_dir / "output_validator").exists(): + (export_dir / "output_validators").mkdir(parents=True) (export_dir / "output_validator").rename( export_dir / "output_validators" / "output_validator" ) From fb56fdef1e602b1039c339754e885e29efc05de6 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 8 Mar 2025 14:46:16 +0100 Subject: [PATCH 06/10] changed class constants --- bin/problem.py | 5 ++--- bin/validate.py | 12 ++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/bin/problem.py b/bin/problem.py index a5fa48f3e..b2019a9f9 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -815,14 +815,13 @@ def _validators( if key in problem._validators_cache: return problem._validators_cache[key] - assert hasattr(cls, "source_dirs") if cls == validate.OutputValidator: - assert len(cls.source_dirs) == 1 if problem.custom_output: - paths = [problem.path / cls.source_dirs[0]] + paths = [problem.path / validate.OutputValidator.source_dirs] else: paths = [config.TOOLS_ROOT / "support" / "default_output_validator.cpp"] else: + assert hasattr(cls, "source_dirs") paths = [ p for source_dir in cls.source_dirs for p in glob(problem.path / source_dir, "*") ] diff --git a/bin/validate.py b/bin/validate.py index 1b4094925..375252ed6 100644 --- a/bin/validate.py +++ b/bin/validate.py @@ -224,9 +224,9 @@ class InputValidator(Validator): def __init__(self, problem, path, **kwargs): super().__init__(problem, path, "input_validators", **kwargs) - validator_type = "input" + validator_type: Final[str] = "input" - source_dirs = ["input_validators", "input_format_validators"] + source_dir: Final[list[str]] = ["input_validators", "input_format_validators"] def run( self, @@ -284,9 +284,9 @@ class AnswerValidator(Validator): def __init__(self, problem, path, **kwargs): super().__init__(problem, path, "answer_validators", **kwargs) - validator_type = "answer" + validator_type: Final[str] = "answer" - source_dirs = ["answer_validators", "answer_format_validators"] + source_dirs: Final[list[str]] = ["answer_validators", "answer_format_validators"] def run( self, @@ -335,9 +335,9 @@ class OutputValidator(Validator): def __init__(self, problem, path, **kwargs): super().__init__(problem, path, "output_validator", **kwargs) - validator_type = "output" + validator_type: Final[str] = "output" - source_dirs = ["output_validator"] + source_dirs: Final[str] = "output_validator" def run( self, From b4b6d38ba1426a1cab4ea15bc724266ecbcd8fd5 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 8 Mar 2025 14:57:46 +0100 Subject: [PATCH 07/10] fix source_dir(s) --- bin/problem.py | 2 +- bin/validate.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/problem.py b/bin/problem.py index b2019a9f9..9820dcfb0 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -817,7 +817,7 @@ def _validators( if cls == validate.OutputValidator: if problem.custom_output: - paths = [problem.path / validate.OutputValidator.source_dirs] + paths = [problem.path / validate.OutputValidator.source_dir] else: paths = [config.TOOLS_ROOT / "support" / "default_output_validator.cpp"] else: diff --git a/bin/validate.py b/bin/validate.py index 375252ed6..3c8d4867d 100644 --- a/bin/validate.py +++ b/bin/validate.py @@ -226,7 +226,7 @@ def __init__(self, problem, path, **kwargs): validator_type: Final[str] = "input" - source_dir: Final[list[str]] = ["input_validators", "input_format_validators"] + source_dirs: Final[list[str]] = ["input_validators", "input_format_validators"] def run( self, @@ -337,7 +337,7 @@ def __init__(self, problem, path, **kwargs): validator_type: Final[str] = "output" - source_dirs: Final[str] = "output_validator" + source_dir: Final[str] = "output_validator" def run( self, From 1b77bcbf747a108131e973240d37e80f42b3386a Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 8 Mar 2025 17:59:09 +0100 Subject: [PATCH 08/10] copy symlinks --- bin/upgrade.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/bin/upgrade.py b/bin/upgrade.py index 547a06a9e..45485f0f5 100644 --- a/bin/upgrade.py +++ b/bin/upgrade.py @@ -198,8 +198,29 @@ def upgrade_output_validators(problem_path: Path, bar: ProgressBar) -> None: content = [*(problem_path / "output_validators").iterdir()] if len(content) == 1 and content[0].is_dir(): bar.log(f"renaming 'output_validators/{content[0].name}' to 'output_validator/'") - content[0].rename(problem_path / "output_validator") - (problem_path / "output_validators").rmdir() + + 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 / "output_validator", copy_function=move) + shutil.rmtree(problem_path / "output_validators") else: bar.log("renaming 'output_validators/' to 'output_validator/'") (problem_path / "output_validators").rename(problem_path / "output_validator") From e8399c0ef21550b66cfa61e5e2319f69b97629bd Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 8 Mar 2025 23:37:40 +0100 Subject: [PATCH 09/10] export legacy --- bin/export.py | 91 +++++++++++++++++++++++++++++++++++--------------- bin/upgrade.py | 69 +++++++++----------------------------- bin/util.py | 37 ++++++++++++++++++++ 3 files changed, 117 insertions(+), 80 deletions(-) diff --git a/bin/export.py b/bin/export.py index 1e577bc13..86d81a647 100644 --- a/bin/export.py +++ b/bin/export.py @@ -92,6 +92,10 @@ def build_samples_zip(problems, output, statement_language): def build_problem_zip(problem: Problem, output: Path): """Make DOMjudge/Kattis ZIP file for specified problem.""" + if not has_ryaml: + error("zip needs the ruamel.yaml python3 library. Install python[3]-ruamel.yaml.") + return + # Add problem PDF for only one language to the zip file (note that Kattis export does not include PDF) statement_language = None if config.args.kattis else force_single_language([problem]) @@ -170,33 +174,64 @@ def add_file(path, source): out = f2.relative_to(problem.path) add_file(out, f2) - # DOMjudge does not support 'type' in problem.yaml nor 'output_validator_args' in testdata.yaml yet. - # TODO: Remove this once it does. - if not config.args.kattis: - yaml_path = export_dir / "problem.yaml" - yaml_data = [yaml_path.read_text(), "\nvalidation:"] - if problem.custom_output: - yaml_data.append(" custom") - if problem.interactive: - yaml_data.append(" interactive") - if problem.multi_pass: - yaml_data.append(" multi-pass") - else: - yaml_data.append(" default") - yaml_data.append("\n") - - validator_flags = " ".join( - problem.get_testdata_yaml( - problem.path / "data", - "output_validator_args", - PrintBar("Getting validator_flags for legacy DOMjudge export"), - ) + # DOMjudge and Kattis do not support draft yet. + # TODO: Remove they do. + from ruamel.yaml.comments import CommentedMap + + yaml_path = export_dir / "problem.yaml" + yaml_data = read_yaml(yaml_path) + # drop format version -> legacy + if "problem_format_version" in yaml_data: + ryaml_filter(yaml_data, "problem_format_version") + # type -> validation + if "type" in yaml_data: + ryaml_filter(yaml_data, "type") + validation = [] + if problem.custom_output: + validation.append("custom") + if problem.interactive: + validation.append("interactive") + if problem.multi_pass: + validation.append("multi-pass") + else: + validation.append("default") + yaml_data["validation"] = " ".join(validation) + # credits -> author + if "credits" in yaml_data: + ryaml_filter(yaml_data, "credits") + if problem.settings.credits.authors: + yaml_data["author"] = ", ".join(p.name for p in problem.settings.credits.authors) + # change source: + if problem.settings.source: + if len(problem.settings.source) > 1: + util.warn(f"Found multiple sources, using '{problem.settings.source[0].name}'.") + yaml_data["source"] = problem.settings.source[0].name + yaml_data["source_url"] = problem.settings.source[0].url + # limits.time_multipliers -> time_multiplier / time_safety_margin + if "limits" not in yaml_data or not yaml_data["limits"]: + yaml_data["limits"] = CommentedMap() + limits = yaml_data["limits"] + if "time_multipliers" in limits: + ryaml_filter(limits, "time_multipliers") + limits["time_multiplier"] = problem.limits.ac_to_time_limit + limits["time_safety_margin"] = problem.limits.time_limit_to_tle + # drop explicit timelimit for kattis: + if "time_limit" in limits: + # keep this for kattis even when "time_limit" is supported + ryaml_filter(limits, "time_limit") + # validator_flags + validator_flags = " ".join( + problem.get_testdata_yaml( + problem.path / "data", + "output_validator_args", + PrintBar("Getting validator_flags for legacy export"), ) - if validator_flags: - yaml_data.append(f"validator_flags: {validator_flags}\n") - - yaml_path.unlink() - yaml_path.write_text("".join(yaml_data)) + ) + if validator_flags: + yaml_data["validator_flags"] = validator_flags + # write legacy style yaml + yaml_path.unlink() + write_yaml(yaml_data, yaml_path) # DOMjudge does not support 'limits.time_limit' in problem.yaml yet. # TODO: Remove this once it does. @@ -293,6 +328,10 @@ def add_file(path, source): # solutions*.{lang}.pdf # Output is def build_contest_zip(problems, zipfiles, outfile, statement_language): + if not has_ryaml: + error("zip needs the ruamel.yaml python3 library. Install python[3]-ruamel.yaml.") + return + print(f"writing ZIP file {outfile}", file=sys.stderr) if not config.args.kattis: # Kattis does not use problems.yaml. diff --git a/bin/upgrade.py b/bin/upgrade.py index 45485f0f5..ee959e7bc 100644 --- a/bin/upgrade.py +++ b/bin/upgrade.py @@ -10,45 +10,6 @@ from ruamel.yaml.comments import CommentedMap, CommentedSeq -# This tries to preserve the correct comments. -def _filter(data: Any, remove: str) -> Any: - assert isinstance(data, CommentedMap) - - remove_index = list(data.keys()).index(remove) - if remove_index == 0: - return data.pop(remove) - - curr = data - prev_key = list(data.keys())[remove_index - 1] - while isinstance(curr[prev_key], list | dict): - # Try to remove the comment from the last element in the preceding list/dict - curr = curr[prev_key] - if isinstance(curr, list): - prev_key = len(curr) - 1 - else: - prev_key = list(curr.keys())[-1] - - if remove in data.ca.items: - # Move the comment that belongs to the removed key (which comes _after_ the removed key) - # to the preceding key - curr.ca.items[prev_key] = data.ca.items.pop(remove) - elif prev_key in data.ca.items: - # If the removed key does not have a comment, - # the comment after the previous key should be removed - curr.ca.items.pop(prev_key) - - return data.pop(remove) - - -# Insert a new key before an old key, then remove the old key. -# If new_value is not given, the default is to simply rename the old key to the new key. -def _replace(data: Any, old_key: str, new_key: str, new_value: Any = None) -> None: - if new_value is None: - new_value = data[old_key] - data.insert(list(data.keys()).index(old_key), new_key, new_value) - _filter(data, old_key) - - def upgrade_data(problem_path: Path, bar: ProgressBar) -> None: rename = [ ("data/invalid_inputs", "data/invalid_input"), @@ -85,7 +46,7 @@ def upgrade_testdata_yaml(problem_path: Path, bar: ProgressBar) -> None: resume=True, ) continue - _replace(data, old, new) + ryaml_replace(data, old, new) write_yaml(data, f) @@ -115,7 +76,7 @@ def upgrade_generators_yaml(problem_path: Path, bar: ProgressBar) -> None: ) continue bar.log(f"renaming 'data.{old_name}' to 'data.{new_name}' in generators.yaml") - _replace(data, old_name, new_name) + ryaml_replace(data, old_name, new_name) changed = True def upgrade_generated_testdata_yaml(data: dict[str, Any], path: str) -> bool: @@ -138,7 +99,7 @@ def upgrade_generated_testdata_yaml(data: dict[str, Any], path: str) -> bool: ) continue bar.log(f"change '{old}' to '{new}' in generators.yaml{print_path}") - _replace(testdata, old, new) + ryaml_replace(testdata, old, new) changed = True if "data" in data and data["data"]: children = data["data"] if isinstance(data["data"], list) else [data["data"]] @@ -256,7 +217,7 @@ def upgrade_problem_yaml(problem_path: Path, bar: ProgressBar) -> None: # "type" comes before "name" in the spec pos = list(data.keys()).index("name") if "name" in data else 0 data.insert(pos, "type", type if len(type) > 1 else type[0]) - _filter(data, "validation") + ryaml_filter(data, "validation") if "author" in data: if "credits" in data: @@ -269,23 +230,23 @@ def upgrade_problem_yaml(problem_path: Path, bar: ProgressBar) -> None: name.strip() for name in data["author"].replace("and", ",").split(",") ) credits = CommentedMap({"authors": authors if len(authors) > 1 else authors[0]}) - _replace(data, "author", "credits", credits) + ryaml_replace(data, "author", "credits", credits) if "source_url" in data: if "source" not in data: - _replace(data, "source_url", "source") + ryaml_replace(data, "source_url", "source") elif data["source"]: bar.log("change 'source_url' to 'source.url' in problem.yaml") old_pos = list(data.keys()).index("source") - old_source = _filter(data, "source") - old_source_url = _filter(data, "source_url") + old_source = ryaml_filter(data, "source") + old_source_url = ryaml_filter(data, "source_url") data.insert( old_pos, "source", CommentedMap({"name": old_source, "url": old_source_url}) ) else: bar.log("remove empty 'source(_url)' in problem.yaml") - _filter(data, "source") - _filter(data, "source_url") + ryaml_filter(data, "source") + ryaml_filter(data, "source_url") if "limits" in data: limits = data["limits"] @@ -304,19 +265,19 @@ def upgrade_problem_yaml(problem_path: Path, bar: ProgressBar) -> None: if "time_multiplier" in limits: if limits["time_multiplier"] != 2: # Skip if it's equal to the new default time_multipliers["ac_to_time_limit"] = limits["time_multiplier"] - _filter(limits, "time_multiplier") + ryaml_filter(limits, "time_multiplier") if "time_safety_margin" in limits: if limits["time_safety_margin"] != 1.5: # Skip if it's equal to the new default time_multipliers["time_limit_to_tle"] = limits["time_safety_margin"] - _filter(limits, "time_safety_margin") + ryaml_filter(limits, "time_safety_margin") if time_multipliers: limits["time_multipliers"] = time_multipliers # If both time multipliers are default, remove the comments (this only works if # there are no other limits configured, but that's the most common case anyway) if not limits: - _filter(data, "limits") + ryaml_filter(data, "limits") def add_args(new_data: dict[str, Any]) -> bool: if "output_validator_args" in new_data: @@ -327,7 +288,7 @@ def add_args(new_data: dict[str, Any]) -> bool: return False bar.log("change 'validator_flags' to 'output_validator_args' in testdata.yaml") new_data["output_validator_args"] = data["validator_flags"] - _filter(data, "validator_flags") + ryaml_filter(data, "validator_flags") return True if "validator_flags" in data: @@ -359,7 +320,7 @@ def add_args(new_data: dict[str, Any]) -> bool: if add_args(testdata_data): write_yaml(testdata_data, testdata_path) else: - _filter(data, "validator_flags") + ryaml_filter(data, "validator_flags") timelimit_path = problem_path / ".timelimit" if timelimit_path.is_file(): diff --git a/bin/util.py b/bin/util.py index 0ff476f2b..b4b570ddf 100644 --- a/bin/util.py +++ b/bin/util.py @@ -713,6 +713,43 @@ def ryaml_get_or_add( assert isinstance(value, t) return value # type: ignore + # This tries to preserve the correct comments. + def ryaml_filter(data: Any, remove: str) -> Any: + assert isinstance(data, ruamel.yaml.comments.CommentedMap) + remove_index = list(data.keys()).index(remove) + if remove_index == 0: + return data.pop(remove) + + curr = data + prev_key = list(data.keys())[remove_index - 1] + while isinstance(curr[prev_key], list | dict): + # Try to remove the comment from the last element in the preceding list/dict + curr = curr[prev_key] + if isinstance(curr, list): + prev_key = len(curr) - 1 + else: + prev_key = list(curr.keys())[-1] + + if remove in data.ca.items: + # Move the comment that belongs to the removed key (which comes _after_ the removed key) + # to the preceding key + curr.ca.items[prev_key] = data.ca.items.pop(remove) + elif prev_key in data.ca.items: + # If the removed key does not have a comment, + # the comment after the previous key should be removed + curr.ca.items.pop(prev_key) + + return data.pop(remove) + + # Insert a new key before an old key, then remove the old key. + # If new_value is not given, the default is to simply rename the old key to the new key. + def ryaml_replace(data: Any, old_key: str, new_key: str, new_value: Any = None) -> None: + assert isinstance(data, ruamel.yaml.comments.CommentedMap) + if new_value is None: + new_value = data[old_key] + data.insert(list(data.keys()).index(old_key), new_key, new_value) + ryaml_filter(data, old_key) + # Only allow one thread to write at the same time. Else, e.g., generating test cases in parallel goes wrong. write_yaml_lock = threading.Lock() From 18417fea1e4692345afdef514e3c442fa9456413 Mon Sep 17 00:00:00 2001 From: Maarten Sijm <9739541+mpsijm@users.noreply.github.com> Date: Sun, 9 Mar 2025 14:09:00 +0100 Subject: [PATCH 10/10] [export] Fix typo in comment --- bin/export.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/export.py b/bin/export.py index 86d81a647..f5d5e6028 100644 --- a/bin/export.py +++ b/bin/export.py @@ -174,8 +174,8 @@ def add_file(path, source): out = f2.relative_to(problem.path) add_file(out, f2) - # DOMjudge and Kattis do not support draft yet. - # TODO: Remove they do. + # DOMjudge and Kattis do not support 2023-07-draft yet. + # TODO: Remove once they do. from ruamel.yaml.comments import CommentedMap yaml_path = export_dir / "problem.yaml"