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