cf-tool-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cf_tool/__init__.py +3 -0
- cf_tool/__main__.py +6 -0
- cf_tool/build.py +211 -0
- cf_tool/cli/__init__.py +117 -0
- cf_tool/cli/account.py +32 -0
- cf_tool/cli/common.py +262 -0
- cf_tool/cli/completion.py +226 -0
- cf_tool/cli/configure.py +386 -0
- cf_tool/cli/problems.py +399 -0
- cf_tool/cli/setup.py +195 -0
- cf_tool/cli/submitting.py +309 -0
- cf_tool/cli/testing.py +354 -0
- cf_tool/codeforces/__init__.py +3 -0
- cf_tool/codeforces/browser.py +346 -0
- cf_tool/codeforces/client.py +521 -0
- cf_tool/codeforces/crypto.py +41 -0
- cf_tool/codeforces/gateway.py +288 -0
- cf_tool/codeforces/http.py +213 -0
- cf_tool/codeforces/pages.py +767 -0
- cf_tool/codeforces/session.py +103 -0
- cf_tool/doctor.py +124 -0
- cf_tool/errors.py +37 -0
- cf_tool/judge.py +481 -0
- cf_tool/languages.py +1195 -0
- cf_tool/listen.py +178 -0
- cf_tool/paths.py +57 -0
- cf_tool/process.py +431 -0
- cf_tool/settings.py +386 -0
- cf_tool/stress.py +167 -0
- cf_tool/ui.py +455 -0
- cf_tool/views.py +306 -0
- cf_tool/workspace.py +777 -0
- cf_tool_cli-1.0.0.dist-info/METADATA +106 -0
- cf_tool_cli-1.0.0.dist-info/RECORD +38 -0
- cf_tool_cli-1.0.0.dist-info/WHEEL +5 -0
- cf_tool_cli-1.0.0.dist-info/entry_points.txt +2 -0
- cf_tool_cli-1.0.0.dist-info/licenses/LICENSE +48 -0
- cf_tool_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Commands for submissions: submit, status, pull, clone."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from cf_tool import judge, languages, ui, views, workspace
|
|
11
|
+
from cf_tool.cli.common import (
|
|
12
|
+
cf_command,
|
|
13
|
+
client,
|
|
14
|
+
code_file_among,
|
|
15
|
+
ensure_login,
|
|
16
|
+
problem_folder_of,
|
|
17
|
+
settings,
|
|
18
|
+
target_of,
|
|
19
|
+
)
|
|
20
|
+
from cf_tool.cli.completion import complete_problems, complete_problems_and_files
|
|
21
|
+
from cf_tool.errors import CFError
|
|
22
|
+
from cf_tool.workspace import Target
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _label(contest: str, problem: str) -> str:
|
|
26
|
+
"""2050A, problem A or contest 2050: what a file name points at."""
|
|
27
|
+
if contest and problem:
|
|
28
|
+
return f"{contest}{problem}"
|
|
29
|
+
return f"problem {problem}" if problem else f"contest {contest}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _submit_target(
|
|
33
|
+
target: Target, filename: str, specs: tuple[str, ...], file_given: bool
|
|
34
|
+
) -> Target:
|
|
35
|
+
"""The problem to submit filename to: target, its gaps filled in from what
|
|
36
|
+
the file's name says. Both a file named for another problem and a target
|
|
37
|
+
read off the name need a yes first. Off a terminal nobody can say yes
|
|
38
|
+
(CP Editor calls cf so): a file and a problem both named are taken as
|
|
39
|
+
meant, anything else is refused."""
|
|
40
|
+
contest, problem = workspace.named_target(filename, Path.cwd())
|
|
41
|
+
clash = (target.contest and contest and contest != target.contest) or (
|
|
42
|
+
target.problem and problem and problem != target.problem
|
|
43
|
+
)
|
|
44
|
+
if clash:
|
|
45
|
+
looks = f"{filename} looks like {_label(contest, problem)}"
|
|
46
|
+
if not ui.interactive():
|
|
47
|
+
if file_given and specs:
|
|
48
|
+
ui.yellow(f"{looks}; submitting it to {target.label} as asked")
|
|
49
|
+
return target
|
|
50
|
+
raise CFError(f"{looks}, not {target.label}: name both")
|
|
51
|
+
question = f"{looks}. Submit it to {target.label}?"
|
|
52
|
+
if not ui.confirm(question, default=False):
|
|
53
|
+
raise CFError("Not submitted")
|
|
54
|
+
return target
|
|
55
|
+
missing = [
|
|
56
|
+
value
|
|
57
|
+
for value, known in ((contest, target.contest), (problem, target.problem))
|
|
58
|
+
if value and not known
|
|
59
|
+
]
|
|
60
|
+
if not missing:
|
|
61
|
+
return target
|
|
62
|
+
target = target_of(specs + tuple(missing))
|
|
63
|
+
if not ui.interactive():
|
|
64
|
+
raise CFError(f"Which problem? Name it: cf submit {target.label} {filename}")
|
|
65
|
+
if not ui.confirm(f"Submit {filename} to {target.label}?", default=True):
|
|
66
|
+
raise CFError("Not submitted")
|
|
67
|
+
return target
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _language_chooser(key: str, path: Path, asked: str):
|
|
71
|
+
"""choose_language for Client.submit: the language asked for (-l) or set
|
|
72
|
+
for the file's language, else the family's best; when the form offers
|
|
73
|
+
none of those, the user picks one of the family (and may keep it)."""
|
|
74
|
+
|
|
75
|
+
def choose(offered: dict[str, str]) -> tuple[str, str]:
|
|
76
|
+
conf = settings().language_settings(key)
|
|
77
|
+
spec = asked or conf.submit
|
|
78
|
+
chosen = languages.choose_submit(key, spec, offered)
|
|
79
|
+
if chosen is not None:
|
|
80
|
+
return chosen
|
|
81
|
+
options = languages.family(key, offered)
|
|
82
|
+
if not options:
|
|
83
|
+
wanted = spec or languages.display_name(key)
|
|
84
|
+
raise CFError(f"Codeforces doesn't offer {wanted} here, nor anything like it")
|
|
85
|
+
old = languages.known_languages().get(spec, spec) if spec else languages.display_name(key)
|
|
86
|
+
if not ui.interactive(): # NOTE: nobody to ask: the family's best, this once
|
|
87
|
+
ui.yellow(f"Codeforces doesn't offer {old} here: submitting as {options[0][1]}")
|
|
88
|
+
return options[0]
|
|
89
|
+
lang_id = ui.select(
|
|
90
|
+
f"Codeforces doesn't offer {old} here. Submit {path.name} as:",
|
|
91
|
+
[(name, lid) for lid, name in options] + [("Don't submit", None)],
|
|
92
|
+
default=options[0][0],
|
|
93
|
+
)
|
|
94
|
+
if lang_id is None:
|
|
95
|
+
raise CFError("Not submitted")
|
|
96
|
+
if not asked and ui.confirm(
|
|
97
|
+
f"Submit {languages.display_name(key)} as {offered[lang_id]} from now on?", default=True
|
|
98
|
+
):
|
|
99
|
+
settings().update("submit", lang_id, table=key)
|
|
100
|
+
return lang_id, offered[lang_id]
|
|
101
|
+
|
|
102
|
+
return choose
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@click.command()
|
|
106
|
+
@click.argument(
|
|
107
|
+
"args", nargs=-1, metavar="[PROBLEM] [FILE]", shell_complete=complete_problems_and_files
|
|
108
|
+
)
|
|
109
|
+
@click.option(
|
|
110
|
+
"-f", "--file", "file_arg", type=click.Path(dir_okay=False), help="The code file to submit."
|
|
111
|
+
)
|
|
112
|
+
@click.option(
|
|
113
|
+
"-l",
|
|
114
|
+
"--lang",
|
|
115
|
+
"language",
|
|
116
|
+
metavar="NAME|ID",
|
|
117
|
+
default="",
|
|
118
|
+
help="The Codeforces language, this once.",
|
|
119
|
+
)
|
|
120
|
+
@cf_command
|
|
121
|
+
def submit(args: tuple[str, ...], file_arg: str | None, language: str) -> None:
|
|
122
|
+
"""Submit your code, then show the verdict as it comes.
|
|
123
|
+
|
|
124
|
+
\b
|
|
125
|
+
cf submit the code of the problem here
|
|
126
|
+
cf submit b problem b of this contest
|
|
127
|
+
cf submit 2050b a.cpp this file to that problem
|
|
128
|
+
With several code files around, you pick one (Esc cancels).
|
|
129
|
+
"""
|
|
130
|
+
named, specs = code_file_among(args)
|
|
131
|
+
if named and file_arg:
|
|
132
|
+
raise CFError("Give the code file once: as -f or as an argument")
|
|
133
|
+
file_arg = file_arg or named
|
|
134
|
+
cwd = Path.cwd()
|
|
135
|
+
target = target_of(specs)
|
|
136
|
+
folder = problem_folder_of(target) if target.problem else workspace.find_problem_folder(cwd)
|
|
137
|
+
if file_arg:
|
|
138
|
+
filename, key = workspace.pick_code(file_arg, cwd, settings())
|
|
139
|
+
path = Path(file_arg).resolve()
|
|
140
|
+
else:
|
|
141
|
+
search = folder or cwd
|
|
142
|
+
name, key = workspace.pick_code(
|
|
143
|
+
None, search, settings(), contest=target.contest, problem=target.problem
|
|
144
|
+
)
|
|
145
|
+
path = (search / name).resolve()
|
|
146
|
+
filename = str(Path(name)) if search == cwd else _relative(path, cwd)
|
|
147
|
+
target = _submit_target(target, filename, specs, file_given=bool(file_arg))
|
|
148
|
+
failed = judge.failed_tests(path)
|
|
149
|
+
if failed and ui.interactive():
|
|
150
|
+
tests = ", ".join(failed[:3]) + (" ..." if len(failed) > 3 else "")
|
|
151
|
+
if not ui.confirm(
|
|
152
|
+
f"This code failed the test {tests} in cf test. Submit it anyway?", default=False
|
|
153
|
+
):
|
|
154
|
+
raise CFError("Not submitted")
|
|
155
|
+
c = client()
|
|
156
|
+
ensure_login(c)
|
|
157
|
+
source = workspace.read_source(path)
|
|
158
|
+
submission = c.submit(target, source, _language_chooser(key, path, language), filename=filename)
|
|
159
|
+
_after_the_verdict(c, target, submission)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
_ON_TEST_RE = re.compile(r"\bon test (\d+)") # NOTE: not "on pretest": hidden in a contest
|
|
163
|
+
_SLOW_RE = re.compile(r"\b(?:Time|Memory) limit exceeded")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _after_the_verdict(c, target: Target, submission) -> None:
|
|
167
|
+
"""What helps after a verdict other than accepted:
|
|
168
|
+
- CPython over a limit: a hint to try PyPy
|
|
169
|
+
- a failed test (once the contest is over): Codeforces' data of it"""
|
|
170
|
+
verdict, _ = submission.styled_status()
|
|
171
|
+
lang = submission.lang.lower()
|
|
172
|
+
if _SLOW_RE.search(verdict) and "python" in lang and "pypy" not in lang:
|
|
173
|
+
ui.cyan(
|
|
174
|
+
"PyPy usually runs the same Python code several times faster, and keeps lists "
|
|
175
|
+
"of numbers smaller: cf submit -l pypy"
|
|
176
|
+
)
|
|
177
|
+
if _ON_TEST_RE.search(verdict):
|
|
178
|
+
_show_failed_test(c, target, submission.id)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _show_failed_test(c, target: Target, submission_id: int, details=None) -> None:
|
|
182
|
+
"""Codeforces' data of the test a submission failed (details, when
|
|
183
|
+
already fetched), shown, and kept among the problem's tests when it is
|
|
184
|
+
all there."""
|
|
185
|
+
if details is None:
|
|
186
|
+
try:
|
|
187
|
+
details = c.details(target, submission_id)
|
|
188
|
+
except CFError as e:
|
|
189
|
+
ui.dim(f"Cannot get the failed test's data ({e})")
|
|
190
|
+
return
|
|
191
|
+
if details is None or details.failed is None:
|
|
192
|
+
return
|
|
193
|
+
failed = details.failed
|
|
194
|
+
ui.console.print(views.failed_test(failed))
|
|
195
|
+
folder = problem_folder_of(target) if target.problem else None
|
|
196
|
+
if failed.cut or folder is None:
|
|
197
|
+
return
|
|
198
|
+
saved = workspace.add_test(folder, failed.input, failed.answer, name=f"cf-{failed.number}")
|
|
199
|
+
ui.green(f"Saved it as the test {saved.name}: cf test {saved.name} runs it again")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _relative(path: Path, cwd: Path) -> str:
|
|
203
|
+
try:
|
|
204
|
+
return str(path.relative_to(cwd.resolve()))
|
|
205
|
+
except ValueError:
|
|
206
|
+
return str(path)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _submission_id(spec: str) -> int | None:
|
|
210
|
+
"""The submission an argument names (396876324, or its URL); None for
|
|
211
|
+
anything else."""
|
|
212
|
+
last = spec.split("?")[0].rstrip("/").split("/")[-1]
|
|
213
|
+
return int(last) if re.fullmatch(r"\d{7,}", last) else None # NOTE: a contest: 6 at most
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@click.command()
|
|
217
|
+
@click.argument(
|
|
218
|
+
"specs", nargs=-1, metavar="[PROBLEM|CONTEST|SUBMISSION]", shell_complete=complete_problems
|
|
219
|
+
)
|
|
220
|
+
@click.option("--all", "show_all", is_flag=True, help="All of them, not the last 10.")
|
|
221
|
+
@cf_command
|
|
222
|
+
def status(specs: tuple[str, ...], show_all: bool) -> None:
|
|
223
|
+
"""Your submissions to the contest, refreshed until they're judged.
|
|
224
|
+
|
|
225
|
+
\b
|
|
226
|
+
cf status your latest submissions to the contest here
|
|
227
|
+
cf status 2050b to problem 2050B
|
|
228
|
+
cf status 396876324 that submission, with the test it failed (by id or URL)
|
|
229
|
+
"""
|
|
230
|
+
if "all" in specs: # NOTE: cf-tool 0.3's cf watch all
|
|
231
|
+
show_all, specs = True, tuple(s for s in specs if s != "all")
|
|
232
|
+
submissions = [n for n in map(_submission_id, specs) if n is not None]
|
|
233
|
+
if submissions:
|
|
234
|
+
_status_of_one(specs, submissions[0])
|
|
235
|
+
return
|
|
236
|
+
target = target_of(specs)
|
|
237
|
+
c = client()
|
|
238
|
+
ensure_login(c)
|
|
239
|
+
c.watch(target, count=-1 if show_all else 10, problem=target.problem)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _status_of_one(specs: tuple[str, ...], submission_id: int) -> None:
|
|
243
|
+
"""One submission of yours: its verdict and, once the contest is over,
|
|
244
|
+
the test it failed."""
|
|
245
|
+
c = client()
|
|
246
|
+
ensure_login(c)
|
|
247
|
+
urls = [s for s in specs if "/" in s]
|
|
248
|
+
target = target_of(urls) if urls else c.find_submission(submission_id)
|
|
249
|
+
if target is None or not target.contest:
|
|
250
|
+
raise CFError(
|
|
251
|
+
f"Submission {submission_id} isn't among your latest ones: give its URL "
|
|
252
|
+
"(https://codeforces.com/contest/<contest>/submission/<id>)"
|
|
253
|
+
)
|
|
254
|
+
details = c.details(target, submission_id)
|
|
255
|
+
if details is None:
|
|
256
|
+
raise CFError(
|
|
257
|
+
"Codeforces shows no test data for it: only for your own submissions, "
|
|
258
|
+
"once the contest is over"
|
|
259
|
+
)
|
|
260
|
+
ui.line(("› ", "cyan bold"), (str(submission_id), "bold"), (f" {details.problem}", ""))
|
|
261
|
+
ui.line((" ", ""), (details.verdict, "bold"))
|
|
262
|
+
index = re.match(r"\((\w+)\)", details.problem) # NOTE: "(F) Tea Blend"
|
|
263
|
+
if not target.problem and index:
|
|
264
|
+
target = target.with_problem(index.group(1).upper())
|
|
265
|
+
_show_failed_test(c, target, submission_id, details)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@click.command()
|
|
269
|
+
@click.argument("specs", nargs=-1, metavar="[PROBLEM|CONTEST]", shell_complete=complete_problems)
|
|
270
|
+
@click.option("--ac", is_flag=True, help="Accepted ones only.")
|
|
271
|
+
@cf_command
|
|
272
|
+
def pull(specs: tuple[str, ...], ac: bool) -> None:
|
|
273
|
+
"""Download the code of your submissions into the problems' folders."""
|
|
274
|
+
if "ac" in specs: # NOTE: cf-tool 0.3's cf pull ac
|
|
275
|
+
ac, specs = True, tuple(s for s in specs if s != "ac")
|
|
276
|
+
target = target_of(specs)
|
|
277
|
+
c = client()
|
|
278
|
+
ensure_login(c)
|
|
279
|
+
root = workspace.get_root(Path.cwd(), settings().root_folder())
|
|
280
|
+
|
|
281
|
+
def folder_of(index: str) -> Path:
|
|
282
|
+
problem = target.with_problem(index)
|
|
283
|
+
return problem_folder_of(problem) or workspace.problem_folder(root, problem)
|
|
284
|
+
|
|
285
|
+
if not c.pull(target, folder_of, ac):
|
|
286
|
+
raise CFError("No code to download")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@click.command()
|
|
290
|
+
@click.argument("args", nargs=-1, metavar="[HANDLE]")
|
|
291
|
+
@click.option("--ac", is_flag=True, help="Accepted ones only.")
|
|
292
|
+
@cf_command
|
|
293
|
+
def clone(args: tuple[str, ...], ac: bool) -> None:
|
|
294
|
+
"""Download the code of every submission of a user (default: you)."""
|
|
295
|
+
if "ac" in args: # NOTE: cf-tool 0.3's cf clone ac
|
|
296
|
+
ac = True
|
|
297
|
+
handles = [arg for arg in args if arg != "ac"]
|
|
298
|
+
if len(handles) > 1:
|
|
299
|
+
raise CFError("One handle at a time")
|
|
300
|
+
handle = handles[0] if handles else None
|
|
301
|
+
c = client()
|
|
302
|
+
handle = handle or c.handle
|
|
303
|
+
if not handle:
|
|
304
|
+
raise CFError("Whose? Give a handle, or log in with cf login")
|
|
305
|
+
saved = c.clone(handle, Path.cwd(), ac)
|
|
306
|
+
ui.green(f"Saved {saved} file{'s' if saved != 1 else ''}")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
COMMANDS = [submit, status, pull, clone]
|
cf_tool/cli/testing.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Commands that run your code: test and stress."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from cf_tool import judge, languages, process, stress as stress_module, ui, workspace
|
|
11
|
+
from cf_tool.build import Program
|
|
12
|
+
from cf_tool.cli.common import (
|
|
13
|
+
build,
|
|
14
|
+
cf_command,
|
|
15
|
+
program,
|
|
16
|
+
settings,
|
|
17
|
+
short,
|
|
18
|
+
target_of,
|
|
19
|
+
)
|
|
20
|
+
from cf_tool.cli.completion import complete_tests
|
|
21
|
+
from cf_tool.errors import CFError
|
|
22
|
+
from cf_tool.languages import Setup
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _comparison_options(command):
|
|
26
|
+
"""The options saying how output is compared with answers (cf test, cf stress)."""
|
|
27
|
+
options = [
|
|
28
|
+
click.option(
|
|
29
|
+
"-e",
|
|
30
|
+
"--epsilon",
|
|
31
|
+
type=float,
|
|
32
|
+
help="Accept numbers within this absolute or relative error.",
|
|
33
|
+
),
|
|
34
|
+
click.option(
|
|
35
|
+
"-i", "--ignore-case", is_flag=True, help="Accept answers in any letter case."
|
|
36
|
+
),
|
|
37
|
+
click.option("--strict", is_flag=True, help="Compare trimmed lines, not just the words."),
|
|
38
|
+
click.option("--exact", is_flag=True, help="Ignore what the statement allows."),
|
|
39
|
+
click.option(
|
|
40
|
+
"--checker",
|
|
41
|
+
metavar="FILE|CMD",
|
|
42
|
+
default="",
|
|
43
|
+
help="Judge with a program, run as: CMD input output answer (exit 0: right).",
|
|
44
|
+
),
|
|
45
|
+
]
|
|
46
|
+
for option in reversed(options):
|
|
47
|
+
command = option(command)
|
|
48
|
+
return command
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def helper(folder: Path, kind: str) -> Path | None:
|
|
52
|
+
"""The problem's helper of a kind (gen, brute, checker, interactor) in
|
|
53
|
+
folder: gen.py, gen2.py, check_a.py; the newest when there are several."""
|
|
54
|
+
known = languages.code_extensions(settings())
|
|
55
|
+
found = [
|
|
56
|
+
entry
|
|
57
|
+
for entry in folder.glob("*.*")
|
|
58
|
+
if entry.suffix.lower() in known
|
|
59
|
+
and workspace.helper_kind(entry.name) == kind
|
|
60
|
+
and entry.is_file()
|
|
61
|
+
]
|
|
62
|
+
return max(found, key=lambda entry: entry.stat().st_mtime) if found else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _named_files(*specs: str) -> list[str]:
|
|
66
|
+
"""The files that FILE|CMD options name (python3 check_a.py -> check_a.py):
|
|
67
|
+
they are helpers, never the code to test."""
|
|
68
|
+
named = []
|
|
69
|
+
for spec in filter(None, specs):
|
|
70
|
+
words = [spec] if Path(spec).is_file() else process.split_command(spec)
|
|
71
|
+
named += [str(Path(word).resolve()) for word in words if Path(word).is_file()]
|
|
72
|
+
return named
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _program_or_command(spec: str, folder: Path) -> Program:
|
|
76
|
+
"""A code file (built like any other), or a command line to run as it is."""
|
|
77
|
+
path = Path(spec)
|
|
78
|
+
if path.is_file() and languages.language_key(path, settings()) is not None:
|
|
79
|
+
return program(path.resolve())
|
|
80
|
+
words = process.split_command(spec)
|
|
81
|
+
if not words:
|
|
82
|
+
raise CFError(f"Nothing to run in {spec!r}")
|
|
83
|
+
# NOTE: it runs in the problem's folder (a program's own), even when the
|
|
84
|
+
# \ command starts with a path to elsewhere (/usr/bin/python3 check.py)
|
|
85
|
+
source = folder / Path(words[0]).name
|
|
86
|
+
return Program(source, Setup("command", words[0], spec, None, tuple(words)))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _checker(spec: str, folder: Path | None) -> Program | None:
|
|
90
|
+
"""The checker to judge with: the one given, else checker.* in the problem's folder."""
|
|
91
|
+
if spec:
|
|
92
|
+
return _program_or_command(spec, folder or Path.cwd())
|
|
93
|
+
found = helper(folder, "checker") if folder else None
|
|
94
|
+
return program(found) if found else None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _problem_and_tests(args: tuple[str, ...]) -> tuple[Path, str | None, list[str]]:
|
|
98
|
+
"""(the problem's folder, the code file named, the tests named) from cf
|
|
99
|
+
test's arguments, which may name a code file, a problem, and tests."""
|
|
100
|
+
cwd = Path.cwd()
|
|
101
|
+
known = languages.code_extensions(settings())
|
|
102
|
+
files = [a for a in args if Path(a).suffix.lower() in known and Path(a).is_file()]
|
|
103
|
+
if len(files) > 1:
|
|
104
|
+
raise CFError(f"One code file at a time, not {' and '.join(files)}")
|
|
105
|
+
file = files[0] if files else None
|
|
106
|
+
here = workspace.find_problem_folder(Path(file).resolve().parent if file else cwd)
|
|
107
|
+
here_tests = {t.name for t in workspace.tests_of(here)} if here else set()
|
|
108
|
+
specs, names = [], []
|
|
109
|
+
for arg in args:
|
|
110
|
+
if arg == file:
|
|
111
|
+
continue
|
|
112
|
+
if arg in here_tests or arg.isdigit():
|
|
113
|
+
names.append(arg)
|
|
114
|
+
else:
|
|
115
|
+
specs.append(arg)
|
|
116
|
+
if specs:
|
|
117
|
+
target = target_of(specs)
|
|
118
|
+
folder = workspace.locate(target, cwd, settings().root_folder())
|
|
119
|
+
if folder is None:
|
|
120
|
+
raise CFError(f"No folder for {target.label} here: cf get {target.label} sets one up")
|
|
121
|
+
elif here is not None:
|
|
122
|
+
folder = here
|
|
123
|
+
elif workspace.tests_of(cwd):
|
|
124
|
+
folder = cwd # NOTE: tests without cf-tool's metadata, cf-tool 0.3's folders
|
|
125
|
+
else:
|
|
126
|
+
raise CFError(
|
|
127
|
+
"Not in a problem's folder: cd into one made by cf get, or name the problem "
|
|
128
|
+
"(cf test 2050b)"
|
|
129
|
+
)
|
|
130
|
+
return folder, file, names
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _read_until_end() -> str:
|
|
134
|
+
return sys.stdin.read()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _add_test(folder: Path) -> workspace.Test:
|
|
138
|
+
"""Type in a new test, its input, then its answer (none: only run it)."""
|
|
139
|
+
end = "Ctrl-Z, then Enter" if sys.platform == "win32" else "Ctrl-D"
|
|
140
|
+
ui.cyan(f"The new test's input (end it with {end} on an empty line):")
|
|
141
|
+
input_text = _read_until_end()
|
|
142
|
+
if not input_text.strip():
|
|
143
|
+
raise CFError("No input: no test added")
|
|
144
|
+
ui.cyan(f"Its answer (end it with {end}; none: only run it):")
|
|
145
|
+
test = workspace.add_test(folder, input_text, _read_until_end())
|
|
146
|
+
saved = f"{test.input.name}" + (f" and {test.answer.name}" if test.answer else "")
|
|
147
|
+
ui.green(f"Added the test {test.name}: tests/{saved}")
|
|
148
|
+
return test
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@click.command()
|
|
152
|
+
@click.argument("args", nargs=-1, metavar="[PROBLEM|FILE] [TEST]...", shell_complete=complete_tests)
|
|
153
|
+
@click.option("-w", "--watch", "watch_it", is_flag=True, help="Test again whenever you save.")
|
|
154
|
+
@_comparison_options
|
|
155
|
+
@click.option("--interactor", metavar="FILE|CMD", default="", help="For an interactive problem.")
|
|
156
|
+
@click.option("--add", "add_one", is_flag=True, help="Type in a new test, then run it.")
|
|
157
|
+
@click.option(
|
|
158
|
+
"--accept", is_flag=True, help="Keep the output of tests without an answer as theirs."
|
|
159
|
+
)
|
|
160
|
+
@cf_command
|
|
161
|
+
def test(
|
|
162
|
+
args: tuple[str, ...],
|
|
163
|
+
watch_it: bool,
|
|
164
|
+
epsilon: float | None,
|
|
165
|
+
ignore_case: bool,
|
|
166
|
+
strict: bool,
|
|
167
|
+
exact: bool,
|
|
168
|
+
checker: str,
|
|
169
|
+
interactor: str,
|
|
170
|
+
add_one: bool,
|
|
171
|
+
accept: bool,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Build your code and run it on the problem's tests.
|
|
174
|
+
|
|
175
|
+
\b
|
|
176
|
+
cf test every test of the problem here
|
|
177
|
+
cf test 2 my the tests 2 and my
|
|
178
|
+
cf test b problem b of this contest
|
|
179
|
+
cf test a.cpp with this code file
|
|
180
|
+
cf test -w again whenever you save
|
|
181
|
+
The statement's rules (numbers within an error, any letter case) apply
|
|
182
|
+
on their own; the options change them.
|
|
183
|
+
"""
|
|
184
|
+
folder, file, names = _problem_and_tests(args)
|
|
185
|
+
info = workspace.load_info(folder)
|
|
186
|
+
target = (info.target if info else None) or workspace.here(folder) or workspace.Target()
|
|
187
|
+
if add_one:
|
|
188
|
+
names = [_add_test(folder).name]
|
|
189
|
+
found = workspace.tests_of(folder)
|
|
190
|
+
if not found:
|
|
191
|
+
raise CFError(
|
|
192
|
+
f"{short(folder)} has no tests: cf get downloads the samples, cf test --add adds one"
|
|
193
|
+
)
|
|
194
|
+
unknown = [name for name in names if name not in {t.name for t in found}]
|
|
195
|
+
if unknown:
|
|
196
|
+
listed = ", ".join(t.name for t in found)
|
|
197
|
+
raise CFError(f"No test {', '.join(unknown)} here (there are {listed})")
|
|
198
|
+
tests = [t for t in found if t.name in names] if names else found
|
|
199
|
+
chosen = {t.name for t in tests}
|
|
200
|
+
|
|
201
|
+
interactor_program = None
|
|
202
|
+
spec = interactor or str(helper(folder, "interactor") or "")
|
|
203
|
+
if spec:
|
|
204
|
+
interactor_program = _program_or_command(spec, folder)
|
|
205
|
+
elif info and info.interactive:
|
|
206
|
+
raise CFError(
|
|
207
|
+
"This problem is interactive: cf test runs it against an interactor. Write one "
|
|
208
|
+
"as interactor.cpp (any language) in its folder, or name one with --interactor"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if file:
|
|
212
|
+
name, key = file, languages.language_key(file, settings())
|
|
213
|
+
path = Path(file).resolve()
|
|
214
|
+
else:
|
|
215
|
+
name, key = workspace.pick_code(
|
|
216
|
+
None,
|
|
217
|
+
folder,
|
|
218
|
+
settings(),
|
|
219
|
+
contest=target.contest,
|
|
220
|
+
problem=target.problem,
|
|
221
|
+
action="test",
|
|
222
|
+
exclude=_named_files(checker, interactor),
|
|
223
|
+
)
|
|
224
|
+
path = (folder / name).resolve()
|
|
225
|
+
solution = program(path, key)
|
|
226
|
+
check_program = _checker(checker, folder)
|
|
227
|
+
|
|
228
|
+
def run_once() -> None:
|
|
229
|
+
build(solution)
|
|
230
|
+
for extra in (interactor_program, check_program):
|
|
231
|
+
if extra is not None:
|
|
232
|
+
build(extra)
|
|
233
|
+
check = judge.check_for(
|
|
234
|
+
info,
|
|
235
|
+
epsilon=epsilon,
|
|
236
|
+
ignore_case=ignore_case,
|
|
237
|
+
strict=strict,
|
|
238
|
+
exact=exact,
|
|
239
|
+
checker=check_program,
|
|
240
|
+
)
|
|
241
|
+
current = [
|
|
242
|
+
t for t in workspace.tests_of(folder) if t.name in chosen
|
|
243
|
+
] # NOTE: read again: --accept may have given answers
|
|
244
|
+
summary = judge.run_tests(
|
|
245
|
+
solution, current, info=info, check=check, accept=accept, interactor=interactor_program
|
|
246
|
+
)
|
|
247
|
+
judge.remember(path, summary)
|
|
248
|
+
stressed = [name for name in summary.failed if name.startswith("stress-")]
|
|
249
|
+
if stressed:
|
|
250
|
+
ui.dim(
|
|
251
|
+
f"{', '.join(stressed)}: saved by cf stress, with the brute force's answer "
|
|
252
|
+
"(the next cf stress updates it if the brute force changed)"
|
|
253
|
+
)
|
|
254
|
+
# NOTE: an output unlike the sample's isn't confirmed right either
|
|
255
|
+
if (summary.failed or summary.differs) and not watch_it:
|
|
256
|
+
sys.exit(1)
|
|
257
|
+
|
|
258
|
+
if not watch_it:
|
|
259
|
+
run_once()
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
def watched() -> list[Path]:
|
|
263
|
+
files = [path, *(t.input for t in tests), *(t.answer for t in tests if t.answer)]
|
|
264
|
+
for extra in (interactor_program, check_program):
|
|
265
|
+
if extra is not None:
|
|
266
|
+
files.append(extra.source)
|
|
267
|
+
return files
|
|
268
|
+
|
|
269
|
+
judge.watch(watched, run_once)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@click.command()
|
|
273
|
+
@click.argument("generator", required=False, type=click.Path(exists=True, dir_okay=False))
|
|
274
|
+
@click.argument("brute", required=False, type=click.Path(exists=True, dir_okay=False))
|
|
275
|
+
@click.argument("solution", required=False, type=click.Path(exists=True, dir_okay=False))
|
|
276
|
+
@click.option("-n", "--tests", default=1000, show_default=True, help="Stop after this many tests.")
|
|
277
|
+
@click.option("--seed", default=1, show_default=True, help="The first test's seed (then +1).")
|
|
278
|
+
@_comparison_options
|
|
279
|
+
@cf_command
|
|
280
|
+
def stress(
|
|
281
|
+
generator: str | None,
|
|
282
|
+
brute: str | None,
|
|
283
|
+
solution: str | None,
|
|
284
|
+
tests: int,
|
|
285
|
+
seed: int,
|
|
286
|
+
epsilon: float | None,
|
|
287
|
+
ignore_case: bool,
|
|
288
|
+
strict: bool,
|
|
289
|
+
exact: bool,
|
|
290
|
+
checker: str,
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Random tests until your solution and a brute force disagree; that
|
|
293
|
+
test is saved among the problem's tests.
|
|
294
|
+
|
|
295
|
+
\b
|
|
296
|
+
GENERATOR prints one test for the seed it gets as its argument; BRUTE's
|
|
297
|
+
output is the answer. They default to gen.* and brute.* in the problem's
|
|
298
|
+
folder, the solution to your code there (any language for all three).
|
|
299
|
+
"""
|
|
300
|
+
cwd = Path.cwd()
|
|
301
|
+
folder = workspace.find_problem_folder(cwd) or cwd
|
|
302
|
+
info = workspace.load_info(folder)
|
|
303
|
+
target = (info.target if info else None) or workspace.here(folder) or workspace.Target()
|
|
304
|
+
gen_path = Path(generator).resolve() if generator else helper(folder, "gen")
|
|
305
|
+
brute_path = Path(brute).resolve() if brute else helper(folder, "brute")
|
|
306
|
+
if gen_path is None:
|
|
307
|
+
raise CFError("No generator: write gen.py (cf new gen.py) or another gen.*, or name one")
|
|
308
|
+
if brute_path is None:
|
|
309
|
+
raise CFError(
|
|
310
|
+
"No brute force: write brute.cpp (cf new brute) or another brute.*, or name one"
|
|
311
|
+
)
|
|
312
|
+
if solution:
|
|
313
|
+
solution_path = Path(solution).resolve()
|
|
314
|
+
else:
|
|
315
|
+
name, _ = workspace.pick_code(
|
|
316
|
+
None,
|
|
317
|
+
folder,
|
|
318
|
+
settings(),
|
|
319
|
+
contest=target.contest,
|
|
320
|
+
problem=target.problem,
|
|
321
|
+
action="stress test",
|
|
322
|
+
exclude=(str(gen_path), str(brute_path), *_named_files(checker)),
|
|
323
|
+
)
|
|
324
|
+
solution_path = (folder / name).resolve()
|
|
325
|
+
programs = [program(gen_path), program(brute_path), program(solution_path)]
|
|
326
|
+
check_program = _checker(checker, folder)
|
|
327
|
+
for each in [*programs, *([check_program] if check_program else [])]:
|
|
328
|
+
build(each)
|
|
329
|
+
check = judge.check_for(
|
|
330
|
+
info,
|
|
331
|
+
epsilon=epsilon,
|
|
332
|
+
ignore_case=ignore_case,
|
|
333
|
+
strict=strict,
|
|
334
|
+
exact=exact,
|
|
335
|
+
checker=check_program,
|
|
336
|
+
)
|
|
337
|
+
if check.any_answer:
|
|
338
|
+
ui.yellow(
|
|
339
|
+
"This problem accepts any right answer, so your output and the brute force's "
|
|
340
|
+
"may differ and both be right: judge with a checker (check*.py or --checker)"
|
|
341
|
+
)
|
|
342
|
+
failed = stress_module.stress(
|
|
343
|
+
folder,
|
|
344
|
+
*programs,
|
|
345
|
+
tests=tests,
|
|
346
|
+
seed=seed,
|
|
347
|
+
check=check,
|
|
348
|
+
time_limit=info.time_limit if info else None,
|
|
349
|
+
)
|
|
350
|
+
if failed is not None:
|
|
351
|
+
sys.exit(1)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
COMMANDS = [test, stress]
|