AtCoderStudyBooster 0.3__py3-none-any.whl → 0.3.2__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.
- atcdr/cli.py +86 -0
- atcdr/download.py +275 -242
- atcdr/generate.py +180 -198
- atcdr/login.py +136 -0
- atcdr/logout.py +27 -0
- atcdr/markdown.py +34 -30
- atcdr/open.py +37 -32
- atcdr/submit.py +302 -0
- atcdr/test.py +365 -370
- atcdr/util/fileops.py +102 -0
- atcdr/util/filetype.py +71 -71
- atcdr/util/gpt.py +102 -96
- atcdr/util/parse.py +206 -0
- atcdr/util/problem.py +94 -91
- atcdr/util/session.py +140 -0
- atcoderstudybooster-0.3.2.dist-info/METADATA +213 -0
- atcoderstudybooster-0.3.2.dist-info/RECORD +21 -0
- {atcoderstudybooster-0.3.dist-info → atcoderstudybooster-0.3.2.dist-info}/WHEEL +1 -1
- atcoderstudybooster-0.3.2.dist-info/entry_points.txt +2 -0
- atcdr/main.py +0 -40
- atcdr/util/cost.py +0 -120
- atcdr/util/execute.py +0 -63
- atcoderstudybooster-0.3.dist-info/METADATA +0 -96
- atcoderstudybooster-0.3.dist-info/RECORD +0 -17
- atcoderstudybooster-0.3.dist-info/entry_points.txt +0 -2
atcdr/test.py
CHANGED
@@ -4,435 +4,430 @@ import tempfile
|
|
4
4
|
import time
|
5
5
|
from dataclasses import dataclass, field
|
6
6
|
from enum import Enum
|
7
|
-
from typing import Dict,
|
7
|
+
from typing import Dict, List, Optional, Tuple, Union
|
8
8
|
|
9
|
-
|
10
|
-
from rich.console import
|
9
|
+
import rich_click as click
|
10
|
+
from rich.console import Group, RenderableType
|
11
|
+
from rich.live import Live
|
11
12
|
from rich.markup import escape
|
12
13
|
from rich.panel import Panel
|
14
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
|
13
15
|
from rich.rule import Rule
|
14
16
|
from rich.style import Style
|
15
17
|
from rich.syntax import Syntax
|
16
18
|
from rich.table import Table
|
17
19
|
from rich.text import Text
|
18
20
|
|
19
|
-
from atcdr.util.
|
21
|
+
from atcdr.util.fileops import add_file_selector
|
20
22
|
from atcdr.util.filetype import (
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
COMPILED_LANGUAGES,
|
24
|
+
INTERPRETED_LANGUAGES,
|
25
|
+
Lang,
|
26
|
+
detect_language,
|
27
|
+
lang2str,
|
26
28
|
)
|
29
|
+
from atcdr.util.parse import ProblemHTML
|
27
30
|
|
28
31
|
|
29
32
|
@dataclass
|
30
33
|
class TestCase:
|
31
|
-
|
32
|
-
|
34
|
+
input: str
|
35
|
+
output: str
|
33
36
|
|
34
37
|
|
35
38
|
@dataclass
|
36
39
|
class LabeledTestCase:
|
37
|
-
|
38
|
-
|
40
|
+
label: str
|
41
|
+
case: TestCase
|
39
42
|
|
40
43
|
|
41
44
|
class ResultStatus(Enum):
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
45
|
+
AC = 'Accepted'
|
46
|
+
WA = 'Wrong Answer'
|
47
|
+
TLE = 'Time Limit Exceeded'
|
48
|
+
MLE = 'Memory Limit Exceeded'
|
49
|
+
RE = 'Runtime Error'
|
50
|
+
CE = 'Compilation Error'
|
51
|
+
WJ = 'Juding ...'
|
49
52
|
|
50
53
|
|
51
54
|
@dataclass
|
52
55
|
class TestCaseResult:
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
56
|
+
output: str
|
57
|
+
executed_time: Union[int, None]
|
58
|
+
# memory_usage: Union[int, None]
|
59
|
+
passed: ResultStatus
|
57
60
|
|
58
61
|
|
59
62
|
@dataclass
|
60
63
|
class LabeledTestCaseResult:
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
+
label: str
|
65
|
+
testcase: TestCase
|
66
|
+
result: TestCaseResult
|
64
67
|
|
65
68
|
|
66
69
|
@dataclass
|
67
70
|
class TestInformation:
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
def
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
71
|
+
lang: Lang
|
72
|
+
sourcename: str
|
73
|
+
case_number: int
|
74
|
+
results: List[ResultStatus] = field(default_factory=list)
|
75
|
+
compiler_message: str = ''
|
76
|
+
compile_time: Optional[int] = None
|
77
|
+
_summary: Optional[ResultStatus] = None
|
78
|
+
|
79
|
+
@property
|
80
|
+
def summary(self) -> ResultStatus:
|
81
|
+
if self._summary:
|
82
|
+
return self._summary
|
83
|
+
else:
|
84
|
+
priority_order = [
|
85
|
+
ResultStatus.CE,
|
86
|
+
ResultStatus.RE,
|
87
|
+
ResultStatus.WA,
|
88
|
+
ResultStatus.TLE,
|
89
|
+
ResultStatus.MLE,
|
90
|
+
ResultStatus.WJ,
|
91
|
+
ResultStatus.AC,
|
92
|
+
]
|
93
|
+
priority_dict = {
|
94
|
+
status: index + 1 for index, status in enumerate(priority_order)
|
95
|
+
}
|
96
|
+
summary = min(
|
97
|
+
self.results,
|
98
|
+
key=lambda status: priority_dict[status],
|
99
|
+
default=ResultStatus.WJ,
|
100
|
+
)
|
101
|
+
|
102
|
+
if len(self.results) == self.case_number:
|
103
|
+
return summary
|
104
|
+
|
105
|
+
return ResultStatus.WJ if summary == ResultStatus.AC else summary
|
106
|
+
|
107
|
+
@summary.setter
|
108
|
+
def summary(self, value: ResultStatus) -> None:
|
109
|
+
self._summary = value
|
110
|
+
|
111
|
+
def update(
|
112
|
+
self, updator: Union[TestCaseResult, LabeledTestCaseResult, ResultStatus]
|
113
|
+
) -> None:
|
114
|
+
match updator:
|
115
|
+
case TestCaseResult():
|
116
|
+
self.results.append(updator.passed)
|
117
|
+
case LabeledTestCaseResult():
|
118
|
+
self.results.append(updator.result.passed)
|
119
|
+
case ResultStatus():
|
120
|
+
self.results.append(updator)
|
121
|
+
|
122
|
+
def __iadd__(
|
123
|
+
self, other: Union[TestCaseResult, LabeledTestCaseResult, ResultStatus]
|
124
|
+
) -> 'TestInformation':
|
125
|
+
self.update(other)
|
126
|
+
return self
|
127
|
+
|
128
|
+
|
129
|
+
class TestRunner:
|
130
|
+
def __init__(self, path: str, lcases: List[LabeledTestCase]) -> None:
|
131
|
+
self.source = path
|
132
|
+
self.lcases = iter(lcases)
|
133
|
+
self.info = TestInformation(
|
134
|
+
lang=detect_language(self.source),
|
135
|
+
sourcename=path,
|
136
|
+
case_number=len(lcases),
|
137
|
+
)
|
138
|
+
|
139
|
+
def __iter__(self):
|
140
|
+
lang = self.info.lang
|
141
|
+
if lang in COMPILED_LANGUAGES:
|
142
|
+
exe_path, compile_result, compile_time = run_compile(self.source, lang)
|
143
|
+
self.info.compiler_message = compile_result.stderr
|
144
|
+
self.info.compile_time = compile_time
|
145
|
+
if compile_result.returncode != 0:
|
146
|
+
self.info.results = [ResultStatus.CE]
|
147
|
+
return iter([])
|
148
|
+
|
149
|
+
self.cmd = [
|
150
|
+
arg.format(source_path=self.source, exec_path=exe_path)
|
151
|
+
for arg in LANGUAGE_RUN_COMMANDS[lang]
|
152
|
+
]
|
153
|
+
self.exe = exe_path
|
154
|
+
run_code(self.cmd, TestCase(input='', output='')) # バイナリーの慣らし運転
|
155
|
+
elif lang in INTERPRETED_LANGUAGES:
|
156
|
+
self.cmd = [
|
157
|
+
arg.format(source_path=self.source)
|
158
|
+
for arg in LANGUAGE_RUN_COMMANDS[lang]
|
159
|
+
]
|
160
|
+
self.exe = None
|
161
|
+
else:
|
162
|
+
raise ValueError(f'{lang}の適切な言語のランナーが見つかりませんでした.')
|
163
|
+
|
164
|
+
return self
|
165
|
+
|
166
|
+
def __next__(self):
|
167
|
+
try:
|
168
|
+
lcase = next(self.lcases)
|
169
|
+
result = run_code(self.cmd, lcase.case)
|
170
|
+
self.info += result
|
171
|
+
return LabeledTestCaseResult(lcase.label, lcase.case, result)
|
172
|
+
except StopIteration:
|
173
|
+
if self.exe and os.path.exists(self.exe):
|
174
|
+
os.remove(self.exe)
|
175
|
+
raise
|
106
176
|
|
107
177
|
|
108
178
|
def run_code(cmd: list, case: TestCase) -> TestCaseResult:
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
179
|
+
start_time = time.time()
|
180
|
+
try:
|
181
|
+
proc = subprocess.run(
|
182
|
+
cmd, input=case.input, text=True, capture_output=True, timeout=4
|
183
|
+
)
|
184
|
+
end_time = time.time()
|
185
|
+
executed_time = int((end_time - start_time) * 1000)
|
186
|
+
except subprocess.TimeoutExpired as e_proc:
|
187
|
+
end_time = time.time()
|
188
|
+
executed_time = int((end_time - start_time) * 1000)
|
189
|
+
stdout_text = e_proc.stdout.decode('utf-8') if e_proc.stdout is not None else ''
|
190
|
+
stderr_text = e_proc.stderr.decode('utf-8') if e_proc.stderr is not None else ''
|
191
|
+
text = stdout_text + '\n' + stderr_text
|
192
|
+
return TestCaseResult(
|
193
|
+
output=text, executed_time=executed_time, passed=ResultStatus.TLE
|
194
|
+
)
|
195
|
+
|
196
|
+
# プロセスの終了コードを確認し、異常終了ならREを返す
|
197
|
+
if proc.returncode != 0:
|
198
|
+
return TestCaseResult(
|
199
|
+
output=proc.stdout + '\n' + proc.stderr,
|
200
|
+
executed_time=executed_time,
|
201
|
+
passed=ResultStatus.RE,
|
202
|
+
)
|
203
|
+
|
204
|
+
# 実際の出力と期待される出力を比較
|
205
|
+
actual_output = proc.stdout.strip()
|
206
|
+
expected_output = case.output.strip()
|
207
|
+
|
208
|
+
if actual_output != expected_output:
|
209
|
+
return TestCaseResult(
|
210
|
+
output=actual_output,
|
211
|
+
executed_time=executed_time,
|
212
|
+
passed=ResultStatus.WA,
|
213
|
+
)
|
214
|
+
else:
|
215
|
+
return TestCaseResult(
|
216
|
+
output=actual_output, executed_time=executed_time, passed=ResultStatus.AC
|
217
|
+
)
|
146
218
|
|
147
219
|
|
148
220
|
LANGUAGE_RUN_COMMANDS: Dict[Lang, list] = {
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
221
|
+
Lang.PYTHON: ['python3', '{source_path}'],
|
222
|
+
Lang.JAVASCRIPT: ['node', '{source_path}'],
|
223
|
+
Lang.C: ['{exec_path}'],
|
224
|
+
Lang.CPP: ['{exec_path}'],
|
225
|
+
Lang.RUST: ['{exec_path}'],
|
226
|
+
Lang.JAVA: ['java', os.path.splitext(os.path.basename('{source_path}'))[0]],
|
155
227
|
}
|
156
228
|
|
157
229
|
LANGUAGE_COMPILE_COMMANDS: Dict[Lang, list] = {
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
230
|
+
Lang.C: ['gcc', '{source_path}', '-o', '{exec_path}'],
|
231
|
+
Lang.CPP: ['g++', '{source_path}', '-o', '{exec_path}'],
|
232
|
+
Lang.RUST: ['rustc', '{source_path}', '-o', '{exec_path}'],
|
233
|
+
Lang.JAVA: ['javac', '{source_path}'],
|
162
234
|
}
|
163
235
|
|
164
236
|
|
165
|
-
def run_compile(
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
def judge_code_from(
|
180
|
-
lcases: List[LabeledTestCase], path: str
|
181
|
-
) -> Generator[
|
182
|
-
Union[LabeledTestCaseResult, TestInformation], # type: ignore
|
183
|
-
None,
|
184
|
-
None,
|
185
|
-
]:
|
186
|
-
lang = detect_language(path)
|
187
|
-
if lang in COMPILED_LANGUAGES:
|
188
|
-
exe_path, compile_result, compile_time = run_compile(path, lang)
|
189
|
-
if compile_result.returncode != 0:
|
190
|
-
yield TestInformation(
|
191
|
-
lang=lang,
|
192
|
-
sourcename=path,
|
193
|
-
case_number=len(lcases),
|
194
|
-
result_summary=ResultStatus.CE,
|
195
|
-
compiler_message=compile_result.stderr,
|
196
|
-
)
|
197
|
-
return
|
198
|
-
else:
|
199
|
-
yield TestInformation(
|
200
|
-
lang=lang,
|
201
|
-
sourcename=path,
|
202
|
-
case_number=len(lcases),
|
203
|
-
compiler_message=compile_result.stderr,
|
204
|
-
compile_time=compile_time,
|
205
|
-
)
|
206
|
-
|
207
|
-
cmd = [
|
208
|
-
arg.format(exec_path=exe_path) for arg in LANGUAGE_RUN_COMMANDS[lang]
|
209
|
-
]
|
210
|
-
|
211
|
-
for lcase in lcases:
|
212
|
-
yield LabeledTestCaseResult(
|
213
|
-
lcase.label, lcase.case, run_code(cmd, lcase.case)
|
214
|
-
)
|
215
|
-
|
216
|
-
if os.path.exists(exe_path):
|
217
|
-
os.remove(exe_path)
|
218
|
-
|
219
|
-
elif lang in INTERPRETED_LANGUAGES:
|
220
|
-
yield TestInformation(
|
221
|
-
lang=lang,
|
222
|
-
sourcename=path,
|
223
|
-
case_number=len(lcases),
|
224
|
-
)
|
225
|
-
cmd = [arg.format(source_path=path) for arg in LANGUAGE_RUN_COMMANDS[lang]]
|
226
|
-
for lcase in lcases:
|
227
|
-
yield LabeledTestCaseResult(
|
228
|
-
lcase.label, lcase.case, run_code(cmd, lcase.case)
|
229
|
-
)
|
230
|
-
else:
|
231
|
-
raise ValueError('適切な言語が見つかりませんでした.')
|
237
|
+
def run_compile(
|
238
|
+
path: str, lang: Lang
|
239
|
+
) -> Tuple[str, subprocess.CompletedProcess, Optional[int]]:
|
240
|
+
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
241
|
+
exec_path = tmp.name
|
242
|
+
cmd = [
|
243
|
+
arg.format(source_path=path, exec_path=exec_path)
|
244
|
+
for arg in LANGUAGE_COMPILE_COMMANDS[lang]
|
245
|
+
]
|
246
|
+
start_time = time.time()
|
247
|
+
compile_result = subprocess.run(cmd, capture_output=True, text=True)
|
248
|
+
compile_time = int((time.time() - start_time) * 1000)
|
249
|
+
|
250
|
+
return exec_path, compile_result, compile_time
|
232
251
|
|
233
252
|
|
234
253
|
COLOR_MAP = {
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
254
|
+
ResultStatus.AC: 'green',
|
255
|
+
ResultStatus.WA: 'red',
|
256
|
+
ResultStatus.TLE: 'yellow',
|
257
|
+
ResultStatus.MLE: 'yellow',
|
258
|
+
ResultStatus.RE: 'yellow',
|
259
|
+
ResultStatus.CE: 'yellow',
|
260
|
+
ResultStatus.WJ: 'grey',
|
242
261
|
}
|
243
262
|
|
244
263
|
STATUS_TEXT_MAP = {
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
264
|
+
ResultStatus.AC: Text.assemble(
|
265
|
+
('\u2713 ', 'green'),
|
266
|
+
(
|
267
|
+
f'{ResultStatus.AC.value}',
|
268
|
+
Style(bgcolor=COLOR_MAP[ResultStatus.AC], bold=True),
|
269
|
+
),
|
270
|
+
),
|
271
|
+
ResultStatus.WA: Text(
|
272
|
+
f'\u00d7 {ResultStatus.WA.value}', style=COLOR_MAP[ResultStatus.WA]
|
273
|
+
),
|
274
|
+
ResultStatus.TLE: Text(
|
275
|
+
f'\u00d7 {ResultStatus.TLE.value}', style=COLOR_MAP[ResultStatus.TLE]
|
276
|
+
),
|
277
|
+
ResultStatus.MLE: Text(
|
278
|
+
f'\u00d7 {ResultStatus.MLE.value}', style=COLOR_MAP[ResultStatus.MLE]
|
279
|
+
),
|
280
|
+
ResultStatus.RE: Text(
|
281
|
+
f'\u00d7 {ResultStatus.RE.value}', style=COLOR_MAP[ResultStatus.RE]
|
282
|
+
),
|
283
|
+
ResultStatus.CE: Text(
|
284
|
+
f'\u00d7 {ResultStatus.CE.value}', style=COLOR_MAP[ResultStatus.CE]
|
285
|
+
),
|
286
|
+
ResultStatus.WJ: Text(
|
287
|
+
f'\u23f3 {ResultStatus.WJ.value}', style=COLOR_MAP[ResultStatus.WJ]
|
288
|
+
),
|
270
289
|
}
|
271
290
|
|
272
291
|
|
273
|
-
def create_renderable_test_info(
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
)
|
313
|
-
test_info.resultlist.append(test_result)
|
314
|
-
|
315
|
-
priority_order = [
|
316
|
-
ResultStatus.RE,
|
317
|
-
ResultStatus.WA,
|
318
|
-
ResultStatus.TLE,
|
319
|
-
ResultStatus.MLE,
|
320
|
-
ResultStatus.WJ,
|
321
|
-
ResultStatus.AC,
|
322
|
-
]
|
323
|
-
|
324
|
-
# 現在の結果の中で最も高い優先順位のステータスを見つける
|
325
|
-
highest_priority_status = (
|
326
|
-
test_info.result_summary
|
327
|
-
) # デフォルトはWJまたは現在のサマリー
|
328
|
-
for result in test_info.resultlist:
|
329
|
-
status = result.result.passed
|
330
|
-
if priority_order.index(status) < priority_order.index(highest_priority_status):
|
331
|
-
highest_priority_status = status
|
332
|
-
|
333
|
-
# 特殊ケース: すべてのテストケースがACである場合(途中でも)
|
334
|
-
if all(result.result.passed == ResultStatus.AC for result in test_info.resultlist):
|
335
|
-
test_info.result_summary = ResultStatus.AC
|
336
|
-
else:
|
337
|
-
test_info.result_summary = highest_priority_status
|
292
|
+
def create_renderable_test_info(
|
293
|
+
test_info: TestInformation, progress: Optional[Progress] = None
|
294
|
+
) -> RenderableType:
|
295
|
+
components = []
|
296
|
+
|
297
|
+
success_count = sum(1 for result in test_info.results if result == ResultStatus.AC)
|
298
|
+
total_count = test_info.case_number
|
299
|
+
|
300
|
+
status_text = STATUS_TEXT_MAP[test_info.summary]
|
301
|
+
|
302
|
+
header_text = Text.assemble(
|
303
|
+
Text.from_markup(f'[cyan]{test_info.sourcename}[/]のテスト \n'),
|
304
|
+
Text.from_markup(
|
305
|
+
f'[italic #0f0f0f]コンパイルにかかった時間: [not italic cyan]{test_info.compile_time}[/] ms[/]\n'
|
306
|
+
)
|
307
|
+
if test_info.compile_time
|
308
|
+
else Text(''),
|
309
|
+
status_text,
|
310
|
+
Text.from_markup(
|
311
|
+
f' [{COLOR_MAP[test_info.summary]} bold]{success_count}[/] / [white bold]{total_count}[/]'
|
312
|
+
),
|
313
|
+
)
|
314
|
+
|
315
|
+
if progress:
|
316
|
+
components.append(Panel(Group(header_text, progress), expand=False))
|
317
|
+
else:
|
318
|
+
components.append(Panel(header_text, expand=False))
|
319
|
+
|
320
|
+
if test_info.compiler_message:
|
321
|
+
rule = Rule(
|
322
|
+
title='コンパイラーのメッセージ',
|
323
|
+
style=COLOR_MAP[ResultStatus.CE],
|
324
|
+
)
|
325
|
+
components.append(rule)
|
326
|
+
error_message = Syntax(
|
327
|
+
test_info.compiler_message, lang2str(test_info.lang), line_numbers=False
|
328
|
+
)
|
329
|
+
components.append(error_message)
|
330
|
+
|
331
|
+
return Group(*components)
|
338
332
|
|
339
333
|
|
340
334
|
def create_renderable_test_result(
|
341
|
-
|
342
|
-
|
335
|
+
i: int,
|
336
|
+
test_result: LabeledTestCaseResult,
|
343
337
|
) -> RenderableType:
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
338
|
+
rule = Rule(
|
339
|
+
title=f'No.{i+1} {test_result.label}',
|
340
|
+
style=COLOR_MAP[test_result.result.passed],
|
341
|
+
)
|
342
|
+
|
343
|
+
# 以下の部分は if-else ブロックの外に移動
|
344
|
+
status_header = Text.assemble(
|
345
|
+
'ステータス ',
|
346
|
+
STATUS_TEXT_MAP[test_result.result.passed], # status_text をここに追加
|
347
|
+
)
|
348
|
+
|
349
|
+
execution_time_text = None
|
350
|
+
if test_result.result.executed_time is not None:
|
351
|
+
execution_time_text = Text.from_markup(
|
352
|
+
f'実行時間 [cyan]{test_result.result.executed_time}[/cyan] ms'
|
353
|
+
)
|
354
|
+
|
355
|
+
table = Table(show_header=True, header_style='bold')
|
356
|
+
table.add_column('入力', style='cyan', min_width=10)
|
357
|
+
|
358
|
+
if test_result.result.passed != ResultStatus.AC:
|
359
|
+
table.add_column(
|
360
|
+
'出力',
|
361
|
+
style=COLOR_MAP[test_result.result.passed],
|
362
|
+
min_width=10,
|
363
|
+
overflow='fold',
|
364
|
+
)
|
365
|
+
table.add_column('正解の出力', style=COLOR_MAP[ResultStatus.AC], min_width=10)
|
366
|
+
table.add_row(
|
367
|
+
escape(test_result.testcase.input),
|
368
|
+
escape(test_result.result.output),
|
369
|
+
escape(test_result.testcase.output),
|
370
|
+
)
|
371
|
+
else:
|
372
|
+
table.add_column(
|
373
|
+
'出力', style=COLOR_MAP[test_result.result.passed], min_width=10
|
374
|
+
)
|
375
|
+
table.add_row(
|
376
|
+
escape(test_result.testcase.input), escape(test_result.result.output)
|
377
|
+
)
|
378
|
+
|
379
|
+
components = [
|
380
|
+
rule,
|
381
|
+
status_header,
|
382
|
+
execution_time_text if execution_time_text else '',
|
383
|
+
table,
|
384
|
+
]
|
385
|
+
|
386
|
+
return Group(*components)
|
387
|
+
|
388
|
+
|
389
|
+
def render_results(test: TestRunner) -> None:
|
390
|
+
progress = Progress(
|
391
|
+
SpinnerColumn(style='white', spinner_name='circleHalves'),
|
392
|
+
TextColumn('{task.description}'),
|
393
|
+
SpinnerColumn(style='white', spinner_name='simpleDots'),
|
394
|
+
BarColumn(),
|
395
|
+
)
|
396
|
+
task_id = progress.add_task(description='テスト進行中', total=test.info.case_number)
|
397
|
+
|
398
|
+
current_display = [create_renderable_test_info(test.info, progress)]
|
399
|
+
|
400
|
+
with Live(Group(*current_display)) as live:
|
401
|
+
for i, result in enumerate(test):
|
402
|
+
progress.advance(task_id, advance=1)
|
403
|
+
current_display[-1] = create_renderable_test_info(test.info, progress)
|
404
|
+
current_display.insert(-1, (create_renderable_test_result(i, result)))
|
405
|
+
live.update(Group(*current_display))
|
406
|
+
|
407
|
+
progress.update(task_id, description='テスト完了') # 完了メッセージに更新
|
408
|
+
current_display[-1] = create_renderable_test_info(test.info, progress)
|
409
|
+
live.update(Group(*current_display))
|
415
410
|
|
416
411
|
|
417
412
|
def run_test(path_of_code: str) -> None:
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
413
|
+
html_paths = [f for f in os.listdir('.') if f.endswith('.html')]
|
414
|
+
if not html_paths:
|
415
|
+
print(
|
416
|
+
'問題のファイルが見つかりません。\n問題のファイルが存在するディレクトリーに移動してから実行してください。'
|
417
|
+
)
|
418
|
+
return
|
419
|
+
|
420
|
+
with open(html_paths[0], 'r') as file:
|
421
|
+
html = file.read()
|
422
|
+
|
423
|
+
lcases = ProblemHTML(html).load_labeled_testcase()
|
424
|
+
test = TestRunner(path_of_code, lcases)
|
425
|
+
render_results(test)
|
426
|
+
|
427
|
+
|
428
|
+
@click.command(short_help='テストを実行')
|
429
|
+
@add_file_selector('files', filetypes=COMPILED_LANGUAGES + INTERPRETED_LANGUAGES)
|
430
|
+
def test(files):
|
431
|
+
"""指定したソースコードをサンプルケースでテストします。"""
|
432
|
+
for path in files:
|
433
|
+
run_test(path)
|