Skip to content

Commit 027ff31

Browse files
mzuennimpsijm
andcommitted
Add .download samples and ans_is_output flag (#436)
* partially implement draft for samples * only set out path if necessary * fix missing key * only check necessary .out files * use outpath if possible * some types * use string type name... * fix union type? * fix typing * drop .out support * dont warn here * add .out support * fix tests * made ans=out assumption optional * fix code * allow ans validators for interactive and multipass problems * add missing validator * properly handle samples in export * properly handle samples in export * properly handle samples in export * allow more answer validators * properly find testcases * [doc] Improve grammar in documentation * [validate] Replace import of Union with string type hint * hide A stat for interactive problems * dont always create empty ans files * add comment * rename * fix samples * only drop known suffixes * simplify code * add more tests * allow standalone in.statement * removed outdated assert * removed outdated if * undo namechange * update files * remove wip file * [export] bt samplezip: check for duplicate files from attachments/ * [validate] Skip sanity checks for empty .ans files for interactive problems * [test] samplezip/zip: assert that the correct samples are in the zip files * [test] Add samples for constants problem * [export] build_problem_zip: Make sure that .*.download files also end up in the zip * improve warning * [problem] Problem._samples: split warning message for has_raw over multiple lines * [export] Simplify getting of all samples: .interaction is included in KNOWN_DATA_EXTENSIONS * [generate] For interactive and/or multi-pass samples, allow .in.download and .interaction when both .in and .in.statement are missing Also generate empty .ans.statement or .ans.download files if they don't exist yet. * simplify code * i hate python tuples * [generate] generate_empty_interactive_sample_ans: stop when .ans file exists * [generate] Move generate_empty_interactive_sample_ans to later step * generators.cue: Add '{in,ans}.{statement,download}' to #testcase * [test] Fix test_schemata.sh: run from correct directory, replace {%placeholders%} * [test] test_schemata.sh: Skip empty snippets for now * [generate] Allow writing empty hardcoded files Kinda ugly, but should be caught by validators and sanity checks anyway, so having the check here should™ be redundant. This does allow writing empty .{in,ans}.{statement,download} files, which are _not_ sanity-checked. --------- Co-authored-by: Maarten Sijm <[email protected]>
1 parent d0c3d80 commit 027ff31

File tree

28 files changed

+543
-297
lines changed

28 files changed

+543
-297
lines changed

bin/config.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,19 @@
6161
".pdf",
6262
]
6363

64+
KNOWN_SAMPLE_TESTCASE_EXTENSIONS: Final[Sequence[str]] = [
65+
".in.statement",
66+
".ans.statement",
67+
".in.download",
68+
".ans.download",
69+
]
70+
6471
KNOWN_TEXT_DATA_EXTENSIONS: Final[Sequence[str]] = [
6572
*KNOWN_TESTCASE_EXTENSIONS,
73+
*KNOWN_SAMPLE_TESTCASE_EXTENSIONS,
6674
".interaction",
6775
".hint",
6876
".desc",
69-
".in.statement",
70-
".ans.statement",
7177
#'.args',
7278
]
7379

bin/constraints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def check_validators(problem):
2323
if not in_constraints:
2424
warn("No constraint validation of input values found in input validators.")
2525
problem.validate_data(validate.Mode.ANSWER, constraints=ans_constraints)
26-
if not problem.interactive and not problem.multi_pass and not ans_constraints:
26+
if not problem.settings.ans_is_output and not ans_constraints:
2727
log("No constraint validation of answer values found in answer or output validators.")
2828
print()
2929

bin/export.py

Lines changed: 55 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def remove_language_suffix(fname, statement_language):
3333
return out
3434

3535

36-
def build_samples_zip(problems, output, statement_language):
36+
def build_samples_zip(problems: list[Problem], output: Path, statement_language: str):
3737
zf = zipfile.ZipFile(output, mode="w", compression=zipfile.ZIP_DEFLATED, allowZip64=False)
3838

3939
# Do not include contest PDF for kattis.
@@ -47,42 +47,51 @@ def build_samples_zip(problems, output, statement_language):
4747
)
4848

4949
for problem in problems:
50+
if not problem.label:
51+
fatal(f"Cannot create samples zip: Problem {problem.name} does not have a label!")
52+
5053
outputdir = Path(problem.label)
5154

5255
attachments_dir = problem.path / "attachments"
5356
if (problem.interactive or problem.multi_pass) and not attachments_dir.is_dir():
54-
interactive = "interactive " if problem.interactive else ""
55-
multi_pass = "multi-pass " if problem.multi_pass else ""
5657
util.error(
57-
f"{interactive}{multi_pass}problem {problem.name} does not have an attachments/ directory."
58+
f"{problem.settings.type_name()} problem {problem.name} does not have an attachments/ directory."
5859
)
5960
continue
6061

61-
empty = True
62+
contents: dict[Path, Path] = {} # Maps desination to source, to allow checking duplicates.
63+
64+
# Add samples.
65+
samples = problem.download_samples()
66+
for i, (in_file, ans_file) in enumerate(samples):
67+
base_name = outputdir / str(i + 1)
68+
contents[base_name.with_suffix(".in")] = in_file
69+
if ans_file.stat().st_size > 0:
70+
contents[base_name.with_suffix(".ans")] = ans_file
6271

