Skip to content

Commit 3a6e84e

Browse files
authored
Force the use of output_validator_args in testdata.yaml, instead of validator_flags in problem.yaml (#428)
* Rename {in,out}put_validator_{flags => args}, only allow it in testdata.yaml (no longer in problem.yaml) * [generate] Add "version" to KNOWN_ROOT_KEYS * support/schemas/generators_yaml_schema.json: Grammar: space between "test{case,data,group}" * [testcase] Remove some unused `args` parameters in validation methods * [skel] Move validator_flags from problem.yaml to output_validator_flags in testdata.yaml in generators.yaml * [problem] Make sure that only .get_testdata_yaml is used, instead of .validator_flags * [interactive][problem] Fix typos * [export] Set validator_flags for legacy DOMjudge zip format * [test] Replace {in,out}put_validator_{flags => args} * Remove outdated generators_yaml_schema_0_9.json It's not referenced anywhere, and if we ever want to support version-switching to outdated schemata, we can get it back from the Git history. * [test] Remove unnecessary quotes from generators.yamls * [export] Fix backslash in f-string
1 parent c317642 commit 3a6e84e

File tree

22 files changed

+300
-454
lines changed

22 files changed

+300
-454
lines changed

bin/export.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import Optional
1010

1111
from contest import *
12+
from problem import Problem
1213

1314

1415
# Replace \problemname{...} by the value of `name:` in problems.yaml in all .tex files.
@@ -113,7 +114,7 @@ def build_samples_zip(problems, output, statement_language):
113114
print("Wrote zip to samples.zip", file=sys.stderr)
114115

115116

116-
def build_problem_zip(problem, output):
117+
def build_problem_zip(problem: Problem, output: Path):
117118
"""Make DOMjudge ZIP file for specified problem."""
118119

119120
# Add problem PDF for only one language to the zip file (note that Kattis export does not include PDF)
@@ -157,10 +158,19 @@ def build_problem_zip(problem, output):
157158

158159
print("Preparing to make ZIP file for problem dir %s" % problem.path, file=sys.stderr)
159160

160-
# DOMjudge does not support 'type' in problem.yaml yet.
161+
# DOMjudge does not support 'type' in problem.yaml nor 'output_validator_args' in testdata.yaml yet.
161162
# TODO: Remove this once it does.
162163
problem_yaml_str = (problem.path / "problem.yaml").read_text()
163164
if not config.args.kattis and not problem.settings.is_legacy():
165+
validator_flags = " ".join(
166+
problem.get_testdata_yaml(
167+
problem.path / "data",
168+
"output_validator_args",
169+
PrintBar("Getting validator_flags for legacy DOMjudge export"),
170+
)
171+
)
172+
if validator_flags:
173+
validator_flags = "validator_flags: " + validator_flags + "\n"
164174
write_file_strs.append(
165175
(
166176
"problem.yaml",
@@ -172,7 +182,7 @@ def build_problem_zip(problem, output):
172182
else "custom"
173183
if problem.custom_output
174184
else "default"
175-
}\n""",
185+
}\n{validator_flags}""",
176186
)
177187
)
178188
else:

bin/generate.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ def run_interaction(self, bar, cwd, t):
246246
return True
247247

248248
testcase = Testcase(self.problem, in_path, short_path=(t.path.parent / (t.name + ".in")))
249+
assert isinstance(self.program, run.Submission)
249250
r = run.Run(self.problem, self.program, testcase)
250251

251252
# No {name}/{seed} substitution is done since all IO should be via stdin/stdout.
@@ -341,7 +342,7 @@ def __init__(self, generator_config):
341342
"retries",
342343
]
343344
RESERVED_DIRECTORY_KEYS: Final[Sequence[str]] = ["command"]
344-
KNOWN_ROOT_KEYS: Final[Sequence[str]] = ["generators", "parallel"]
345+
KNOWN_ROOT_KEYS: Final[Sequence[str]] = ["generators", "parallel", "version"]
345346
DEPRECATED_ROOT_KEYS: Final[Sequence[str]] = ["gitignore_generated"]
346347

347348

