AtCoderStudyBooster 0.3__tar.gz → 0.23__tar.gz
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.
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/PKG-INFO +1 -1
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/atcdr/generate.py +1 -1
- atcoderstudybooster-0.23/atcdr/test.py +294 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/atcdr/util/filetype.py +8 -22
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/pyproject.toml +1 -1
- atcoderstudybooster-0.3/atcdr/test.py +0 -438
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/.github/workflows/deploy.yaml +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/.gitignore +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/.images/demo1.png +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/.pre-commit-config.yaml +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/.python-version +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/.vscode/extensions.json +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/.vscode/setting.json +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/.vscode/tasks.json +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/README.md +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/atcdr/__init__.py +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/atcdr/download.py +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/atcdr/main.py +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/atcdr/markdown.py +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/atcdr/open.py +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/atcdr/util/__init__.py +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/atcdr/util/cost.py +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/atcdr/util/execute.py +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/atcdr/util/gpt.py +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/atcdr/util/problem.py +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/requirements-dev.lock +0 -0
- {atcoderstudybooster-0.3 → atcoderstudybooster-0.23}/requirements.lock +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: AtCoderStudyBooster
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.23
|
4
4
|
Summary: A tool to download and manage AtCoder problems.
|
5
5
|
Project-URL: Homepage, https://github.com/yuta6/AtCoderStudyBooster
|
6
6
|
Author-email: yuta6 <46110512+yuta6@users.noreply.github.com>
|
@@ -140,8 +140,8 @@ def solve_problem(file: Filename, lang: Lang) -> None:
|
|
140
140
|
|
141
141
|
for i in range(1, 4):
|
142
142
|
with console.status(f'{i}回目のコード生成 (by {gpt.model.value})...'):
|
143
|
-
test_report = ''
|
144
143
|
if i == 1:
|
144
|
+
test_report = ''
|
145
145
|
reply = gpt.tell(md)
|
146
146
|
else:
|
147
147
|
reply = gpt.tell(f"""The following is the test report for the code you provided:
|
@@ -0,0 +1,294 @@
|
|
1
|
+
import os
|
2
|
+
import subprocess
|
3
|
+
import tempfile
|
4
|
+
import time
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from enum import Enum
|
7
|
+
from typing import Callable, Dict, List, Optional, Union
|
8
|
+
|
9
|
+
from bs4 import BeautifulSoup as bs
|
10
|
+
from rich.console import Console
|
11
|
+
from rich.markup import escape
|
12
|
+
from rich.panel import Panel
|
13
|
+
from rich.table import Table
|
14
|
+
from rich.text import Text
|
15
|
+
|
16
|
+
from atcdr.util.execute import execute_files
|
17
|
+
from atcdr.util.filetype import FILE_EXTENSIONS, SOURCE_LANGUAGES, Lang
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class TestCase:
|
22
|
+
input: str
|
23
|
+
output: str
|
24
|
+
|
25
|
+
|
26
|
+
@dataclass
|
27
|
+
class LabeledTestCase:
|
28
|
+
label: str
|
29
|
+
case: TestCase
|
30
|
+
|
31
|
+
|
32
|
+
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'
|
39
|
+
|
40
|
+
|
41
|
+
@dataclass
|
42
|
+
class TestCaseResult:
|
43
|
+
output: str
|
44
|
+
executed_time: Union[int, None]
|
45
|
+
# memory_usage: Union[int, None]
|
46
|
+
passed: ResultStatus
|
47
|
+
|
48
|
+
|
49
|
+
@dataclass
|
50
|
+
class LabeledTestCaseResult:
|
51
|
+
label: str
|
52
|
+
testcase: TestCase
|
53
|
+
# TODO : 実はラベル自体を使わない方がいいかもしれない.ラベルという概念が削除してプリントするときに適当にTest1, Test2と適当に名前をつけてもいいかも.
|
54
|
+
result: TestCaseResult
|
55
|
+
|
56
|
+
|
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
|
66
|
+
|
67
|
+
sample_input_pre = sample_input_section.find_next('pre')
|
68
|
+
sample_output_pre = sample_output_section.find_next('pre')
|
69
|
+
|
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
|
+
)
|
80
|
+
|
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)
|
84
|
+
|
85
|
+
return test_cases
|
86
|
+
|
87
|
+
|
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,
|
193
|
+
}
|
194
|
+
|
195
|
+
|
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)
|
275
|
+
|
276
|
+
|
277
|
+
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
|
284
|
+
|
285
|
+
with open(html_paths[0], 'r') as file:
|
286
|
+
html = file.read()
|
287
|
+
|
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)
|
291
|
+
|
292
|
+
|
293
|
+
def test(*args: str) -> None:
|
294
|
+
execute_files(*args, func=run_test, target_filetypes=SOURCE_LANGUAGES)
|
@@ -1,6 +1,5 @@
|
|
1
|
-
import os
|
2
1
|
from enum import Enum
|
3
|
-
from typing import Dict, List,
|
2
|
+
from typing import Dict, List, TypeAlias
|
4
3
|
|
5
4
|
# ファイル名と拡張子の型エイリアスを定義
|
6
5
|
Filename: TypeAlias = str
|
@@ -24,7 +23,7 @@ class Lang(Enum):
|
|
24
23
|
|
25
24
|
|
26
25
|
# ファイル拡張子と対応する言語の辞書
|
27
|
-
FILE_EXTENSIONS: Dict[Lang,
|
26
|
+
FILE_EXTENSIONS: Dict[Lang, Extension] = {
|
28
27
|
Lang.PYTHON: '.py',
|
29
28
|
Lang.JAVASCRIPT: '.js',
|
30
29
|
Lang.JAVA: '.java',
|
@@ -40,29 +39,24 @@ FILE_EXTENSIONS: Dict[Lang, str] = {
|
|
40
39
|
Lang.JSON: '.json',
|
41
40
|
}
|
42
41
|
|
43
|
-
# ドキュメント言語のリスト
|
44
42
|
DOCUMENT_LANGUAGES: List[Lang] = [
|
45
43
|
Lang.HTML,
|
46
44
|
Lang.MARKDOWN,
|
47
45
|
Lang.JSON,
|
48
46
|
]
|
49
47
|
|
50
|
-
#
|
51
|
-
|
48
|
+
# ソースコードファイルと言語のリスト
|
49
|
+
SOURCE_LANGUAGES: List[Lang] = [
|
50
|
+
Lang.PYTHON,
|
51
|
+
Lang.JAVASCRIPT,
|
52
52
|
Lang.JAVA,
|
53
53
|
Lang.C,
|
54
54
|
Lang.CPP,
|
55
55
|
Lang.CSHARP,
|
56
|
-
Lang.GO,
|
57
|
-
Lang.RUST,
|
58
|
-
]
|
59
|
-
|
60
|
-
# インタプリター型言語のリスト
|
61
|
-
INTERPRETED_LANGUAGES: List[Lang] = [
|
62
|
-
Lang.PYTHON,
|
63
|
-
Lang.JAVASCRIPT,
|
64
56
|
Lang.RUBY,
|
65
57
|
Lang.PHP,
|
58
|
+
Lang.GO,
|
59
|
+
Lang.RUST,
|
66
60
|
]
|
67
61
|
|
68
62
|
|
@@ -95,11 +89,3 @@ def str2lang(lang: str) -> Lang:
|
|
95
89
|
|
96
90
|
def lang2str(lang: Lang) -> str:
|
97
91
|
return lang.value
|
98
|
-
|
99
|
-
|
100
|
-
def detect_language(path: str) -> Optional[Lang]:
|
101
|
-
ext = os.path.splitext(path)[1] # ファイルの拡張子を取得
|
102
|
-
lang = next(
|
103
|
-
(lang for lang, extension in FILE_EXTENSIONS.items() if extension == ext), None
|
104
|
-
)
|
105
|
-
return lang
|
@@ -1,438 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import subprocess
|
3
|
-
import tempfile
|
4
|
-
import time
|
5
|
-
from dataclasses import dataclass, field
|
6
|
-
from enum import Enum
|
7
|
-
from typing import Dict, Generator, List, Tuple, Union, Optional
|
8
|
-
|
9
|
-
from bs4 import BeautifulSoup as bs
|
10
|
-
from rich.console import Console, Group, RenderableType
|
11
|
-
from rich.markup import escape
|
12
|
-
from rich.panel import Panel
|
13
|
-
from rich.rule import Rule
|
14
|
-
from rich.style import Style
|
15
|
-
from rich.syntax import Syntax
|
16
|
-
from rich.table import Table
|
17
|
-
from rich.text import Text
|
18
|
-
|
19
|
-
from atcdr.util.execute import execute_files
|
20
|
-
from atcdr.util.filetype import (
|
21
|
-
COMPILED_LANGUAGES,
|
22
|
-
INTERPRETED_LANGUAGES,
|
23
|
-
Lang,
|
24
|
-
detect_language,
|
25
|
-
lang2str,
|
26
|
-
)
|
27
|
-
|
28
|
-
|
29
|
-
@dataclass
|
30
|
-
class TestCase:
|
31
|
-
input: str
|
32
|
-
output: str
|
33
|
-
|
34
|
-
|
35
|
-
@dataclass
|
36
|
-
class LabeledTestCase:
|
37
|
-
label: str
|
38
|
-
case: TestCase
|
39
|
-
|
40
|
-
|
41
|
-
class ResultStatus(Enum):
|
42
|
-
AC = 'Accepted'
|
43
|
-
WA = 'Wrong Answer'
|
44
|
-
TLE = 'Time Limit Exceeded'
|
45
|
-
MLE = 'Memory Limit Exceeded'
|
46
|
-
RE = 'Runtime Error'
|
47
|
-
CE = 'Compilation Error'
|
48
|
-
WJ = 'Juding ...'
|
49
|
-
|
50
|
-
|
51
|
-
@dataclass
|
52
|
-
class TestCaseResult:
|
53
|
-
output: str
|
54
|
-
executed_time: Union[int, None]
|
55
|
-
# memory_usage: Union[int, None]
|
56
|
-
passed: ResultStatus
|
57
|
-
|
58
|
-
|
59
|
-
@dataclass
|
60
|
-
class LabeledTestCaseResult:
|
61
|
-
label: str
|
62
|
-
testcase: TestCase
|
63
|
-
result: TestCaseResult
|
64
|
-
|
65
|
-
|
66
|
-
@dataclass
|
67
|
-
class TestInformation:
|
68
|
-
lang: Lang
|
69
|
-
sourcename: str
|
70
|
-
case_number: int
|
71
|
-
result_summary: ResultStatus = ResultStatus.WJ
|
72
|
-
resultlist: List[LabeledTestCaseResult] = field(default_factory=list) # 修正
|
73
|
-
compiler_message: str = ''
|
74
|
-
compile_time : Optional[int] = None
|
75
|
-
|
76
|
-
|
77
|
-
def create_testcases_from_html(html: str) -> List[LabeledTestCase]:
|
78
|
-
soup = bs(html, 'html.parser')
|
79
|
-
test_cases = []
|
80
|
-
|
81
|
-
for i in range(1, 20):
|
82
|
-
sample_input_section = soup.find('h3', text=f'Sample Input {i}')
|
83
|
-
sample_output_section = soup.find('h3', text=f'Sample Output {i}')
|
84
|
-
if not sample_input_section or not sample_output_section:
|
85
|
-
break
|
86
|
-
|
87
|
-
sample_input_pre = sample_input_section.find_next('pre')
|
88
|
-
sample_output_pre = sample_output_section.find_next('pre')
|
89
|
-
|
90
|
-
sample_input = (
|
91
|
-
sample_input_pre.get_text(strip=True)
|
92
|
-
if sample_input_pre is not None
|
93
|
-
else ''
|
94
|
-
)
|
95
|
-
sample_output = (
|
96
|
-
sample_output_pre.get_text(strip=True)
|
97
|
-
if sample_output_pre is not None
|
98
|
-
else ''
|
99
|
-
)
|
100
|
-
|
101
|
-
test_case = TestCase(input=sample_input, output=sample_output)
|
102
|
-
labeled_test_case = LabeledTestCase(label=f'Sample {i}', case=test_case)
|
103
|
-
test_cases.append(labeled_test_case)
|
104
|
-
|
105
|
-
return test_cases
|
106
|
-
|
107
|
-
|
108
|
-
def run_code(cmd: list, case: TestCase) -> TestCaseResult:
|
109
|
-
start_time = time.time()
|
110
|
-
try:
|
111
|
-
proc = subprocess.run(
|
112
|
-
cmd, input=case.input, text=True, capture_output=True, timeout=4
|
113
|
-
)
|
114
|
-
executed_time = int((time.time() - start_time) * 1000)
|
115
|
-
except subprocess.TimeoutExpired as e_proc:
|
116
|
-
executed_time = int((time.time() - start_time) * 1000)
|
117
|
-
stdout_text = e_proc.stdout.decode('utf-8') if e_proc.stdout is not None else ''
|
118
|
-
stderr_text = e_proc.stderr.decode('utf-8') if e_proc.stderr is not None else ''
|
119
|
-
text = stdout_text + '\n' + stderr_text
|
120
|
-
return TestCaseResult(
|
121
|
-
output=text, executed_time=executed_time, passed=ResultStatus.TLE
|
122
|
-
)
|
123
|
-
|
124
|
-
# プロセスの終了コードを確認し、異常終了ならREを返す
|
125
|
-
if proc.returncode != 0:
|
126
|
-
return TestCaseResult(
|
127
|
-
output=proc.stdout + '\n' + proc.stderr,
|
128
|
-
executed_time=executed_time,
|
129
|
-
passed=ResultStatus.RE,
|
130
|
-
)
|
131
|
-
|
132
|
-
# 実際の出力と期待される出力を比較
|
133
|
-
actual_output = proc.stdout.strip()
|
134
|
-
expected_output = case.output.strip()
|
135
|
-
|
136
|
-
if actual_output != expected_output:
|
137
|
-
return TestCaseResult(
|
138
|
-
output=actual_output,
|
139
|
-
executed_time=executed_time,
|
140
|
-
passed=ResultStatus.WA,
|
141
|
-
)
|
142
|
-
else:
|
143
|
-
return TestCaseResult(
|
144
|
-
output=actual_output, executed_time=executed_time, passed=ResultStatus.AC
|
145
|
-
)
|
146
|
-
|
147
|
-
|
148
|
-
LANGUAGE_RUN_COMMANDS: Dict[Lang, list] = {
|
149
|
-
Lang.PYTHON: ['python3', '{source_path}'],
|
150
|
-
Lang.JAVASCRIPT: ['node', '{source_path}'],
|
151
|
-
Lang.C: ['{exec_path}'],
|
152
|
-
Lang.CPP: ['{exec_path}'],
|
153
|
-
Lang.RUST: ['{exec_path}'],
|
154
|
-
Lang.JAVA: ['java', '{exec_path}'],
|
155
|
-
}
|
156
|
-
|
157
|
-
LANGUAGE_COMPILE_COMMANDS: Dict[Lang, list] = {
|
158
|
-
Lang.C: ['gcc', '{source_path}', '-o', '{exec_path}'],
|
159
|
-
Lang.CPP: ['g++', '{source_path}', '-o', '{exec_path}'],
|
160
|
-
Lang.RUST: ['rustc', '{source_path}', '-o', '{exec_path}'],
|
161
|
-
Lang.JAVA: ['javac', '{source_path}'],
|
162
|
-
}
|
163
|
-
|
164
|
-
|
165
|
-
def run_compile(path: str, lang: Lang) -> Tuple[str, subprocess.CompletedProcess]:
|
166
|
-
with tempfile.NamedTemporaryFile(delete=True) as tmp:
|
167
|
-
exec_path = tmp.name
|
168
|
-
cmd = [
|
169
|
-
arg.format(source_path=path, exec_path=exec_path)
|
170
|
-
for arg in LANGUAGE_COMPILE_COMMANDS[lang]
|
171
|
-
]
|
172
|
-
start_time = time.time()
|
173
|
-
compile_result = subprocess.run(cmd, capture_output=True, text=True)
|
174
|
-
compile_time = int((time.time() - start_time) * 1000)
|
175
|
-
|
176
|
-
return exec_path, compile_result, compile_time
|
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('適切な言語が見つかりませんでした.')
|
232
|
-
|
233
|
-
|
234
|
-
COLOR_MAP = {
|
235
|
-
ResultStatus.AC: 'green',
|
236
|
-
ResultStatus.WA: 'red',
|
237
|
-
ResultStatus.TLE: 'yellow',
|
238
|
-
ResultStatus.MLE: 'yellow',
|
239
|
-
ResultStatus.RE: 'yellow',
|
240
|
-
ResultStatus.CE: 'yellow',
|
241
|
-
ResultStatus.WJ: 'grey',
|
242
|
-
}
|
243
|
-
|
244
|
-
STATUS_TEXT_MAP = {
|
245
|
-
ResultStatus.AC: Text.assemble(
|
246
|
-
('\u2713 ', 'green'),
|
247
|
-
(
|
248
|
-
f'{ResultStatus.AC.value}',
|
249
|
-
Style(bgcolor=COLOR_MAP[ResultStatus.AC], bold=True),
|
250
|
-
),
|
251
|
-
),
|
252
|
-
ResultStatus.WA: Text(
|
253
|
-
f'\u00d7 {ResultStatus.WA.value}', style=COLOR_MAP[ResultStatus.WA]
|
254
|
-
),
|
255
|
-
ResultStatus.TLE: Text(
|
256
|
-
f'\u00d7 {ResultStatus.TLE.value}', style=COLOR_MAP[ResultStatus.TLE]
|
257
|
-
),
|
258
|
-
ResultStatus.MLE: Text(
|
259
|
-
f'\u00d7 {ResultStatus.MLE.value}', style=COLOR_MAP[ResultStatus.MLE]
|
260
|
-
),
|
261
|
-
ResultStatus.RE: Text(
|
262
|
-
f'\u00d7 {ResultStatus.RE.value}', style=COLOR_MAP[ResultStatus.RE]
|
263
|
-
),
|
264
|
-
ResultStatus.CE: Text(
|
265
|
-
f'\u00d7 {ResultStatus.CE.value}', style=COLOR_MAP[ResultStatus.CE]
|
266
|
-
),
|
267
|
-
ResultStatus.WJ: Text(
|
268
|
-
f'\u23f3 {ResultStatus.WJ.value}', style=COLOR_MAP[ResultStatus.WJ]
|
269
|
-
),
|
270
|
-
}
|
271
|
-
|
272
|
-
|
273
|
-
def create_renderable_test_info(test_info: TestInformation) -> RenderableType:
|
274
|
-
components = []
|
275
|
-
|
276
|
-
success_count = sum(
|
277
|
-
1 for result in test_info.resultlist if result.result.passed == ResultStatus.AC
|
278
|
-
)
|
279
|
-
total_count = test_info.case_number
|
280
|
-
|
281
|
-
# 結果に応じたスタイル付きのテキストを取得
|
282
|
-
status_text = STATUS_TEXT_MAP[test_info.result_summary]
|
283
|
-
|
284
|
-
# ヘッダーのテキストを構築
|
285
|
-
header_text = Text.assemble(
|
286
|
-
Text.from_markup(f'[cyan]{test_info.sourcename}[/]のテスト [\n]'),
|
287
|
-
Text.from_markup(f'[gray]コンパイルにかかった時間: [italic]{test_info.compile_time} ms[/][/][\n]') if test_info.compile_time else Text(''),
|
288
|
-
status_text,
|
289
|
-
Text.from_markup(
|
290
|
-
f' [{COLOR_MAP[test_info.result_summary]} bold]{success_count}[/] / [white bold]{total_count}[/]'
|
291
|
-
),
|
292
|
-
)
|
293
|
-
|
294
|
-
components.append(Panel(header_text, expand=False))
|
295
|
-
|
296
|
-
if test_info.compiler_message:
|
297
|
-
rule = Rule(
|
298
|
-
title='コンパイラーのメッセージ',
|
299
|
-
style=COLOR_MAP[ResultStatus.CE],
|
300
|
-
)
|
301
|
-
components.append(rule)
|
302
|
-
error_message = Syntax(
|
303
|
-
test_info.compiler_message, lang2str(test_info.lang), line_numbers=False
|
304
|
-
)
|
305
|
-
components.append(error_message)
|
306
|
-
|
307
|
-
return Group(*components)
|
308
|
-
|
309
|
-
|
310
|
-
def update_test_info(
|
311
|
-
test_info: TestInformation, test_result: LabeledTestCaseResult
|
312
|
-
) -> None:
|
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
|
338
|
-
|
339
|
-
|
340
|
-
def create_renderable_test_result(
|
341
|
-
i: int,
|
342
|
-
test_result: LabeledTestCaseResult,
|
343
|
-
) -> RenderableType:
|
344
|
-
rule = Rule(
|
345
|
-
title=f'No.{i+1} {test_result.label}',
|
346
|
-
style=COLOR_MAP[test_result.result.passed],
|
347
|
-
)
|
348
|
-
|
349
|
-
# 以下の部分は if-else ブロックの外に移動
|
350
|
-
status_header = Text.assemble(
|
351
|
-
'ステータス ',
|
352
|
-
STATUS_TEXT_MAP[test_result.result.passed], # status_text をここに追加
|
353
|
-
)
|
354
|
-
|
355
|
-
execution_time_text = None
|
356
|
-
if test_result.result.executed_time is not None:
|
357
|
-
execution_time_text = Text.from_markup(
|
358
|
-
f'実行時間 [cyan]{test_result.result.executed_time}[/cyan] ms'
|
359
|
-
)
|
360
|
-
|
361
|
-
table = Table(show_header=True, header_style='bold')
|
362
|
-
table.add_column('入力', style='cyan', min_width=10)
|
363
|
-
|
364
|
-
if test_result.result.passed != ResultStatus.AC:
|
365
|
-
table.add_column(
|
366
|
-
'出力', style=COLOR_MAP[test_result.result.passed], min_width=10
|
367
|
-
)
|
368
|
-
table.add_column('正解の出力', style=COLOR_MAP[ResultStatus.AC], min_width=10)
|
369
|
-
table.add_row(
|
370
|
-
escape(test_result.testcase.input),
|
371
|
-
escape(test_result.result.output),
|
372
|
-
escape(test_result.testcase.output),
|
373
|
-
)
|
374
|
-
else:
|
375
|
-
table.add_column(
|
376
|
-
'出力', style=COLOR_MAP[test_result.result.passed], min_width=10
|
377
|
-
)
|
378
|
-
table.add_row(
|
379
|
-
escape(test_result.testcase.input), escape(test_result.result.output)
|
380
|
-
)
|
381
|
-
|
382
|
-
components = [
|
383
|
-
rule,
|
384
|
-
status_header,
|
385
|
-
execution_time_text if execution_time_text else '',
|
386
|
-
table,
|
387
|
-
]
|
388
|
-
|
389
|
-
return Group(*components)
|
390
|
-
|
391
|
-
|
392
|
-
def render_results(
|
393
|
-
results: Generator[Union[LabeledTestCaseResult, TestInformation], None, None],
|
394
|
-
) -> None:
|
395
|
-
console = Console()
|
396
|
-
|
397
|
-
# 最初の結果は TestInformation として取得
|
398
|
-
first_result = next(results)
|
399
|
-
if not isinstance(first_result, TestInformation):
|
400
|
-
raise ValueError('最初のジェネレーターの結果はTestInformationです')
|
401
|
-
test_info: TestInformation = first_result
|
402
|
-
|
403
|
-
curernt_display_object = [test_info]
|
404
|
-
|
405
|
-
while True :
|
406
|
-
# 各テストケースの結果表示
|
407
|
-
for i, result in enumerate(results):
|
408
|
-
if isinstance(result, LabeledTestCaseResult):
|
409
|
-
console.print(create_renderable_test_result(i, result))
|
410
|
-
update_test_info(test_info, result)
|
411
|
-
else:
|
412
|
-
raise ValueError('テスト結果がyieldする型はLabeledTestCaseResultです')
|
413
|
-
|
414
|
-
console.print(create_renderable_test_info(test_info))
|
415
|
-
|
416
|
-
|
417
|
-
def run_test(path_of_code: str) -> None:
|
418
|
-
html_paths = [f for f in os.listdir('.') if f.endswith('.html')]
|
419
|
-
if not html_paths:
|
420
|
-
print(
|
421
|
-
'問題のファイルが見つかりません。\n問題のファイルが存在するディレクトリーに移動してから実行してください。'
|
422
|
-
)
|
423
|
-
return
|
424
|
-
|
425
|
-
with open(html_paths[0], 'r') as file:
|
426
|
-
html = file.read()
|
427
|
-
|
428
|
-
test_cases = create_testcases_from_html(html)
|
429
|
-
test_results = judge_code_from(test_cases, path_of_code)
|
430
|
-
render_results(test_results)
|
431
|
-
|
432
|
-
|
433
|
-
def test(*args: str) -> None:
|
434
|
-
execute_files(
|
435
|
-
*args,
|
436
|
-
func=run_test,
|
437
|
-
target_filetypes=INTERPRETED_LANGUAGES + COMPILED_LANGUAGES,
|
438
|
-
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|