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.
@@ -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]
@@ -0,0 +1,3 @@
1
+ """Everything that talks to Codeforces: the HTTP transport, the way in
2
+ (Cloudflare and logging in), the session, page parsing, and the operations
3
+ built on them."""