6372
# Add attachments if they exist.
6473
if attachments_dir.is_dir():
6574
for f in attachments_dir.iterdir():
6675
if f.is_dir():
6776
util.error(f"{f} directory attachments are not yet supported.")
6877
elif f.is_file() and f.exists():
69-
zf.write(f, outputdir / f.name)
70-
empty = False
78+
destination = outputdir / f.name
79+
if destination in contents:
80+
util.error(
81+
f"Cannot overwrite {destination} from attachments/"
82+
+ f" (sourced from {contents[destination]})."
83+
+ "\n\tDo not include samples in attachments/,"
84+
+ " use .{in,ans}.statement or .{in,ans}.download instead."
85+
)
86+
else:
87+
contents[destination] = f
7188
else:
7289
util.error(f"Cannot include broken file {f}.")
7390

74-
# Add samples for non-interactive and non-multi-pass problems.
75-
if not problem.interactive and not problem.multi_pass:
76-
samples = problem.testcases(only_samples=True)
77-
if samples:
78-
for i in range(0, len(samples)):
79-
sample = samples[i]
80-
basename = outputdir / str(i + 1)
81-
zf.write(sample.in_path, basename.with_suffix(".in"))
82-
zf.write(sample.ans_path, basename.with_suffix(".ans"))
83-
empty = False
84-
85-
if empty:
91+
if contents:
92+
for destination, source in contents.items():
93+
zf.write(source, destination)
94+
else:
8695
util.error(f"No attachments or samples found for problem {problem.name}.")
8796

8897
zf.close()
@@ -107,22 +116,8 @@ def build_problem_zip(problem: Problem, output: Path):
107116
("attachments/**/*", problem.interactive or problem.multi_pass),
108117
]
109118

110-
testcases = [
111-
("data/secret/**/*.in", True),
112-
("data/sample/**/*.in", not problem.interactive and not problem.multi_pass),
113-
]
114-
115-
if problem.interactive or problem.multi_pass:
116-
# .interaction files don't need a corresponding .in
117-
# therefore we can handle them like all other files
118-
files += [("data/sample/**/*.interaction", False)]
119-
120119
if not config.args.kattis:
121-
files += [
122-
(f"problem.{statement_language}.pdf", True),
123-
("data/sample/**/*.in.statement", False),
124-
("data/sample/**/*.ans.statement", False),
125-
]
120+
files.append((f"problem.{statement_language}.pdf", True))
126121

127122
if problem.custom_output:
128123
files.append(("output_validator/**/*", True))
@@ -141,7 +136,7 @@ def build_problem_zip(problem: Problem, output: Path):
141136
export_dir /= problem.name
142137
export_dir.mkdir(parents=True, exist_ok=True)
143138

144-
def add_file(path, source):
139+
def add_file(path: Path, source: Path):
145140
path = export_dir / path
146141
path.parent.mkdir(parents=True, exist_ok=True)
147142
ensure_symlink(path, source)
@@ -158,21 +153,32 @@ def add_file(path, source):
158153
out = remove_language_suffix(out, statement_language)
159154
add_file(out, f)
160155

161-
# Include all testcases (specified by a .in file) and copy all related files
162-
for pattern, required in testcases:
163-
paths = list(util.glob(problem.path, pattern))
164-
if required and len(paths) == 0:
165-
util.error(f"No matches for required path {pattern}.")
166-
for f in paths:
156+
def add_testcase(in_file: Path):
157+
base_name = util.drop_suffix(in_file, [".in", ".in.statement", ".in.download"])
158+
for ext in config.KNOWN_DATA_EXTENSIONS:
159+
f = base_name.with_suffix(ext)
167160
if f.is_file():
168-
if not f.with_suffix(".ans").is_file():
169-
util.warn(f"No answer file found for {f}, skipping.")
170-
else:
171-
for ext in config.KNOWN_DATA_EXTENSIONS:
172-
f2 = f.with_suffix(ext)
173-
if f2.is_file():
174-
out = f2.relative_to(problem.path)
175-
add_file(out, f2)
161+
out = f.relative_to(problem.path)
162+
add_file(out, f)
163+
164+
# Include all sample test cases and copy all related files.
165+
samples = problem.download_samples()
166+
if len(samples) == 0:
167+
util.error("No samples found.")
168+
for in_file, _ in samples:
169+
add_testcase(in_file)
170+
171+
# Include all secret test cases and copy all related files.
172+
pattern = "data/secret/**/*.in"
173+
paths = util.glob(problem.path, pattern)
174+
if len(paths) == 0:
175+
util.error(f"No secret test cases found in {pattern}.")
176+
for f in paths:
177+
if f.is_file():
178+
if f.with_suffix(".ans").is_file():
179+
add_testcase(f)
180+
else:
181+
util.warn(f"No answer file found for {f}, skipping.")
176182

177183
# DOMjudge and Kattis do not support 2023-07-draft yet.
178184
# TODO: Remove once they do.

0 commit comments

Comments
 (0)