Skip to content

Commit d56ea40

Browse files
authored
Implement 'type' field in problem.yaml from 2023-07-draft spec (#421)
* Implement 'type' field in problem.yaml from 2023-07-draft spec * [problem] Move determining fields interactive, multipass, and custom_output to constructor of ProblemSettings * Rename variables `multipass` to `multi_pass` * [skel] Read type instead of validation * [problem] Mark TODOs for #424 (support singular output_validator/) * [skel] Remove support for `bt new_problem --type default` in favour of `pass-fail` * [problem] If we don't find output validator paths, abort when problem type is interactive and/or multi-pass
1 parent 0721ea7 commit d56ea40

17 files changed

+140
-101
lines changed

bin/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
grep -Ev '^(h|jobs|time|verbose)$' | sed "s/^/'/;s/$/',/" | tr '\n' ' ' | sed 's/^/ARGS_LIST: Final = [/;s/, $/]\n/'
9797
"""
9898
# fmt: off
99-
ARGS_LIST: Final = ['1', 'add', 'all', 'answer', 'api', 'author', 'check_deterministic', 'clean', 'colors', 'contest', 'contest_id', 'contestname', 'cp', 'default_solution', 'depth', 'directory', 'error', 'force', 'force_build', 'input', 'interaction', 'interactive', 'invalid', 'kattis', 'language', 'memory', 'more', 'move_to', 'no_bar', 'no_generate', 'no_solution', 'no_solutions', 'no_testcase_sanity_checks', 'no_time_limit', 'no_validators', 'no_visualizer', 'open', 'order', 'order_from_ccs', 'overview', 'password', 'post_freeze', 'problem', 'problemname', 'remove', 'reorder', 'samples', 'sanitizer', 'skel', 'skip', 'sort', 'submissions', 'table', 'testcases', 'time_limit', 'timeout', 'token', 'tree', 'username', 'validation', 'watch', 'web', 'write']
99+
ARGS_LIST: Final = ['1', 'add', 'all', 'answer', 'api', 'author', 'check_deterministic', 'clean', 'colors', 'contest', 'contest_id', 'contestname', 'cp', 'default_solution', 'depth', 'directory', 'error', 'force', 'force_build', 'input', 'interaction', 'interactive', 'invalid', 'kattis', 'language', 'memory', 'more', 'move_to', 'no_bar', 'no_generate', 'no_solution', 'no_solutions', 'no_testcase_sanity_checks', 'no_time_limit', 'no_validators', 'no_visualizer', 'open', 'order', 'order_from_ccs', 'overview', 'password', 'post_freeze', 'problem', 'problemname', 'remove', 'reorder', 'samples', 'sanitizer', 'skel', 'skip', 'sort', 'submissions', 'table', 'testcases', 'time_limit', 'timeout', 'token', 'tree', 'type', 'username', 'watch', 'web', 'write']
100100
# fmt: on
101101

102102

bin/constraints.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def check_validators(problem):
2222
if not in_constraints:
2323
warn("No constraint validation of input values found in input validators.")
2424
problem.validate_data(validate.Mode.ANSWER, constraints=ans_constraints)
25-
if not problem.interactive and not problem.multipass and not ans_constraints:
25+
if not problem.interactive and not problem.multi_pass and not ans_constraints:
2626
log("No constraint validation of answer values found in answer or output validators.")
2727
print()
2828

bin/export.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,11 @@ def build_samples_zip(problems, output, statement_language):
7474
outputdir = Path(problem.label)
7575

7676
attachments_dir = problem.path / "attachments"
77-
if (problem.interactive or problem.multipass) and not attachments_dir.is_dir():
77+
if (problem.interactive or problem.multi_pass) and not attachments_dir.is_dir():
7878
interactive = "interactive " if problem.interactive else ""
79-
multipass = "multi-pass " if problem.multipass else ""
79+
multi_pass = "multi-pass " if problem.multi_pass else ""
8080
util.error(
81-
f"{interactive}{multipass}problem {problem.name} does not have an attachments/ directory."
81+
f"{interactive}{multi_pass}problem {problem.name} does not have an attachments/ directory."
8282
)
8383
continue
8484

@@ -96,7 +96,7 @@ def build_samples_zip(problems, output, statement_language):
9696
util.error(f"Cannot include broken file {f}.")
9797

9898
# Add samples for non-interactive and non-multi-pass problems.
99-
if not problem.interactive and not problem.multipass:
99+
if not problem.interactive and not problem.multi_pass:
100100
samples = problem.testcases(only_samples=True)
101101
if samples:
102102
for i in range(0, len(samples)):
@@ -128,15 +128,15 @@ def build_problem_zip(problem, output):
128128
("problem_statement/*", True),
129129
("submissions/accepted/**/*", True),
130130
("submissions/*/**/*", False),
131-
("attachments/**/*", problem.interactive or problem.multipass),
131+
("attachments/**/*", problem.interactive or problem.multi_pass),
132132
]
133133

134134
testcases = [
135135
("data/secret/**/*.in", True),
136-
("data/sample/**/*.in", not problem.interactive and not problem.multipass),
136+
("data/sample/**/*.in", not problem.interactive and not problem.multi_pass),
137137
]
138138

139-
if problem.interactive or problem.multipass:
139+
if problem.interactive or problem.multi_pass:
140140
# .interaction files don't need a corresponding .in
141141
# therefore we can handle them like all other files
142142
files += [("data/sample/**/*.interaction", False)]
@@ -148,7 +148,7 @@ def build_problem_zip(problem, output):
148148
("data/sample/**/*.ans.statement", False),
149149
]
150150

151-
if "custom" in problem.settings.validation:
151+
if problem.custom_output:
152152
files.append(("output_validators/**/*", True))
153153

154154
if config.args.kattis:

bin/fuzz.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def _run(self, bar):
7272
localbar.done()
7373

7474
# Generate .ans.
75-
if not self.fuzz.problem.interactive and not self.fuzz.problem.multipass:
75+
if not self.fuzz.problem.interactive and not self.fuzz.problem.multi_pass:
7676
if self.solution and not testcase.ans_path.is_file():
7777
if testcase.ans_path.is_file():
7878
testcase.ans_path.unlink()

bin/generate.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -706,11 +706,11 @@ def validate_ans(t, problem, testcase, meta_yaml, bar):
706706
ansfile = problem.tmpdir / "data" / t.hash / "testcase.ans"
707707
assert ansfile.is_file()
708708

709-
if problem.interactive or problem.multipass:
709+
if problem.interactive or problem.multi_pass:
710710
if ansfile.stat().st_size != 0:
711711
interactive = "interaction " if problem.interactive else ""
712-
multipass = "multipass " if problem.multipass else ""
713-
bar.warn(f".ans file for {interactive}{multipass}problem is expected to be empty.")
712+
multi_pass = "multi-pass " if problem.multi_pass else ""
713+
bar.warn(f".ans file for {interactive}{multi_pass}problem is expected to be empty.")
714714
else:
715715
size = ansfile.stat().st_size
716716
if (
@@ -958,8 +958,8 @@ def needed(ext):
958958

959959
used_solution = False
960960
changed_ans = False
961-
if problem.interactive or problem.multipass:
962-
# Generate empty ans file for interactive/multipass problems
961+
if problem.interactive or problem.multi_pass:
962+
# Generate empty ans file for interactive/multi-pass problems
963963
if ".ans" not in meta_yaml["generated_extensions"]:
964964
if not ansfile.is_file() or ansfile.stat().st_size != 0:
965965
ansfile.write_text("")
@@ -1829,7 +1829,7 @@ def build_program(p):
18291829
build_programs(program.Visualizer, visualizers_used)
18301830

18311831
self.problem.validators(validate.InputValidator)
1832-
if not self.problem.interactive and not self.problem.multipass:
1832+
if not self.problem.interactive and not self.problem.multi_pass:
18331833
self.problem.validators(validate.AnswerValidator)
18341834
self.problem.validators(validate.OutputValidator)
18351835

bin/interactive.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def get_validator_command():
6464
validator_dir = output_validator.tmpdir
6565
submission_dir = run.submission.tmpdir
6666

67-
nextpass = run.feedbackdir / "nextpass.in" if run.problem.multipass else False
67+
nextpass = run.feedbackdir / "nextpass.in" if run.problem.multi_pass else False
6868

6969
if config.args.verbose >= 2:
7070
print("Validator: ", *get_validator_command(), file=sys.stderr)
@@ -132,7 +132,7 @@ def get_validator_command():
132132
_feedback(run, validator_err),
133133
exec_res.err,
134134
verdict,
135-
pass_id if run.problem.multipass else None,
135+
pass_id if run.problem.multi_pass else None,
136136
)
137137
else:
138138
tle_result.timeout_expired |= max_duration >= timeout
@@ -170,7 +170,7 @@ def get_validator_command():
170170
_feedback(run, validator_err),
171171
exec_res.err,
172172
verdict,
173-
pass_id if run.problem.multipass else None,
173+
pass_id if run.problem.multi_pass else None,
174174
)
175175
else:
176176
tle_result.duration = max_duration
@@ -401,7 +401,7 @@ def kill_handler_function():
401401
val_err,
402402
team_err,
403403
verdict,
404-
pass_id if run.problem.multipass else None,
404+
pass_id if run.problem.multi_pass else None,
405405
)
406406
else:
407407
tle_result.timeout_expired |= aborted
@@ -433,7 +433,7 @@ def kill_handler_function():
433433
val_err,
434434
team_err,
435435
verdict,
436-
pass_id if run.problem.multipass else None,
436+
pass_id if run.problem.multi_pass else None,
437437
)
438438
else:
439439
tle_result.duration = max_duration

bin/latex.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,10 @@ def flush():
104104
last = line[0]
105105
flush()
106106
else:
107-
assert problem.multipass
107+
assert problem.multi_pass
108108

109-
multipass_dir = builddir / "multipass"
110-
multipass_dir.mkdir(exist_ok=True)
109+
multi_pass_dir = builddir / "multi_pass"
110+
multi_pass_dir.mkdir(exist_ok=True)
111111

112112
lines = sample.read_text()
113113
last = "<"
@@ -121,8 +121,8 @@ def flush():
121121
def flush():
122122
nonlocal current_sample
123123

124-
in_path = multipass_dir / f"{sample_name}-{pass_id:02}.in"
125-
out_path = multipass_dir / f"{sample_name}-{pass_id:02}.out"
124+
in_path = multi_pass_dir / f"{sample_name}-{pass_id:02}.in"
125+
out_path = multi_pass_dir / f"{sample_name}-{pass_id:02}.out"
126126
in_path.write_text(cur_in)
127127
out_path.write_text(cur_out)
128128

bin/problem.py

+69-21
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@
2222
from colorama import Fore, Style
2323

2424

25+
# Parse validation mode (only for legacy problem format version)
26+
def parse_legacy_validation(mode: str) -> set[str]:
27+
if mode == "default":
28+
return {mode}
29+
else:
30+
ok = True
31+
parsed = set()
32+
for part in mode.split():
33+
if part in ["custom", "interactive", "multi-pass"] and part not in parsed:
34+
parsed.add(part)
35+
else:
36+
ok = False
37+
if "custom" not in parsed or not ok:
38+
fatal(f"Unrecognised validation mode {mode}.")
39+
return parsed
40+
41+
2542
class ProblemLimits:
2643
def __init__(
2744
self,
@@ -104,7 +121,12 @@ def __init__(
104121

105122

106123
class ProblemSettings:
107-
def __init__(self, yamldata: dict[str, Any], legacy_time_limit: Optional[float] = None):
124+
def __init__(
125+
self,
126+
yamldata: dict[str, Any],
127+
problem: "Problem",
128+
legacy_time_limit: Optional[float] = None,
129+
):
108130
assert isinstance(yamldata, dict)
109131

110132
if "name" in yamldata and isinstance(yamldata["name"], str):
@@ -120,7 +142,28 @@ def __init__(self, yamldata: dict[str, Any], legacy_time_limit: Optional[float]
120142
)
121143
if not self.is_legacy() and self.problem_format_version != "2023-07-draft":
122144
fatal(f"problem_format_version {self.problem_format_version} not supported")
123-
# TODO: also support 'type'. For now, we only support legacy 'validation'.
145+
146+
if self.is_legacy():
147+
mode = parse_legacy_validation(parse_setting(yamldata, "validation", "default"))
148+
else:
149+
if "validation" in yamldata:
150+
warn(
151+
"problem.yaml: 'validation' is removed in 2023-07-draft, please use 'type' instead"
152+
)
153+
mode = set(parse_setting(yamldata, "type", "pass-fail").split(" "))
154+
self.interactive: bool = "interactive" in mode
155+
self.multi_pass: bool = "multi-pass" in mode
156+
self.custom_output: bool = (
157+
self.interactive
158+
or self.multi_pass
159+
or (
160+
"custom" in mode
161+
if self.is_legacy()
162+
# TODO #424: output_validator should be singular, but DOMjudge does not support this yet, so this should be fixed during export.
163+
else (problem.path / "output_validators").exists()
164+
)
165+
)
166+
124167
self.name: dict[str, str] = parse_setting(yamldata, "name", {"en": ""})
125168
self.uuid: str = parse_setting(yamldata, "uuid", "")
126169
self.author: str = parse_setting(yamldata, "author", "")
@@ -129,7 +172,6 @@ def __init__(self, yamldata: dict[str, Any], legacy_time_limit: Optional[float]
129172
self.license: str = parse_setting(yamldata, "license", "unknown")
130173
self.rights_owner: str = parse_setting(yamldata, "rights_owner", "")
131174
self.limits = ProblemLimits(parse_setting(yamldata, "limits", {}), self, legacy_time_limit)
132-
self.validation: str = parse_setting(yamldata, "validation", "default")
133175
self.validator_flags: list[str] = parse_setting(yamldata, "validator_flags", [])
134176
self.keywords: str = parse_setting(yamldata, "keywords", "")
135177

@@ -279,19 +321,20 @@ def _read_settings(self):
279321
yaml_path.write_text(raw)
280322
log("Added new UUID to problem.yaml")
281323

282-
self.settings = ProblemSettings(yaml_data, self._get_legacy_time_limit(yaml_data))
283-
self.limits = self.settings.limits
324+
self.settings = ProblemSettings(yaml_data, self, self._get_legacy_time_limit(yaml_data))
284325

285-
mode = parse_validation(self.settings.validation)
286-
self.interactive = "interactive" in mode
287-
self.multipass = "multi-pass" in mode
326+
# Aliasing fields makes life easier for us 😛
327+
self.limits: ProblemLimits = self.settings.limits
328+
self.interactive: bool = self.settings.interactive
329+
self.multi_pass: bool = self.settings.multi_pass
330+
self.custom_output: bool = self.settings.custom_output
288331

289332
# Handle dependencies...
290333
has_validation_passes = self.limits.validation_passes is not None
291-
if self.multipass and not has_validation_passes:
334+
if self.multi_pass and not has_validation_passes:
292335
self.limits.validation_passes = 2
293-
if not self.multipass and has_validation_passes:
294-
warn("limit: validation_passes is only used for multipass problems. SKIPPED.")
336+
if not self.multi_pass and has_validation_passes:
337+
warn("limit: validation_passes is only used for multi_pass problems. SKIPPED.")
295338

296339
def _parse_testdata_yaml(p, path, bar):
297340
assert path.is_relative_to(p.path / "data")
@@ -444,14 +487,14 @@ def testcases(
444487
for f in in_paths:
445488
t = testcase.Testcase(p, f, print_warn=True)
446489
if (
447-
(p.interactive or p.multipass)
490+
(p.interactive or p.multi_pass)
448491
and mode == validate.Mode.INVALID
449492
and t.root in ["invalid_answers", "invalid_outputs"]
450493
):
451494
msg = ""
452495
if p.interactive:
453496
msg += " interactive"
454-
if p.multipass:
497+
if p.multi_pass:
455498
msg += " multi-pass"
456499
warn(f"Found file {f} for {mode} validation in{msg} problem. Skipping.")
457500
continue
@@ -504,7 +547,7 @@ def statement_samples(p) -> list[Path | tuple[Path, Path]]:
504547
# Non-interactive and Non-multi-pass problems should not have .interaction files.
505548
# On the other hand, interactive problems are allowed to have .{in,ans}.statement files,
506549
# so that they can emulate a non-interactive problem with on-the-fly generated input.
507-
if not p.interactive and not p.multipass:
550+
if not p.interactive and not p.multi_pass:
508551
if len(interaction_paths) != 0:
509552
warn(
510553
f"Non-interactive/Non-multi-pass problem {p.name} should not have data/sample/*.interaction files."
@@ -672,13 +715,18 @@ def _validators(
672715
return problem._validators_cache[key]
673716

674717
assert hasattr(cls, "source_dirs")
718+
# TODO #424: We should not support multiple output validators inside output_validator/.
675719
paths = [p for source_dir in cls.source_dirs for p in glob(problem.path / source_dir, "*")]
676720

677721
# Handle default output validation
678-
if cls == validate.OutputValidator and problem.settings.validation == "default":
679-
if paths:
722+
if cls == validate.OutputValidator:
723+
if problem.settings.is_legacy() and not problem.custom_output and paths:
680724
error("Validation is default but custom output validator exists (ignoring it)")
681-
paths = [config.TOOLS_ROOT / "support" / "default_output_validator.cpp"]
725+
paths = []
726+
if not paths:
727+
if problem.custom_output:
728+
fatal("Problem validation type requires output_validators/")
729+
paths = [config.TOOLS_ROOT / "support" / "default_output_validator.cpp"]
682730

683731
# TODO: Instead of checking file contents, maybe specify this in generators.yaml?
684732
def has_constraints_checking(f):
@@ -906,12 +954,12 @@ def validate_data(problem, mode: validate.Mode, constraints: dict | bool | None
906954
constraints = {}
907955
assert constraints is None or isinstance(constraints, dict)
908956

909-
if (problem.interactive or problem.multipass) and mode == validate.Mode.ANSWER:
957+
if (problem.interactive or problem.multi_pass) and mode == validate.Mode.ANSWER:
910958
if (problem.path / "answer_validators").exists():
911959
msg = ""
912960
if problem.interactive:
913961
msg += " interactive"
914-
if problem.multipass:
962+
if problem.multi_pass:
915963
msg += " multi-pass"
916964
log(f"Not running answer_validators for{msg} problems.")
917965
return True
@@ -925,12 +973,12 @@ def validate_data(problem, mode: validate.Mode, constraints: dict | bool | None
925973
testcases = problem.testcases(mode=mode)
926974
case validate.Mode.ANSWER:
927975
assert not problem.interactive
928-
assert not problem.multipass
976+
assert not problem.multi_pass
929977
problem.validators(validate.AnswerValidator, check_constraints=check_constraints)
930978
testcases = problem.testcases(mode=mode)
931979
case validate.Mode.INVALID:
932980
problem.validators(validate.InputValidator)
933-
if not problem.interactive and not problem.multipass:
981+
if not problem.interactive and not problem.multi_pass:
934982
problem.validators(validate.AnswerValidator)
935983
testcases = problem.testcases(mode=mode)
936984
case _:

0 commit comments

Comments
 (0)