Skip to content

Commit 9c6059d

Browse files
authored
Merge branch 'main' into local-time-multiplier
2 parents b9158e4 + f7a9269 commit 9c6059d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2599
-1415
lines changed

.pre-commit-config.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ repos:
99
hooks:
1010
- id: ruff
1111
args: [ --fix ]
12+
- id: ruff
13+
files: ^bin/.*\.py$
14+
args: ["--select=I", "--fix"]
1215
- id: ruff-format
1316
- repo: https://github.com/pre-commit/mirrors-mypy
1417
rev: v1.15.0
@@ -30,5 +33,5 @@ repos:
3033
- --no-incremental # Fixes ruamel.yaml, see https://stackoverflow.com/a/65223004
3134
- --python-version=3.10
3235
- --scripts-are-modules
33-
#- --strict # TODO #102: Enable flag once everything has type annotations
34-
exclude: ^test/
36+
- --strict
37+
exclude: ^(test|skel|scripts|bin/misc)/

bin/check_testing_tool.py

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import shutil
2+
import sys
3+
from collections.abc import Sequence
4+
from pathlib import Path
5+
from typing import Optional, TYPE_CHECKING
6+
7+
import config
8+
import parallel
9+
from program import Program
10+
from run import Submission
11+
from util import (
12+
command_supports_memory_limit,
13+
default_exec_code_map,
14+
ensure_symlink,
15+
error,
16+
ExecResult,
17+
ExecStatus,
18+
ProgressBar,
19+
)
20+
21+
if TYPE_CHECKING: # Prevent circular import: https://stackoverflow.com/a/39757388
22+
from problem import Problem
23+
24+
"""DISCLAIMER:
25+
26+
This tool was only made to check testing tools faster.
27+
You should still carefully review the code of the testing tool.
28+
29+
For this tool to work the following things must hold:
30+
- the testing tool must be found under `attachments/testing_tool.<ext>`
31+
- the testing tool must be callable as `{program} -f {in_path} {submission program}`
32+
- the testing tool must accept the downloadable samples as well as those found under
33+
`data/testing_tool_test/` as input files
34+
- the testing tool must exits with a non zero exit code if something goes wrong
35+
- the testing tool must not change the working directory
36+
"""
37+
38+
39+
class TestInput:
40+
def __init__(self, problem: "Problem", in_path: Path, short_path: Path) -> None:
41+
assert in_path.suffix in [".in", ".download", ".statement"]
42+
self.problem = problem
43+
self.in_path = in_path
44+
self.short_path = short_path
45+
if self.short_path.suffix in [".download", ".statement"]:
46+
ext = self.short_path.suffix
47+
name = self.short_path.with_suffix("")
48+
assert name.suffix in [".in"]
49+
self.name = str(name.with_suffix(ext))
50+
else:
51+
self.name = str(self.short_path.with_suffix(""))
52+
53+
54+
class WrappedSubmission:
55+
def __init__(self, problem: "Problem", submission: Submission) -> None:
56+
self.problem = problem
57+
self.submission = submission
58+
self.name = submission.name
59+
self.tmpdir = (
60+
problem.tmpdir / "testing_tool" / submission.tmpdir.relative_to(problem.tmpdir)
61+
)
62+
self.tmpdir.mkdir(parents=True, exist_ok=True)
63+
self.run_command: Optional[list[Path | str]] = None
64+
65+
def supports_memory_limit(self) -> bool:
66+
assert self.run_command is not None
67+
assert self.submission.run_command is not None
68+
return command_supports_memory_limit(self.run_command) and command_supports_memory_limit(
69+
self.submission.run_command
70+
)
71+
72+
def _wrapper_script(self) -> str:
73+
assert self.submission.run_command is not None
74+
args = ", ".join(map(repr, self.submission.run_command))
75+
# script assumes that the working directory is not changed
76+
script = """#!/usr/bin/env python3
77+
import subprocess
78+
import sys
79+
from pathlib import Path
80+
81+
result = subprocess.run(
82+
[{args}],
83+
stdout=sys.stdout,
84+
stderr=sys.stderr,
85+
stdin=sys.stdin,
86+
)
87+
returncode_file = Path(".returncode")
88+
# For multipass we store the first non zero return code
89+
write_returncode = True
90+
if returncode_file.is_file():
91+
raw = returncode_file.read_text()
92+
try:
93+
if int(raw) != 0:
94+
write_returncode = False
95+
except ValueError:
96+
pass
97+
if write_returncode:
98+
returncode_file.write_text(f"{result.returncode}\\n")
99+
sys.exit(result.returncode)
100+
"""
101+
return script.replace("{args}", args)
102+
103+
def build(self) -> None:
104+
wrapper_file = self.tmpdir / "wrapper.py"
105+
wrapper_file.write_text(self._wrapper_script())
106+
self.run_command = [sys.executable, wrapper_file]
107+
108+
def run(self, bar: ProgressBar, testing_tool: "TestingTool", testinput: TestInput) -> bool:
109+
assert self.run_command is not None
110+
rundir = self.tmpdir / testinput.short_path
111+
if rundir.is_file():
112+
rundir.unlink()
113+
elif rundir.exists():
114+
shutil.rmtree(rundir)
115+
rundir.mkdir(exist_ok=True, parents=True)
116+
117+
returncode_file = rundir / ".returncode"
118+
in_path = rundir / "testcase.in"
119+
ensure_symlink(in_path, testinput.in_path)
120+
121+
localbar = bar.start(testinput)
122+
123+
result = testing_tool.run(in_path, self)
124+
submission_returncode = None
125+
submission_status = None
126+
if returncode_file.is_file():
127+
raw = returncode_file.read_text()
128+
try:
129+
submission_returncode = int(raw)
130+
submission_status = default_exec_code_map(submission_returncode)
131+
except ValueError:
132+
pass
133+
ok = bool(result.status) and bool(submission_status)
134+
135+
message = []
136+
if result.status == ExecStatus.TIMEOUT:
137+
message.append("TIMEOUT")
138+
elif not result.status:
139+
message.append(f"Testing Tool exit code: {result.returncode}")
140+
if (
141+
submission_status is not None
142+
and not submission_status
143+
and submission_status != ExecStatus.TIMEOUT
144+
):
145+
message.append(f"Submission exit code: {submission_returncode}")
146+
if not message:
147+
message.append("OK")
148+
149+
data = ""
150+
if result.out and result.err:
151+
data = (
152+
"TESTING TOOL STDERR:"
153+
+ localbar._format_data(result.err)
154+
+ "\nTESTING TOOL STDOUT:"
155+
+ localbar._format_data(result.out)
156+
+ "\n"
157+
)
158+
elif result.err:
159+
data = result.err
160+
elif result.out:
161+
data = result.out
162+
163+
localbar.done(ok, ", ".join(message), data)
164+
return ok
165+
166+
167+
class TestingTool(Program):
168+
def __init__(self, problem: "Problem", path: Path) -> None:
169+
super().__init__(
170+
problem,
171+
path,
172+
"testing_tool",
173+
limits={
174+
"timeout": problem.limits.timeout,
175+
"memory": problem.limits.memory,
176+
},
177+
)
178+
179+
def run(self, in_path: Path, submission: WrappedSubmission) -> ExecResult:
180+
assert self.run_command is not None
181+
assert submission.run_command is not None
182+
exec_res = self._exec_command(
183+
[*self.run_command, "-f", in_path, *submission.run_command],
184+
cwd=in_path.parent,
185+
crop=True,
186+
memory=self.limits["memory"] if submission.supports_memory_limit() else None,
187+
)
188+
return exec_res
189+
190+
191+
def run(
192+
problem: "Problem", testinputs: Sequence[TestInput], submissions: Sequence[Submission]
193+
) -> bool:
194+
wrapped_submissions = [WrappedSubmission(problem, submission) for submission in submissions]
195+
for submission in wrapped_submissions:
196+
submission.build()
197+
198+
tool_dir = problem.path / "attachments" / "testing_tool"
199+
tool_files = list((problem.path / "attachments").glob("testing_tool.*"))
200+
if (tool_dir.is_dir() and tool_files) or len(tool_files) > 1:
201+
error("Multiple testing tools found!")
202+
return False
203+
elif not tool_dir.is_dir() and not tool_files:
204+
error("No testing tool found!")
205+
return False
206+
207+
if tool_dir.is_dir():
208+
testing_tool = TestingTool(problem, tool_dir)
209+
else:
210+
testing_tool = TestingTool(problem, tool_files[0])
211+
212+
bar = ProgressBar("Building testing tool", items=[testing_tool])
213+
localbar = bar.start(testing_tool)
214+
if not testing_tool.build(bar):
215+
localbar.done(False)
216+
return False
217+
localbar.done()
218+
bar.finalize(print_done=False)
219+
220+
ok = True
221+
222+
max_submission_len = max([len(x.name) for x in wrapped_submissions])
223+
max_testinput_len = max(len(x.name) for x in testinputs)
224+
225+
# When True, the ProgressBar will print a newline before the first error log.
226+
needs_leading_newline = False if config.args.verbose else True
227+
for submission in wrapped_submissions:
228+
bar = ProgressBar(
229+
submission.name,
230+
count=len(testinputs),
231+
max_len=max_testinput_len + max_submission_len - len(submission.name),
232+
needs_leading_newline=needs_leading_newline,
233+
)
234+
cur_ok = True
235+
236+
def run_submission(testinput: TestInput) -> None:
237+
nonlocal cur_ok
238+
# skip after first error
239+
if not cur_ok and not config.args.all:
240+
bar.skip()
241+
return
242+
if not submission.run(bar, testing_tool, testinput):
243+
# just writing False is thread safe
244+
cur_ok = False
245+
246+
parallel.run_tasks(run_submission, testinputs, pin=True)
247+
ok &= cur_ok
248+
needs_leading_newline = bar.finalize()
249+
250+
return ok

0 commit comments

Comments
 (0)