Skip to content

Commit 4ea74f5

Browse files
mzuennimpsijm
andcommitted
Output validator (#435)
* use output_validator * upgraded tests * updated skel * fix symlink * create output_validators dir * changed class constants * fix source_dir(s) * copy symlinks * export legacy * [export] Fix typo in comment --------- Co-authored-by: Maarten Sijm <[email protected]>
1 parent 2f1740a commit 4ea74f5

File tree

29 files changed

+203
-135
lines changed

29 files changed

+203
-135
lines changed

bin/export.py

+74-28
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ def build_samples_zip(problems, output, statement_language):
9292
def build_problem_zip(problem: Problem, output: Path):
9393
"""Make DOMjudge/Kattis ZIP file for specified problem."""
9494

95+
if not has_ryaml:
96+
error("zip needs the ruamel.yaml python3 library. Install python[3]-ruamel.yaml.")
97+
return
98+
9599
# Add problem PDF for only one language to the zip file (note that Kattis export does not include PDF)
96100
statement_language = None if config.args.kattis else force_single_language([problem])
97101

@@ -121,7 +125,7 @@ def build_problem_zip(problem: Problem, output: Path):
121125
]
122126

123127
if problem.custom_output:
124-
files.append(("output_validators/**/*", True))
128+
files.append(("output_validator/**/*", True))
125129

126130
if config.args.kattis:
127131
files.append(("input_validators/**/*", True))
@@ -170,33 +174,64 @@ def add_file(path, source):
170174
out = f2.relative_to(problem.path)
171175
add_file(out, f2)
172176