@@ -445,7 +446,7 @@ def __init__(self, problem, generator_config, key, name: str, yaml, parent, coun
445446
self.hardcoded = {}
446447

447448
# Hash of testcase for caching.
448-
self.hash = None
449+
self.hash: str
449450

450451
# Yaml of rule
451452
self.rule = dict[str, str | int]()
@@ -654,12 +655,12 @@ def link(t, problem, generator_config, bar, dst):
654655
# both source and target do not exist
655656
pass
656657

657-
def validate_in(t, problem, testcase, meta_yaml, bar):
658+
def validate_in(t, problem: Problem, testcase: Testcase, meta_yaml: dict, bar: ProgressBar):
658659
infile = problem.tmpdir / "data" / t.hash / "testcase.in"
659660
assert infile.is_file()
660661

661662
input_validator_hashes = testcase.validator_hashes(validate.InputValidator, bar)
662-
if all(h in meta_yaml.get("input_validator_hashes") for h in input_validator_hashes):
663+
if all(h in meta_yaml["input_validator_hashes"] for h in input_validator_hashes):
663664
return True
664665

665666
if not testcase.validate_format(
@@ -696,7 +697,7 @@ def validate_in(t, problem, testcase, meta_yaml, bar):
696697
)
697698
return True
698699

699-
def validate_ans(t, problem, testcase, meta_yaml, bar):
700+
def validate_ans(t, problem: Problem, testcase: Testcase, meta_yaml: dict, bar: ProgressBar):
700701
infile = problem.tmpdir / "data" / t.hash / "testcase.in"
701702
assert infile.is_file()
702703

@@ -725,7 +726,7 @@ def validate_ans(t, problem, testcase, meta_yaml, bar):
725726
**testcase.validator_hashes(validate.AnswerValidator, bar),
726727
**testcase.validator_hashes(validate.OutputValidator, bar),
727728
}
728-
if all(h in meta_yaml.get("answer_validator_hashes") for h in answer_validator_hashes):
729+
if all(h in meta_yaml["answer_validator_hashes"] for h in answer_validator_hashes):
729730
return True
730731

731732
if not testcase.validate_format(
@@ -1176,7 +1177,7 @@ def __init__(
11761177
)
11771178
if key in DEPRECATED_ROOT_KEYS:
11781179
message(
1179-
f"Dreprecated root level key: {key}, ignored",
1180+
f"Deprecated root level key: {key}, ignored",
11801181
"generators.yaml",
11811182
self.path,
11821183
color_type=MessageType.WARN,

bin/interactive.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def run_interactive_testcase(
2929
# else: path
3030
interaction: Optional[bool | Path] = False,
3131
submission_args: Optional[list[str]] = None,
32+
bar: Optional[ProgressBar] = None,
3233
):
3334
output_validators = run.problem.validators(validate.OutputValidator)
3435
if len(output_validators) != 1:
@@ -53,9 +54,13 @@ def get_validator_command():
5354
run.testcase.ans_path.resolve(),
5455
run.feedbackdir.resolve(),
5556
]
56-
+ run.problem.settings.validator_flags
57+
+ run.testcase.testdata_yaml_validator_args(
58+
output_validator,
59+
bar or PrintBar("Run interactive test case"),
60+
)
5761
)
5862

63+
assert run.submission.run_command, "Submission must be built"
5964
submission_command = run.submission.run_command
6065
if submission_args:
6166
submission_command += submission_args
@@ -64,7 +69,7 @@ def get_validator_command():
6469
validator_dir = output_validator.tmpdir
6570
submission_dir = run.submission.tmpdir
6671

67-
nextpass = run.feedbackdir / "nextpass.in" if run.problem.multi_pass else False
72+
nextpass = run.feedbackdir / "nextpass.in" if run.problem.multi_pass else None
6873

6974
if config.args.verbose >= 2:
7075
print("Validator: ", *get_validator_command(), file=sys.stderr)

bin/problem.py