173-
# DOMjudge does not support 'type' in problem.yaml nor 'output_validator_args' in testdata.yaml yet.
174-
# TODO: Remove this once it does.
175-
if not config.args.kattis:
176-
yaml_path = export_dir / "problem.yaml"
177-
yaml_data = [yaml_path.read_text(), "\nvalidation:"]
178-
if problem.custom_output:
179-
yaml_data.append(" custom")
180-
if problem.interactive:
181-
yaml_data.append(" interactive")
182-
if problem.multi_pass:
183-
yaml_data.append(" multi-pass")
184-
else:
185-
yaml_data.append(" default")
186-
yaml_data.append("\n")
187-
188-
validator_flags = " ".join(
189-
problem.get_testdata_yaml(
190-
problem.path / "data",
191-
"output_validator_args",
192-
PrintBar("Getting validator_flags for legacy DOMjudge export"),
193-
)
177+
# DOMjudge and Kattis do not support 2023-07-draft yet.
178+
# TODO: Remove once they do.
179+
from ruamel.yaml.comments import CommentedMap
180+
181+
yaml_path = export_dir / "problem.yaml"
182+
yaml_data = read_yaml(yaml_path)
183+
# drop format version -> legacy
184+
if "problem_format_version" in yaml_data:
185+
ryaml_filter(yaml_data, "problem_format_version")
186+
# type -> validation
187+
if "type" in yaml_data:
188+
ryaml_filter(yaml_data, "type")
189+
validation = []
190+
if problem.custom_output:
191+
validation.append("custom")
192+
if problem.interactive:
193+
validation.append("interactive")
194+
if problem.multi_pass:
195+
validation.append("multi-pass")
196+
else:
197+
validation.append("default")
198+
yaml_data["validation"] = " ".join(validation)
199+
# credits -> author
200+
if "credits" in yaml_data:
201+
ryaml_filter(yaml_data, "credits")
202+
if problem.settings.credits.authors:
203+
yaml_data["author"] = ", ".join(p.name for p in problem.settings.credits.authors)
204+
# change source:
205+
if problem.settings.source:
206+
if len(problem.settings.source) > 1:
207+
util.warn(f"Found multiple sources, using '{problem.settings.source[0].name}'.")
208+
yaml_data["source"] = problem.settings.source[0].name
209+
yaml_data["source_url"] = problem.settings.source[0].url
210+
# limits.time_multipliers -> time_multiplier / time_safety_margin
211+
if "limits" not in yaml_data or not yaml_data["limits"]:
212+
yaml_data["limits"] = CommentedMap()
213+
limits = yaml_data["limits"]
214+
if "time_multipliers" in limits:
215+
ryaml_filter(limits, "time_multipliers")
216+
limits["time_multiplier"] = problem.limits.ac_to_time_limit
217+
limits["time_safety_margin"] = problem.limits.time_limit_to_tle
218+
# drop explicit timelimit for kattis:
219+
if "time_limit" in limits:
220+
# keep this for kattis even when "time_limit" is supported
221+
ryaml_filter(limits, "time_limit")
222+
# validator_flags
223+
validator_flags = " ".join(
224+
problem.get_testdata_yaml(
225+
problem.path / "data",
226+
"output_validator_args",
227+
PrintBar("Getting validator_flags for legacy export"),
194228
)
195-
if validator_flags:
196-
yaml_data.append(f"validator_flags: {validator_flags}\n")
197-
198-
yaml_path.unlink()
199-
yaml_path.write_text("".join(yaml_data))
229+
)
230+
if validator_flags:
231+
yaml_data["validator_flags"] = validator_flags
232+
# write legacy style yaml
233+
yaml_path.unlink()
234+
write_yaml(yaml_data, yaml_path)
200235

201236
# DOMjudge does not support 'limits.time_limit' in problem.yaml yet.
202237
# TODO: Remove this once it does.
@@ -224,7 +259,7 @@ def add_file(path, source):
224259
if problem.settings.constants:
225260
constants_supported = [
226261
"data/**/testdata.yaml",
227-
"output_validators/**/*",
262+
"output_validator/**/*",
228263
"input_validators/**/*",
229264
# "statement/*", uses \constants
230265
# "submissions/*/**/*", removed support?
@@ -242,6 +277,13 @@ def add_file(path, source):
242277
f.unlink()
243278
f.write_text(text)
244279

280+
# TODO: Remove this if we know others use the output_validator dir
281+
if (export_dir / "output_validator").exists():
282+
(export_dir / "output_validators").mkdir(parents=True)
283+
(export_dir / "output_validator").rename(
284+
export_dir / "output_validators" / "output_validator"
285+
)
286+
245287
# TODO: Remove this if we know others import the statement folder
246288
if (export_dir / "statement").exists():
247289
(export_dir / "statement").rename(export_dir / "problem_statement")
@@ -286,6 +328,10 @@ def add_file(path, source):
286328
# solutions*.{lang}.pdf
287329
# Output is <outfile>
288330
def build_contest_zip(problems, zipfiles, outfile, statement_language):
331+
if not has_ryaml:
332+
error("zip needs the ruamel.yaml python3 library. Install python[3]-ruamel.yaml.")
333+
return
334+
289335
print(f"writing ZIP file {outfile}", file=sys.stderr)
290336

291337
if not config.args.kattis: # Kattis does not use problems.yaml.

bin/generate.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -729,10 +729,7 @@ def validate_ans(t, problem: Problem, testcase: Testcase, meta_yaml: dict, bar:
729729
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)"
730730
)
731731

732-
answer_validator_hashes = {
733-
**testcase.validator_hashes(validate.AnswerValidator, bar),
734-
**testcase.validator_hashes(validate.OutputValidator, bar),
735-
}
732+
answer_validator_hashes = {**testcase.validator_hashes(validate.AnswerValidator, bar)}
736733
if all(h in meta_yaml["answer_validator_hashes"] for h in answer_validator_hashes):
737734
return True
738735

bin/interactive.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def run_interactive_testcase(
3232
bar: Optional[ProgressBar] = None,
3333
):
3434
output_validators = run.problem.validators(validate.OutputValidator)
35-
if len(output_validators) != 1:
35+
if not output_validators:
3636
return None
3737
output_validator = output_validators[0]
3838

bin/problem.py

+10-17
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def __init__(
224224
self.interactive: bool = "interactive" in mode
225225
self.multi_pass: bool = "multi-pass" in mode
226226
self.custom_output: bool = (
227-
self.interactive or self.multi_pass or (problem.path / "output_validators").exists()
227+
self.interactive or self.multi_pass or (problem.path / "output_validator").is_dir()
228228
)
229229

230230
self.name: dict[str, str] = parse_setting(yaml_data, "name", {"en": ""})
@@ -801,8 +801,6 @@ def validators(
801801
warn("No input validators found.")
802802
case validate.AnswerValidator, 0:
803803
warn("No answer validators found")
804-
case validate.OutputValidator, l if l != 1:
805-
error(f"Found {len(validators)} output validators, expected exactly one.")
806804

807805
build_ok = all(v.ok for v in validators)
808806

@@ -817,16 +815,16 @@ def _validators(
817815
if key in problem._validators_cache:
818816
return problem._validators_cache[key]
819817

820-
assert hasattr(cls, "source_dirs")
821-
# TODO #424: We should not support multiple output validators inside output_validator/.
822-
paths = [p for source_dir in cls.source_dirs for p in glob(problem.path / source_dir, "*")]
823-
824-
# Handle default output validation
825818
if cls == validate.OutputValidator:
826-
if not paths:
827-
if problem.custom_output:
828-
fatal("Problem validation type requires output_validators/")
819+
if problem.custom_output:
820+
paths = [problem.path / validate.OutputValidator.source_dir]
821+
else:
829822
paths = [config.TOOLS_ROOT / "support" / "default_output_validator.cpp"]
823+
else:
824+
assert hasattr(cls, "source_dirs")
825+
paths = [
826+
p for source_dir in cls.source_dirs for p in glob(problem.path / source_dir, "*")
827+
]
830828

831829
# TODO: Instead of checking file contents, maybe specify this in generators.yaml?
832830
def has_constraints_checking(f):
@@ -878,13 +876,8 @@ def prepare_run(problem):
878876
if not testcases:
879877
return False
880878

881-
if problem.interactive or problem.multi_pass:
882-
validators = problem.validators(validate.OutputValidator)
883-
if not validators:
884-
return False
885-
886879
# Pre build the output validator to prevent nested ProgressBars.
887-
if problem.validators(validate.OutputValidator) is False:
880+
if not problem.validators(validate.OutputValidator):
888881
return False
889882

890883
submissions = problem.submissions()

bin/run.py

+9-13
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,12 @@ def _prepare_nextpass(self, nextpass):
217217

218218
def _validate_output(self, bar):
219219
output_validators = self.problem.validators(validate.OutputValidator)
220-
if len(output_validators) != 1:
220+
if not output_validators:
221221
return None
222-
validator = output_validators[0]
223-
224-
return validator.run(
225-
self.testcase, self, args=self.testcase.testdata_yaml_validator_args(validator, bar)
222+
return output_validators[0].run(
223+
self.testcase,
224+
self,
225+
args=self.testcase.testdata_yaml_validator_args(output_validators[0], bar),
226226
)
227227

228228

@@ -522,10 +522,8 @@ def test(self):
522522

523523
testcases = self.problem.testcases(needans=False)
524524

525-
if self.problem.interactive:
526-
output_validators = self.problem.validators(validate.OutputValidator)
527-
if output_validators is False:
528-
return
525+
if not self.problem.validators(validate.OutputValidator):
526+
return
529527

530528
for testcase in testcases:
531529
header = ProgressBar.action("Running " + str(self.name), testcase.name)
@@ -589,10 +587,8 @@ def test(self):
589587

590588
# Run the submission using stdin as input.
591589
def test_interactive(self):
592-
if self.problem.interactive:
593-
output_validators = self.problem.validators(validate.OutputValidator)
594-
if output_validators is False:
595-
return
590+
if not self.problem.validators(validate.OutputValidator):
591+
return
596592

597593
bar = ProgressBar("Running " + str(self.name), max_len=1, count=1)
598594
bar.start()

bin/skel.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ def new_problem():
243243
variables,
244244
exist_ok=True,
245245
preserve_symlinks=preserve_symlinks,
246-
skip=[skeldir / "output_validators"] if not custom_output else None,
246+
skip=[skeldir / "output_validator"] if not custom_output else None,
247247
)
248248

249249
# Warn about missing problem statement skeletons for non-en languages

bin/stats.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def problem_stats(problems):
5353
("sol", str(latex.PdfType.SOLUTION.path("*")), 1),
5454
(" val: I", ["input_validators/*", "input_format_validators/*"]),
5555
("A", ["answer_validators/*"]),
56-
("O", ["output_validators/*"]),
56+
("O", ["output_validator/"]),
5757
(
5858
" sample",
5959
[lambda s: {x.stem for x in s if x.parts[2] == "sample"}],

bin/testcase.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def validator_hashes(self, cls: type["validate.AnyValidator"], bar):
150150
indicating which validators will be run for this testcase.
151151
"""
152152
assert cls in [validate.InputValidator, validate.AnswerValidator, validate.OutputValidator]
153-
validators = self.problem.validators(cls) or []
153+
validators = self.problem.validators(cls)
154154

155155
d = dict()
156156

0 commit comments

Comments
 (0)