Lines changed: 86 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,6 @@ def __init__(
268268
if "name" in yaml_data and isinstance(yaml_data["name"], str):
269269
yaml_data["name"] = {"en": yaml_data["name"]}
270270

271-
if "validator_flags" in yaml_data and isinstance(yaml_data["validator_flags"], str):
272-
yaml_data["validator_flags"] = shlex.split(yaml_data["validator_flags"])
273-
274271
# Known keys:
275272
# (defaults from https://icpc.io/problem-package-format/spec/2023-07-draft.html#problem-metadata)
276273
self.problem_format_version: str = parse_setting(
@@ -328,8 +325,21 @@ def __init__(
328325
# Not implemented in BAPCtools. Should be a date, but we don't do anything with this anyway.
329326
self.embargo_until: str = parse_setting(yaml_data, "embargo-until", "")
330327
self.limits = ProblemLimits(parse_setting(yaml_data, "limits", {}), problem, self)
331-
# TODO: move to testdata.yaml
332-
self.validator_flags: list[str] = parse_setting(yaml_data, "validator_flags", [])
328+
329+
# If problem.yaml uses 2023-07-draft, disallow `validator_flags`.
330+
if self.is_legacy():
331+
if "validator_flags" in yaml_data and isinstance(yaml_data["validator_flags"], str):
332+
yaml_data["validator_flags"] = shlex.split(yaml_data["validator_flags"])
333+
# This field should not be used anywhere except the default result of Problem.get_testdata_yaml().
334+
self._validator_flags: list[str] = parse_setting(yaml_data, "validator_flags", [])
335+
else:
336+
self._validator_flags = []
337+
if "validator_flags" in yaml_data:
338+
warn(
339+
"problem.yaml: 'validator_flags' is removed in 2023-07-draft, please use 'output_validator_args' in 'testdata.yaml' instead. SKIPPED."
340+
)
341+
yaml_data.pop("validator_flags")
342+
333343
self.keywords: str = parse_setting(yaml_data, "keywords", "")
334344
# Not implemented in BAPCtools. We always test all languges in langauges.yaml.
335345
self.languages: list[str] = parse_optional_list_setting(yaml_data, "languages", str)
@@ -481,6 +491,7 @@ def _read_settings(self):
481491
self.multi_pass: bool = self.settings.multi_pass
482492
self.custom_output: bool = self.settings.custom_output
483493

494+
# TODO #102 move to TestData class
484495
def _parse_testdata_yaml(p, path, bar):
485496
assert path.is_relative_to(p.path / "data")
486497
for dir in [path] + list(path.parents):
@@ -495,20 +506,36 @@ def _parse_testdata_yaml(p, path, bar):
495506
if f not in p._testdata_yamls:
496507
p._testdata_yamls[f] = flags = read_yaml(f, plain=True)
497508

498-
# verify testdata.yaml
509+
if p.settings.is_legacy():
510+
# For legacy problems, support both _flags and _args, but move to _args.
511+
if (
512+
"output_validator_flags" in flags
513+
and "output_validator_args" not in flags
514+
):
515+
flags["output_validator_args"] = flags.pop("output_validator_flags")
516+
if "input_validator_flags" in flags and "input_validator_args" not in flags:
517+
flags["input_validator_args"] = flags.pop("input_validator_flags")
518+
else:
519+
# For 2023-07-draft problems, skip the old name and warn to use the new one.
520+
if "input_validator_flags" in flags:
521+
bar.warn(
522+
"input_validator_flags is removed in 2023-07-draft, use ..._args instead. SKIPPED."
523+
)
524+
if "output_validator_flags" in flags:
525+
bar.warn(
526+
"output_validator_flags is removed in 2023-07-draft, use ..._args instead. SKIPPED."
527+
)
528+
529+
# Verify testdata.yaml
499530
for k in flags:
500531
match k:
501-
case "output_validator_flags":
532+
case "output_validator_args":
502533
if not isinstance(flags[k], str):
503-
bar.error(
504-
"ouput_validator_flags must be string",
505-
resume=True,
506-
print_item=False,
507-
)
508-
case "input_validator_flags":
534+
bar.error(f"{k} must be string", resume=True, print_item=False)
535+
case "input_validator_args":
509536
if not isinstance(flags[k], (str, dict)):
510537
bar.error(
511-
"input_validator_flags must be string or map",
538+
f"{k} must be string or map",
512539
resume=True,
513540
print_item=False,
514541
)
@@ -521,16 +548,32 @@ def _parse_testdata_yaml(p, path, bar):
521548
f"Unknown input validator {name}; expected {input_validator_names}",
522549
print_item=False,
523550
)
524-
case "grading" | "run_samples":
525-
bar.warn(f"{k} not implemented in BAPCtools", print_item=False)
551+
case (
552+
"args"
553+
| "description"
554+
| "full_feedback"
555+
| "hint"
556+
| "scoring"
557+
| "static_validation"
558+
):
559+
bar.warn(
560+
f"{k} in testdata.yaml not implemented in BAPCtools",
561+
print_item=False,
562+
)
526563
case _:
527564
path = f.relative_to(p.path / "data")
528565
bar.warn(f'Unknown key "{k}" in {path}', print_item=False)
529566
# Do not go above the data directory.
530567
if dir == p.path / "data":
531568
break
532569

533-
def get_testdata_yaml(p, path, key, bar, name=None) -> str | None:
570+
def get_testdata_yaml(
571+
p,
572+
path: Path,
573+
key: Literal["input_validator_args"] | Literal["output_validator_args"],
574+
bar: ProgressBar | PrintBar,
575+
name: Optional[str] = None,
576+
) -> list[str]:
534577
"""
535578
Find the testdata flags applying at the given path for the given key.
536579
If necessary, walk up from `path` looking for the first testdata.yaml file that applies,
@@ -540,50 +583,55 @@ def get_testdata_yaml(p, path, key, bar, name=None) -> str | None:
540583
Arguments
541584
---------
542585
path: absolute path (a file or a directory)
543-
key: The testdata.yaml key to look for, either of 'input_validator_flags', 'output_validator_flags', or 'grading'.
544-
'grading' is not implemented
545-
name: If key == 'input_validator_flags', optionally the name of the input validator
586+
key: The testdata.yaml key to look for, either of 'input_validator_args', 'output_validator_args', or 'grading'.
587+
TODO: 'grading' is not yet implemented.
588+
name: If key == 'input_validator_args', optionally the name of the input validator.
546589
547590
Returns:
548591
--------
549-
string or None if no testdata.yaml is found.
592+
A list of string arguments, which is empty if no testdata.yaml is found.
550593
TODO: when 'grading' is supported, it also can return dict
551594
"""
552-
if key not in ["input_validator_flags", "output_validator_flags"]:
595+
if key not in ["input_validator_args", "output_validator_args"]:
553596
raise NotImplementedError(key)
554-
if key != "input_validator_flags" and name is not None:
597+
if key != "input_validator_args" and name is not None:
555598
raise ValueError(
556599
f"Only input validators support flags by validator name, got {key} and {name}"
557600
)
558601

559602
# parse and cache testdata.yaml
560603
p._parse_testdata_yaml(path, bar)
561604

605+
# For legacy problems, default to validator_flags from problem.yaml
606+
default_result = []
607+
if p.settings.is_legacy() and p.settings._validator_flags:
608+
default_result = p.settings._validator_flags
609+
562610
# extract the flags
563611
for dir in [path] + list(path.parents):
564612
# Do not go above the data directory.
565613
if dir == p.path:
566-
return None
614+
return default_result
567615

568616
f = dir / "testdata.yaml"
569617
if f not in p._testdata_yamls:
570618
continue
571619
flags = p._testdata_yamls[f]
572620
if key in flags:
573-
if key == "output_validator_flags":
621+
if key == "output_validator_args":
574622
if not isinstance(flags[key], str):
575-
bar.error("ouput_validator_flags must be string")
576-
return flags[key]
623+
bar.error("ouput_validator_args must be string")
624+
return flags[key].split()
577625

578-
if key == "input_validator_flags":
626+
if key == "input_validator_args":
579627
if not isinstance(flags[key], (str, dict)):
580-
bar.error("input_validator_flags must be string or map")
628+
bar.error("input_validator_args must be string or map")
581629
if isinstance(flags[key], str):
582-
return flags[key]
630+
return flags[key].split()
583631
elif name in flags[key]:
584-
return flags[key][name]
632+
return flags[key][name].split()
585633

586-
return None
634+
return default_result
587635

588636
def testcases(
589637
p,
@@ -1210,14 +1258,11 @@ def validate_valid_extra_data(p) -> bool:
12101258
if not p.validators(validate.OutputValidator, strict=True, print_warn=False):
12111259
return True
12121260

1213-
args = (
1214-
p.get_testdata_yaml(
1215-
p.path / "data" / "valid_output",
1216-
"output_validator_flags",
1217-
PrintBar("Generic Output Validation"),
1218-
)
1219-
or ""
1220-
).split()
1261+
args = p.get_testdata_yaml(
1262+
p.path / "data" / "valid_output",
1263+
"output_validator_args",
1264+
PrintBar("Generic Output Validation"),
1265+
)
12211266
is_space_sensitive = "space_change_sensitive" in args
12221267
is_case_sensitive = "case_sensitive" in args
12231268

@@ -1313,7 +1358,7 @@ def _validate_data(
13131358
# validate the testcases
13141359
bar = ProgressBar(action, items=[t.name for t in testcases])
13151360

1316-
def process_testcase(testcase):
1361+
def process_testcase(testcase: testcase.Testcase):
13171362
nonlocal success
13181363

13191364
localbar = bar.start(testcase.name)

0 commit comments

Comments
 (0)