ej-test-gen 0.4.1__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.
- ej_test_gen-0.4.1/LICENSE +21 -0
- ej_test_gen-0.4.1/PKG-INFO +98 -0
- ej_test_gen-0.4.1/README.md +86 -0
- ej_test_gen-0.4.1/pyproject.toml +42 -0
- ej_test_gen-0.4.1/setup.cfg +4 -0
- ej_test_gen-0.4.1/src/ej_test_gen/__about__.py +11 -0
- ej_test_gen-0.4.1/src/ej_test_gen/__init__.py +51 -0
- ej_test_gen-0.4.1/src/ej_test_gen/ej_test_gen.py +353 -0
- ej_test_gen-0.4.1/src/ej_test_gen.egg-info/PKG-INFO +98 -0
- ej_test_gen-0.4.1/src/ej_test_gen.egg-info/SOURCES.txt +11 -0
- ej_test_gen-0.4.1/src/ej_test_gen.egg-info/dependency_links.txt +1 -0
- ej_test_gen-0.4.1/src/ej_test_gen.egg-info/top_level.txt +1 -0
- ej_test_gen-0.4.1/tests/test_runner.py +579 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Sergey Shashkov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ej_test_gen
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Create tests for ejudge and co
|
|
5
|
+
Author-email: Sergey Shashkov <sh57@yandex.ru>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ShashkovS/ej_test_gen
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# Create tests for ejudge using solution
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
```bash
|
|
17
|
+
pip install git+https://github.com/ShashkovS/ej_test_gen.git --user --upgrade
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Example:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
# sol.py
|
|
24
|
+
n = int(input())
|
|
25
|
+
fct = 1
|
|
26
|
+
for i in range(2, n + 1):
|
|
27
|
+
fct *= i
|
|
28
|
+
print(fct)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
# test_creator.py
|
|
33
|
+
from ej_test_gen import TestRunner, random
|
|
34
|
+
runner = TestRunner(solution="sol.py", tests_dir="tests")
|
|
35
|
+
# runner = TestRunner(solution="sol.cpp", tests_dir="tests", use_WSL=True)
|
|
36
|
+
|
|
37
|
+
runner.test("""3""")
|
|
38
|
+
runner.test("""5""")
|
|
39
|
+
|
|
40
|
+
for tests_in_group, group_max in [(2, 10), (5, 50)]:
|
|
41
|
+
for __ in range(tests_in_group):
|
|
42
|
+
n = random.randint(0, group_max)
|
|
43
|
+
test = f'{n}'
|
|
44
|
+
runner.test(test)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
> python test_creator.py
|
|
50
|
+
001: 3 --> 6¶ Done! 0.27c
|
|
51
|
+
002: 2 --> 2¶ Done! 0.25c
|
|
52
|
+
003: 3 --> 6¶ Done! 0.26c
|
|
53
|
+
004: 31 --> 8222838654177922817725562880000000¶ Done! 0.25c
|
|
54
|
+
005: 10 --> 3628800¶ Done! 0.26c
|
|
55
|
+
006: 15 --> 1307674368000¶ Done! 0.26c
|
|
56
|
+
007: 41 --> 3345252661316380710817006205344075166515 Done! 0.27c
|
|
57
|
+
008: 15 --> 1307674368000¶ Done! 0.27c
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`TestRunner` resolves relative `solution` and `tests_dir` paths from the
|
|
61
|
+
directory of the script where `TestRunner` is created. This means `sol.py` and
|
|
62
|
+
`test_creator.py` can live in the same directory, and generated tests will be
|
|
63
|
+
written to `tests/` next to them even if you run the script from another
|
|
64
|
+
directory:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python path/to/test_creator.py
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Pass `working_dir` explicitly when you want a different base directory. For
|
|
71
|
+
example, `working_dir="."` keeps the old behavior where relative paths are
|
|
72
|
+
resolved from the process current working directory.
|
|
73
|
+
|
|
74
|
+
## Solution errors
|
|
75
|
+
|
|
76
|
+
Use `on_error` to choose what happens when the solution exits with a non-zero
|
|
77
|
+
return code or writes to stderr:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# default: stop generation and raise RuntimeError
|
|
81
|
+
runner = TestRunner(solution="sol.py", tests_dir="tests")
|
|
82
|
+
|
|
83
|
+
# ignore: skip failed tests and keep generating the next ones
|
|
84
|
+
runner = TestRunner(solution="sol.py", tests_dir="tests", on_error="ignore")
|
|
85
|
+
|
|
86
|
+
# output: write stderr/traceback to the answer file
|
|
87
|
+
runner = TestRunner(solution="sol.py", tests_dir="tests", on_error="output")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Old names are still accepted for compatibility: `on_error="raise"` means
|
|
91
|
+
`on_error="default"`, and `on_error="skip"` means `on_error="ignore"`.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# License
|
|
95
|
+
|
|
96
|
+
This is free and unencumbered software released into the public domain.
|
|
97
|
+
|
|
98
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Create tests for ejudge using solution
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
```bash
|
|
5
|
+
pip install git+https://github.com/ShashkovS/ej_test_gen.git --user --upgrade
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
## Example:
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
# sol.py
|
|
12
|
+
n = int(input())
|
|
13
|
+
fct = 1
|
|
14
|
+
for i in range(2, n + 1):
|
|
15
|
+
fct *= i
|
|
16
|
+
print(fct)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# test_creator.py
|
|
21
|
+
from ej_test_gen import TestRunner, random
|
|
22
|
+
runner = TestRunner(solution="sol.py", tests_dir="tests")
|
|
23
|
+
# runner = TestRunner(solution="sol.cpp", tests_dir="tests", use_WSL=True)
|
|
24
|
+
|
|
25
|
+
runner.test("""3""")
|
|
26
|
+
runner.test("""5""")
|
|
27
|
+
|
|
28
|
+
for tests_in_group, group_max in [(2, 10), (5, 50)]:
|
|
29
|
+
for __ in range(tests_in_group):
|
|
30
|
+
n = random.randint(0, group_max)
|
|
31
|
+
test = f'{n}'
|
|
32
|
+
runner.test(test)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
> python test_creator.py
|
|
38
|
+
001: 3 --> 6¶ Done! 0.27c
|
|
39
|
+
002: 2 --> 2¶ Done! 0.25c
|
|
40
|
+
003: 3 --> 6¶ Done! 0.26c
|
|
41
|
+
004: 31 --> 8222838654177922817725562880000000¶ Done! 0.25c
|
|
42
|
+
005: 10 --> 3628800¶ Done! 0.26c
|
|
43
|
+
006: 15 --> 1307674368000¶ Done! 0.26c
|
|
44
|
+
007: 41 --> 3345252661316380710817006205344075166515 Done! 0.27c
|
|
45
|
+
008: 15 --> 1307674368000¶ Done! 0.27c
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`TestRunner` resolves relative `solution` and `tests_dir` paths from the
|
|
49
|
+
directory of the script where `TestRunner` is created. This means `sol.py` and
|
|
50
|
+
`test_creator.py` can live in the same directory, and generated tests will be
|
|
51
|
+
written to `tests/` next to them even if you run the script from another
|
|
52
|
+
directory:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python path/to/test_creator.py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Pass `working_dir` explicitly when you want a different base directory. For
|
|
59
|
+
example, `working_dir="."` keeps the old behavior where relative paths are
|
|
60
|
+
resolved from the process current working directory.
|
|
61
|
+
|
|
62
|
+
## Solution errors
|
|
63
|
+
|
|
64
|
+
Use `on_error` to choose what happens when the solution exits with a non-zero
|
|
65
|
+
return code or writes to stderr:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# default: stop generation and raise RuntimeError
|
|
69
|
+
runner = TestRunner(solution="sol.py", tests_dir="tests")
|
|
70
|
+
|
|
71
|
+
# ignore: skip failed tests and keep generating the next ones
|
|
72
|
+
runner = TestRunner(solution="sol.py", tests_dir="tests", on_error="ignore")
|
|
73
|
+
|
|
74
|
+
# output: write stderr/traceback to the answer file
|
|
75
|
+
runner = TestRunner(solution="sol.py", tests_dir="tests", on_error="output")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Old names are still accepted for compatibility: `on_error="raise"` means
|
|
79
|
+
`on_error="default"`, and `on_error="skip"` means `on_error="ignore"`.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# License
|
|
83
|
+
|
|
84
|
+
This is free and unencumbered software released into the public domain.
|
|
85
|
+
|
|
86
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ej_test_gen"
|
|
7
|
+
version = "0.4.1" # Версию нужно будет поддерживать вручную
|
|
8
|
+
description = "Create tests for ejudge and co"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Sergey Shashkov", email = "sh57@yandex.ru"}
|
|
12
|
+
]
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
"Homepage" = "https://github.com/ShashkovS/ej_test_gen"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
package-dir = {"" = "src"}
|
|
24
|
+
include-package-data = true # Включает все указанные данные в пакет
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.package-data]
|
|
30
|
+
"*" = ["*.txt", "*.rst", "*.in", "*.png"]
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.3.5",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
# Здесь можно добавить консольные команды, если нужно
|
|
42
|
+
# пример: "command_name" = "module:function"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# ejudge test generator
|
|
3
|
+
|
|
4
|
+
# Example1:
|
|
5
|
+
from ej_test_gen import TestRunner, random
|
|
6
|
+
runner = TestRunner(solution="sol.py", tests_dir="tests")
|
|
7
|
+
runner.test("3")
|
|
8
|
+
|
|
9
|
+
# sol.py
|
|
10
|
+
n = int(input())
|
|
11
|
+
fct = 1
|
|
12
|
+
for i in range(2, n + 1):
|
|
13
|
+
fct *= i
|
|
14
|
+
print(fct)
|
|
15
|
+
|
|
16
|
+
# Example2:
|
|
17
|
+
runner2 = TestRunner(
|
|
18
|
+
solution='sol.py',
|
|
19
|
+
tests_dir='tests',
|
|
20
|
+
|
|
21
|
+
test_name_template='{:02}',
|
|
22
|
+
test_is_binary=False,
|
|
23
|
+
test_encoding="utf-8",
|
|
24
|
+
|
|
25
|
+
ans_name_template='{:02}.a',
|
|
26
|
+
ans_encoding="utf-8",
|
|
27
|
+
ans_is_binary=False,
|
|
28
|
+
|
|
29
|
+
py_executable=sys.executable,
|
|
30
|
+
cpp_compiler="g++",
|
|
31
|
+
timeout=5,
|
|
32
|
+
use_WSL=False,
|
|
33
|
+
compilation_timeout=30,
|
|
34
|
+
|
|
35
|
+
# 'default': raise RuntimeError when solution fails
|
|
36
|
+
# 'ignore': skip failed tests
|
|
37
|
+
# 'output': save stderr/traceback as the answer
|
|
38
|
+
on_error='default',
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# By default, relative solution and tests_dir paths are resolved from the
|
|
42
|
+
# directory of the script creating TestRunner. Pass working_dir explicitly
|
|
43
|
+
# when you want another base directory; working_dir='.' resolves paths from
|
|
44
|
+
# the process current working directory.
|
|
45
|
+
#
|
|
46
|
+
# Old on_error aliases are still accepted:
|
|
47
|
+
# 'raise' -> 'default', 'skip' -> 'ignore'.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from .__about__ import *
|
|
51
|
+
from .ej_test_gen import *
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-.
|
|
2
|
+
import inspect
|
|
3
|
+
import subprocess
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import sys
|
|
7
|
+
import logging
|
|
8
|
+
import random
|
|
9
|
+
import platform
|
|
10
|
+
|
|
11
|
+
random.seed(2000)
|
|
12
|
+
logging.basicConfig(level=logging.INFO)
|
|
13
|
+
lg = logging.getLogger('Runner')
|
|
14
|
+
|
|
15
|
+
__all__ = ['TestRunner', 'random']
|
|
16
|
+
|
|
17
|
+
_is_windows = platform.system() == 'Windows'
|
|
18
|
+
_ON_ERROR_ALIASES = {
|
|
19
|
+
'default': 'default',
|
|
20
|
+
'raise': 'default',
|
|
21
|
+
'ignore': 'ignore',
|
|
22
|
+
'skip': 'ignore',
|
|
23
|
+
'output': 'output',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class TestRunner:
|
|
27
|
+
__test__ = False
|
|
28
|
+
|
|
29
|
+
solution: str
|
|
30
|
+
working_dir: str
|
|
31
|
+
tests_dir: str
|
|
32
|
+
|
|
33
|
+
test_name_template: str
|
|
34
|
+
test_is_binary: bool
|
|
35
|
+
test_encoding: str
|
|
36
|
+
|
|
37
|
+
ans_name_template: str
|
|
38
|
+
ans_encoding: str
|
|
39
|
+
ans_is_binary: bool
|
|
40
|
+
|
|
41
|
+
py_executable: str
|
|
42
|
+
cpp_compiler: str
|
|
43
|
+
timeout: int
|
|
44
|
+
use_WSL: bool
|
|
45
|
+
compilation_timeout: int
|
|
46
|
+
|
|
47
|
+
on_error: str # 'default' | 'ignore' | 'output'
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
solution='sol.py',
|
|
52
|
+
working_dir=None,
|
|
53
|
+
tests_dir='.',
|
|
54
|
+
|
|
55
|
+
test_name_template='{:02}',
|
|
56
|
+
test_is_binary=False,
|
|
57
|
+
test_encoding="utf-8",
|
|
58
|
+
|
|
59
|
+
ans_name_template='{:02}.a',
|
|
60
|
+
ans_encoding="utf-8",
|
|
61
|
+
ans_is_binary=False,
|
|
62
|
+
|
|
63
|
+
py_executable=sys.executable,
|
|
64
|
+
cpp_compiler="g++",
|
|
65
|
+
timeout=5,
|
|
66
|
+
use_WSL=False,
|
|
67
|
+
compilation_timeout=30,
|
|
68
|
+
|
|
69
|
+
on_error='default',
|
|
70
|
+
):
|
|
71
|
+
self.__dict__.update({k: v for k, v in locals().items() if k != 'self'})
|
|
72
|
+
|
|
73
|
+
self.solution = os.fspath(self.solution)
|
|
74
|
+
if self.tests_dir is not None:
|
|
75
|
+
self.tests_dir = os.fspath(self.tests_dir)
|
|
76
|
+
self.on_error = self._normalize_on_error(self.on_error)
|
|
77
|
+
|
|
78
|
+
# working_dir=None: default to the directory of the script creating TestRunner.
|
|
79
|
+
# Explicit relative working_dir values keep the old cwd-based behavior.
|
|
80
|
+
if self.working_dir is None:
|
|
81
|
+
self.working_dir = self._default_working_dir()
|
|
82
|
+
else:
|
|
83
|
+
self.working_dir = os.fspath(self.working_dir)
|
|
84
|
+
if not os.path.isabs(self.working_dir):
|
|
85
|
+
self.working_dir = os.path.normpath(os.path.join(os.getcwd(), self.working_dir))
|
|
86
|
+
# tests_dir: relative paths resolved from working_dir
|
|
87
|
+
if not os.path.isabs(self.tests_dir):
|
|
88
|
+
self.tests_dir = os.path.normpath(os.path.join(self.working_dir, self.tests_dir))
|
|
89
|
+
|
|
90
|
+
os.makedirs(self.tests_dir, exist_ok=True)
|
|
91
|
+
self.compile_sol()
|
|
92
|
+
self._clean_up()
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _default_working_dir():
|
|
96
|
+
frame = inspect.currentframe()
|
|
97
|
+
try:
|
|
98
|
+
caller_frame = frame.f_back.f_back if frame and frame.f_back else None
|
|
99
|
+
filename = caller_frame.f_code.co_filename if caller_frame else None
|
|
100
|
+
if filename and not filename.startswith('<'):
|
|
101
|
+
return os.path.dirname(os.path.abspath(filename))
|
|
102
|
+
return os.getcwd()
|
|
103
|
+
finally:
|
|
104
|
+
del frame
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _normalize_on_error(on_error):
|
|
108
|
+
try:
|
|
109
|
+
return _ON_ERROR_ALIASES[on_error]
|
|
110
|
+
except KeyError:
|
|
111
|
+
allowed = "', '".join(sorted(_ON_ERROR_ALIASES))
|
|
112
|
+
raise ValueError("on_error must be one of: '{}'".format(allowed))
|
|
113
|
+
|
|
114
|
+
def __repr__(self):
|
|
115
|
+
return (f'{self.__class__.__name__}('
|
|
116
|
+
f' {self.solution=!r}, '
|
|
117
|
+
f' {self.working_dir=!r}, '
|
|
118
|
+
f' {self.tests_dir=!r}, '
|
|
119
|
+
f' {self.test_name_template=!r}, '
|
|
120
|
+
f' {self.test_is_binary=!r}, '
|
|
121
|
+
f' {self.test_encoding=!r}, '
|
|
122
|
+
f' {self.ans_name_template=!r}, '
|
|
123
|
+
f' {self.ans_encoding=!r}, '
|
|
124
|
+
f' {self.ans_is_binary=!r}, '
|
|
125
|
+
f' {self.py_executable=!r}, '
|
|
126
|
+
f' {self.cpp_compiler=!r}, '
|
|
127
|
+
f' {self.timeout=!r}, '
|
|
128
|
+
f' {self.use_WSL=!r}, '
|
|
129
|
+
f' {self.on_error=!r}'
|
|
130
|
+
f')')
|
|
131
|
+
|
|
132
|
+
def _run(self, to_stdin):
|
|
133
|
+
if self.test_is_binary:
|
|
134
|
+
input_data = to_stdin
|
|
135
|
+
else:
|
|
136
|
+
input_data = bytes(to_stdin, encoding=self.test_encoding)
|
|
137
|
+
st = time.time()
|
|
138
|
+
if self._compiled:
|
|
139
|
+
to_run = [self._compiled if os.path.isabs(self._compiled) else './' + self._compiled]
|
|
140
|
+
else: # TODO Вообще-то, это если питон
|
|
141
|
+
to_run = [self.py_executable, self.solution]
|
|
142
|
+
if self.use_WSL and _is_windows:
|
|
143
|
+
to_run = 'bash -c "{}"'.format(' '.join(to_run))
|
|
144
|
+
elif _is_windows:
|
|
145
|
+
to_run = ' '.join(to_run)
|
|
146
|
+
lg.debug(to_run)
|
|
147
|
+
pr = subprocess.Popen(
|
|
148
|
+
to_run,
|
|
149
|
+
stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
150
|
+
cwd=self.working_dir
|
|
151
|
+
)
|
|
152
|
+
stdout_bytes, stderr_bytes = pr.communicate(input=input_data, timeout=self.timeout)
|
|
153
|
+
dur = time.time() - st
|
|
154
|
+
if self.ans_is_binary:
|
|
155
|
+
from_stdout = stdout_bytes
|
|
156
|
+
else:
|
|
157
|
+
from_stdout = stdout_bytes.decode(self.ans_encoding, errors="ignore")
|
|
158
|
+
from_stdout = from_stdout.replace('\r\n', '\n').replace('\r', '\n')
|
|
159
|
+
trash_pos = from_stdout.find('pydev debugger:')
|
|
160
|
+
if trash_pos >= 0:
|
|
161
|
+
from_stdout = from_stdout[:trash_pos]
|
|
162
|
+
return from_stdout, stderr_bytes, pr.returncode, dur
|
|
163
|
+
|
|
164
|
+
def _clean_up(self):
|
|
165
|
+
# Удаляем старые тесты
|
|
166
|
+
for filename in os.listdir(self.tests_dir):
|
|
167
|
+
testname = filename
|
|
168
|
+
if testname.endswith('.a'):
|
|
169
|
+
testname = testname[:-2]
|
|
170
|
+
if testname.isdigit():
|
|
171
|
+
os.remove(os.path.join(self.tests_dir, filename))
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _list_test_files(path):
|
|
175
|
+
"""Find all files in given path which names contains only decimal digits.
|
|
176
|
+
Such files are considered as test files.
|
|
177
|
+
Result is sorted by int(test_name)"""
|
|
178
|
+
test_files = [tname for tname in os.listdir(path)
|
|
179
|
+
if tname.isdecimal() and os.path.isfile(os.path.join(path, tname))]
|
|
180
|
+
test_files.sort(key=lambda x: int(x))
|
|
181
|
+
return test_files
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def _prc_text_for_console(text, is_binary=False, max_len=40):
|
|
185
|
+
if is_binary:
|
|
186
|
+
show_text = str(text[:max_len])[:max_len]
|
|
187
|
+
else:
|
|
188
|
+
show_text = text[:max_len]
|
|
189
|
+
return show_text.replace('\r\n', '¶').replace('\n', '¶').replace('\r', '¶')
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _cmp_two_outputs(output1, output2):
|
|
193
|
+
eq = output1.rstrip() == output2.rstrip()
|
|
194
|
+
return eq, ''
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def _read_test_or_ans(file_full_path, is_binary, encoding):
|
|
198
|
+
open_parms = dict(file=file_full_path, mode='r')
|
|
199
|
+
if is_binary:
|
|
200
|
+
open_parms['mode'] = 'rb'
|
|
201
|
+
else:
|
|
202
|
+
open_parms['encoding'] = encoding
|
|
203
|
+
open_parms['errors'] = "ignore"
|
|
204
|
+
with open(**open_parms) as f:
|
|
205
|
+
test_data = f.read()
|
|
206
|
+
return test_data
|
|
207
|
+
|
|
208
|
+
def _stderr_as_answer(self, stderr_bytes, returncode):
|
|
209
|
+
if stderr_bytes:
|
|
210
|
+
if self.ans_is_binary:
|
|
211
|
+
return stderr_bytes
|
|
212
|
+
return stderr_bytes.decode(self.ans_encoding, errors='replace').replace('\r\n', '\n').replace('\r', '\n')
|
|
213
|
+
fallback = 'Solution exited with returncode={}\n'.format(returncode)
|
|
214
|
+
if self.ans_is_binary:
|
|
215
|
+
return fallback.encode(self.ans_encoding, errors='replace')
|
|
216
|
+
return fallback
|
|
217
|
+
|
|
218
|
+
def _run_given_tests(self, test_files):
|
|
219
|
+
for tname in test_files:
|
|
220
|
+
lg.info('Processing test ' + tname + '...')
|
|
221
|
+
|
|
222
|
+
# First we read the test
|
|
223
|
+
try:
|
|
224
|
+
test_data = self._read_test_or_ans(
|
|
225
|
+
os.path.join(self.tests_dir, tname), self.test_is_binary, self.test_encoding
|
|
226
|
+
)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
lg.error('Error while reading test ' + tname + ': ' + str(e))
|
|
229
|
+
continue
|
|
230
|
+
# Then we read the result
|
|
231
|
+
tname += self.ans_suffix
|
|
232
|
+
try:
|
|
233
|
+
ans_data = self._read_test_or_ans(
|
|
234
|
+
os.path.join(self.tests_dir, tname), self.ans_is_binary, self.ans_encoding
|
|
235
|
+
)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
lg.error('Error while reading test result for ' + tname + ': ' + str(e))
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
# Ok, now we are ready to run pgm
|
|
241
|
+
try:
|
|
242
|
+
from_stdout, stderr_bytes, returncode, dur = self._run(to_stdin=test_data)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
lg.error('Error while running test ' + tname + ': ' + str(e))
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
show_test = self._prc_text_for_console(test_data, self.test_is_binary)
|
|
248
|
+
show_res = self._prc_text_for_console(from_stdout, self.ans_is_binary)
|
|
249
|
+
show_ans = self._prc_text_for_console(ans_data, self.ans_is_binary)
|
|
250
|
+
|
|
251
|
+
eq, description = self._cmp_two_outputs(ans_data, from_stdout)
|
|
252
|
+
msg = 'Test {}, {}. Dur:{:0.2f}. {} -> {} (Corr: {})'.format(
|
|
253
|
+
tname, "OK" if eq else "WA", dur, show_test, show_res, show_ans
|
|
254
|
+
)
|
|
255
|
+
if eq:
|
|
256
|
+
lg.info(msg)
|
|
257
|
+
else:
|
|
258
|
+
lg.error(msg)
|
|
259
|
+
|
|
260
|
+
def compile_sol(self):
|
|
261
|
+
self._compiled = None
|
|
262
|
+
name, _, ext = self.solution.rpartition('.')
|
|
263
|
+
ext = ext.lower()
|
|
264
|
+
if ext == 'py':
|
|
265
|
+
return
|
|
266
|
+
elif ext == 'cpp':
|
|
267
|
+
self._compiled = '{}.exe'.format(name) if _is_windows else name
|
|
268
|
+
compiled_path = self._compiled
|
|
269
|
+
if not os.path.isabs(compiled_path):
|
|
270
|
+
compiled_path = os.path.join(self.working_dir, compiled_path)
|
|
271
|
+
if os.path.isfile(compiled_path):
|
|
272
|
+
os.remove(compiled_path)
|
|
273
|
+
cmd = [self.cpp_compiler, self.solution, '-o', self._compiled]
|
|
274
|
+
if self.use_WSL and _is_windows:
|
|
275
|
+
cmd = 'bash -c "{}"'.format(' '.join(cmd))
|
|
276
|
+
elif _is_windows:
|
|
277
|
+
cmd = ' '.join(cmd)
|
|
278
|
+
|
|
279
|
+
lg.debug(cmd)
|
|
280
|
+
pr = subprocess.Popen(
|
|
281
|
+
cmd,
|
|
282
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
283
|
+
cwd=self.working_dir
|
|
284
|
+
)
|
|
285
|
+
stdout, stderr = pr.communicate(timeout=self.compilation_timeout)
|
|
286
|
+
lg.debug('stdout={}\nstderr={}'.format(stdout, stderr))
|
|
287
|
+
if stderr:
|
|
288
|
+
raise EnvironmentError(stderr.decode('utf-8', 'ignore'))
|
|
289
|
+
|
|
290
|
+
def run_test(self):
|
|
291
|
+
test_files = self._list_test_files(self.tests_dir)
|
|
292
|
+
self._run_given_tests(test_files)
|
|
293
|
+
|
|
294
|
+
def test(self, test, *, _test_num=[0], _max_len=40):
|
|
295
|
+
test = test.strip()
|
|
296
|
+
if not self.test_is_binary:
|
|
297
|
+
test += '\n'
|
|
298
|
+
text_prt = self._prc_text_for_console(test, self.test_is_binary)
|
|
299
|
+
_test_num[0] += 1
|
|
300
|
+
_test_num_str = self.test_name_template.format(_test_num[0])
|
|
301
|
+
_ans_num_str = self.ans_name_template.format(_test_num[0])
|
|
302
|
+
print(
|
|
303
|
+
'{}: {}{} --> '.format(
|
|
304
|
+
_test_num_str, text_prt[:_max_len].ljust(_max_len),
|
|
305
|
+
'...' if text_prt[_max_len:] else ' '
|
|
306
|
+
), end=''
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
ans, stderr_bytes, returncode, dur = self._run(test)
|
|
310
|
+
if not self.ans_is_binary and not ans.endswith('\n'):
|
|
311
|
+
ans += '\n'
|
|
312
|
+
|
|
313
|
+
failed = returncode != 0 or bool(stderr_bytes)
|
|
314
|
+
if failed:
|
|
315
|
+
stderr_preview = stderr_bytes.decode('utf-8', errors='replace')[:200] if stderr_bytes else ''
|
|
316
|
+
if self.on_error == 'default':
|
|
317
|
+
print('ERROR (returncode={})'.format(returncode))
|
|
318
|
+
raise RuntimeError(
|
|
319
|
+
'Test {}: solution exited with returncode={}, stderr={!r}'.format(
|
|
320
|
+
_test_num_str, returncode, stderr_preview
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
elif self.on_error == 'ignore':
|
|
324
|
+
print('skipped (returncode={}{})'.format(
|
|
325
|
+
returncode,
|
|
326
|
+
', stderr: ' + stderr_preview if stderr_preview else ''
|
|
327
|
+
))
|
|
328
|
+
_test_num[0] -= 1
|
|
329
|
+
return
|
|
330
|
+
else: # 'output'
|
|
331
|
+
ans = self._stderr_as_answer(stderr_bytes, returncode)
|
|
332
|
+
|
|
333
|
+
ans_prt = self._prc_text_for_console(ans, self.ans_is_binary)
|
|
334
|
+
if dur <= self.timeout:
|
|
335
|
+
print(
|
|
336
|
+
'{}{} Done! {:.2}c'.format(
|
|
337
|
+
ans_prt[:_max_len].ljust(_max_len),
|
|
338
|
+
'...' if ans_prt[_max_len:] else ' ', dur
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
with open(os.path.join(self.tests_dir, _test_num_str), 'w' + ('b' if self.test_is_binary else '')) as f:
|
|
343
|
+
f.write(test)
|
|
344
|
+
with open(os.path.join(self.tests_dir, _ans_num_str), 'w' + ('b' if self.ans_is_binary else '')) as f:
|
|
345
|
+
f.write(ans)
|
|
346
|
+
else:
|
|
347
|
+
print('timeout', '{:.2}c'.format(dur))
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
if __name__ == '__main__':
|
|
351
|
+
# runner = TestRunner()
|
|
352
|
+
# runner.run_test()
|
|
353
|
+
print('sudo rm -rf, are you sure? Ok, type "password".')
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ej_test_gen
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Create tests for ejudge and co
|
|
5
|
+
Author-email: Sergey Shashkov <sh57@yandex.ru>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ShashkovS/ej_test_gen
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# Create tests for ejudge using solution
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
```bash
|
|
17
|
+
pip install git+https://github.com/ShashkovS/ej_test_gen.git --user --upgrade
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Example:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
# sol.py
|
|
24
|
+
n = int(input())
|
|
25
|
+
fct = 1
|
|
26
|
+
for i in range(2, n + 1):
|
|
27
|
+
fct *= i
|
|
28
|
+
print(fct)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
# test_creator.py
|
|
33
|
+
from ej_test_gen import TestRunner, random
|
|
34
|
+
runner = TestRunner(solution="sol.py", tests_dir="tests")
|
|
35
|
+
# runner = TestRunner(solution="sol.cpp", tests_dir="tests", use_WSL=True)
|
|
36
|
+
|
|
37
|
+
runner.test("""3""")
|
|
38
|
+
runner.test("""5""")
|
|
39
|
+
|
|
40
|
+
for tests_in_group, group_max in [(2, 10), (5, 50)]:
|
|
41
|
+
for __ in range(tests_in_group):
|
|
42
|
+
n = random.randint(0, group_max)
|
|
43
|
+
test = f'{n}'
|
|
44
|
+
runner.test(test)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
> python test_creator.py
|
|
50
|
+
001: 3 --> 6¶ Done! 0.27c
|
|
51
|
+
002: 2 --> 2¶ Done! 0.25c
|
|
52
|
+
003: 3 --> 6¶ Done! 0.26c
|
|
53
|
+
004: 31 --> 8222838654177922817725562880000000¶ Done! 0.25c
|
|
54
|
+
005: 10 --> 3628800¶ Done! 0.26c
|
|
55
|
+
006: 15 --> 1307674368000¶ Done! 0.26c
|
|
56
|
+
007: 41 --> 3345252661316380710817006205344075166515 Done! 0.27c
|
|
57
|
+
008: 15 --> 1307674368000¶ Done! 0.27c
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`TestRunner` resolves relative `solution` and `tests_dir` paths from the
|
|
61
|
+
directory of the script where `TestRunner` is created. This means `sol.py` and
|
|
62
|
+
`test_creator.py` can live in the same directory, and generated tests will be
|
|
63
|
+
written to `tests/` next to them even if you run the script from another
|
|
64
|
+
directory:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python path/to/test_creator.py
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Pass `working_dir` explicitly when you want a different base directory. For
|
|
71
|
+
example, `working_dir="."` keeps the old behavior where relative paths are
|
|
72
|
+
resolved from the process current working directory.
|
|
73
|
+
|
|
74
|
+
## Solution errors
|
|
75
|
+
|
|
76
|
+
Use `on_error` to choose what happens when the solution exits with a non-zero
|
|
77
|
+
return code or writes to stderr:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# default: stop generation and raise RuntimeError
|
|
81
|
+
runner = TestRunner(solution="sol.py", tests_dir="tests")
|
|
82
|
+
|
|
83
|
+
# ignore: skip failed tests and keep generating the next ones
|
|
84
|
+
runner = TestRunner(solution="sol.py", tests_dir="tests", on_error="ignore")
|
|
85
|
+
|
|
86
|
+
# output: write stderr/traceback to the answer file
|
|
87
|
+
runner = TestRunner(solution="sol.py", tests_dir="tests", on_error="output")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Old names are still accepted for compatibility: `on_error="raise"` means
|
|
91
|
+
`on_error="default"`, and `on_error="skip"` means `on_error="ignore"`.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# License
|
|
95
|
+
|
|
96
|
+
This is free and unencumbered software released into the public domain.
|
|
97
|
+
|
|
98
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/ej_test_gen/__about__.py
|
|
5
|
+
src/ej_test_gen/__init__.py
|
|
6
|
+
src/ej_test_gen/ej_test_gen.py
|
|
7
|
+
src/ej_test_gen.egg-info/PKG-INFO
|
|
8
|
+
src/ej_test_gen.egg-info/SOURCES.txt
|
|
9
|
+
src/ej_test_gen.egg-info/dependency_links.txt
|
|
10
|
+
src/ej_test_gen.egg-info/top_level.txt
|
|
11
|
+
tests/test_runner.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ej_test_gen
|
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import sqlite3
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
|
|
12
|
+
from ej_test_gen import TestRunner
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def write_solution(tmp_path, code):
|
|
16
|
+
sol = tmp_path / 'sol.py'
|
|
17
|
+
sol.write_text(code, encoding='utf-8')
|
|
18
|
+
return str(sol)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_input_files(tmp_path):
|
|
22
|
+
"""Return sorted list of test input files (names are all digits)."""
|
|
23
|
+
files = [f for f in tmp_path.iterdir() if f.name.isdecimal()]
|
|
24
|
+
return sorted(files, key=lambda f: int(f.name))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_answer_file(input_file):
|
|
28
|
+
return input_file.parent / (input_file.name + '.a')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Text input / text output
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
class TestTextIO:
|
|
36
|
+
def test_echo(self, tmp_path):
|
|
37
|
+
sol = write_solution(tmp_path, "print(input())")
|
|
38
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path))
|
|
39
|
+
runner.test('hello')
|
|
40
|
+
|
|
41
|
+
inputs = get_input_files(tmp_path)
|
|
42
|
+
assert len(inputs) == 1
|
|
43
|
+
assert inputs[0].read_text(encoding='utf-8') == 'hello\n'
|
|
44
|
+
assert get_answer_file(inputs[0]).read_text(encoding='utf-8').strip() == 'hello'
|
|
45
|
+
|
|
46
|
+
def test_text_files_have_final_newline(self, tmp_path):
|
|
47
|
+
sol = write_solution(tmp_path, "import sys; sys.stdout.write(input())")
|
|
48
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path))
|
|
49
|
+
runner.test('hello\n\n')
|
|
50
|
+
|
|
51
|
+
input_file = get_input_files(tmp_path)[0]
|
|
52
|
+
assert input_file.read_bytes() == b'hello\n'
|
|
53
|
+
assert get_answer_file(input_file).read_bytes() == b'hello\n'
|
|
54
|
+
|
|
55
|
+
def test_arithmetic(self, tmp_path):
|
|
56
|
+
sol = write_solution(tmp_path, "a, b = map(int, input().split()); print(a + b)")
|
|
57
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path))
|
|
58
|
+
runner.test('3 4')
|
|
59
|
+
|
|
60
|
+
inputs = get_input_files(tmp_path)
|
|
61
|
+
assert get_answer_file(inputs[0]).read_text(encoding='utf-8').strip() == '7'
|
|
62
|
+
|
|
63
|
+
def test_multiple_tests(self, tmp_path):
|
|
64
|
+
sol = write_solution(tmp_path, "print(int(input()) * 2)")
|
|
65
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path))
|
|
66
|
+
runner.test('3')
|
|
67
|
+
runner.test('5')
|
|
68
|
+
|
|
69
|
+
inputs = get_input_files(tmp_path)
|
|
70
|
+
assert len(inputs) == 2
|
|
71
|
+
answers = [get_answer_file(f).read_text().strip() for f in inputs]
|
|
72
|
+
assert answers == ['6', '10']
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Binary input / text output
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
class TestBinaryInput:
|
|
80
|
+
def test_binary_stdin_byte_count(self, tmp_path):
|
|
81
|
+
"""Binary input (arbitrary bytes) → text output: length of data."""
|
|
82
|
+
code = "import sys; data = sys.stdin.buffer.read(); print(len(data))"
|
|
83
|
+
sol = write_solution(tmp_path, code)
|
|
84
|
+
binary_data = bytes(range(256))
|
|
85
|
+
|
|
86
|
+
runner = TestRunner(
|
|
87
|
+
solution=sol,
|
|
88
|
+
working_dir=str(tmp_path),
|
|
89
|
+
test_is_binary=True,
|
|
90
|
+
ans_is_binary=False,
|
|
91
|
+
)
|
|
92
|
+
runner.test(binary_data)
|
|
93
|
+
|
|
94
|
+
inputs = get_input_files(tmp_path)
|
|
95
|
+
assert inputs[0].read_bytes() == binary_data
|
|
96
|
+
assert get_answer_file(inputs[0]).read_text().strip() == '256'
|
|
97
|
+
|
|
98
|
+
def test_binary_stdin_null_bytes(self, tmp_path):
|
|
99
|
+
"""Binary input containing null bytes (common in binary files)."""
|
|
100
|
+
code = "import sys; d = sys.stdin.buffer.read(); print(d.count(0))"
|
|
101
|
+
sol = write_solution(tmp_path, code)
|
|
102
|
+
binary_data = bytes([0x00, 0x01, 0x00, 0xFF, 0x00]) # three null bytes
|
|
103
|
+
|
|
104
|
+
runner = TestRunner(
|
|
105
|
+
solution=sol,
|
|
106
|
+
working_dir=str(tmp_path),
|
|
107
|
+
test_is_binary=True,
|
|
108
|
+
ans_is_binary=False,
|
|
109
|
+
)
|
|
110
|
+
runner.test(binary_data)
|
|
111
|
+
|
|
112
|
+
inputs = get_input_files(tmp_path)
|
|
113
|
+
assert inputs[0].read_bytes() == binary_data
|
|
114
|
+
assert get_answer_file(inputs[0]).read_text().strip() == '3'
|
|
115
|
+
|
|
116
|
+
def test_sqlite_database_input(self, tmp_path):
|
|
117
|
+
"""Binary input is a SQLite database file."""
|
|
118
|
+
# Build a tiny DB
|
|
119
|
+
db_path = tmp_path / 'input.db'
|
|
120
|
+
conn = sqlite3.connect(str(db_path))
|
|
121
|
+
conn.execute('CREATE TABLE nums (val INTEGER)')
|
|
122
|
+
conn.execute('INSERT INTO nums VALUES (42)')
|
|
123
|
+
conn.commit()
|
|
124
|
+
conn.close()
|
|
125
|
+
db_bytes = db_path.read_bytes()
|
|
126
|
+
|
|
127
|
+
code = '''
|
|
128
|
+
import sys, sqlite3, tempfile, os
|
|
129
|
+
data = sys.stdin.buffer.read()
|
|
130
|
+
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
|
|
131
|
+
f.write(data)
|
|
132
|
+
fname = f.name
|
|
133
|
+
conn = sqlite3.connect(fname)
|
|
134
|
+
row = conn.execute('SELECT val FROM nums').fetchone()
|
|
135
|
+
conn.close()
|
|
136
|
+
os.unlink(fname)
|
|
137
|
+
print(row[0])
|
|
138
|
+
'''
|
|
139
|
+
sol = write_solution(tmp_path, code)
|
|
140
|
+
runner = TestRunner(
|
|
141
|
+
solution=sol,
|
|
142
|
+
working_dir=str(tmp_path),
|
|
143
|
+
test_is_binary=True,
|
|
144
|
+
ans_is_binary=False,
|
|
145
|
+
)
|
|
146
|
+
runner.test(db_bytes)
|
|
147
|
+
|
|
148
|
+
inputs = get_input_files(tmp_path)
|
|
149
|
+
assert inputs[0].read_bytes() == db_bytes
|
|
150
|
+
assert get_answer_file(inputs[0]).read_text().strip() == '42'
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# Text input / binary output
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
class TestBinaryOutput:
|
|
158
|
+
def test_text_in_binary_out(self, tmp_path):
|
|
159
|
+
"""Text input → binary output: emit n sequential bytes."""
|
|
160
|
+
code = "import sys; n = int(input()); sys.stdout.buffer.write(bytes(range(n)))"
|
|
161
|
+
sol = write_solution(tmp_path, code)
|
|
162
|
+
|
|
163
|
+
runner = TestRunner(
|
|
164
|
+
solution=sol,
|
|
165
|
+
working_dir=str(tmp_path),
|
|
166
|
+
test_is_binary=False,
|
|
167
|
+
ans_is_binary=True,
|
|
168
|
+
)
|
|
169
|
+
runner.test('5')
|
|
170
|
+
|
|
171
|
+
inputs = get_input_files(tmp_path)
|
|
172
|
+
assert inputs[0].read_text().strip() == '5'
|
|
173
|
+
assert get_answer_file(inputs[0]).read_bytes() == bytes(range(5))
|
|
174
|
+
|
|
175
|
+
def test_text_in_binary_out_with_nulls(self, tmp_path):
|
|
176
|
+
"""Binary output must preserve null bytes."""
|
|
177
|
+
code = "import sys; sys.stdout.buffer.write(bytes([0, 1, 0, 2, 0]))"
|
|
178
|
+
sol = write_solution(tmp_path, code)
|
|
179
|
+
|
|
180
|
+
runner = TestRunner(
|
|
181
|
+
solution=sol,
|
|
182
|
+
working_dir=str(tmp_path),
|
|
183
|
+
test_is_binary=False,
|
|
184
|
+
ans_is_binary=True,
|
|
185
|
+
)
|
|
186
|
+
runner.test('ignored')
|
|
187
|
+
|
|
188
|
+
inputs = get_input_files(tmp_path)
|
|
189
|
+
assert get_answer_file(inputs[0]).read_bytes() == bytes([0, 1, 0, 2, 0])
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Binary input / binary output
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
class TestBinaryInputOutput:
|
|
197
|
+
def test_invert_bytes(self, tmp_path):
|
|
198
|
+
"""Binary in/out: bitwise invert every byte."""
|
|
199
|
+
code = "import sys; d = sys.stdin.buffer.read(); sys.stdout.buffer.write(bytes(b ^ 0xFF for b in d))"
|
|
200
|
+
sol = write_solution(tmp_path, code)
|
|
201
|
+
|
|
202
|
+
input_bytes = bytes([0x00, 0xFF, 0xAA, 0x55])
|
|
203
|
+
expected = bytes([0xFF, 0x00, 0x55, 0xAA])
|
|
204
|
+
|
|
205
|
+
runner = TestRunner(
|
|
206
|
+
solution=sol,
|
|
207
|
+
working_dir=str(tmp_path),
|
|
208
|
+
test_is_binary=True,
|
|
209
|
+
ans_is_binary=True,
|
|
210
|
+
)
|
|
211
|
+
runner.test(input_bytes)
|
|
212
|
+
|
|
213
|
+
inputs = get_input_files(tmp_path)
|
|
214
|
+
assert inputs[0].read_bytes() == input_bytes
|
|
215
|
+
assert get_answer_file(inputs[0]).read_bytes() == expected
|
|
216
|
+
|
|
217
|
+
def test_image_like_binary(self, tmp_path):
|
|
218
|
+
"""Binary in/out: strip first 8 bytes (simulate PNG header extraction)."""
|
|
219
|
+
png_header = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
|
|
220
|
+
input_bytes = png_header + bytes(range(100))
|
|
221
|
+
|
|
222
|
+
code = "import sys; d = sys.stdin.buffer.read(); sys.stdout.buffer.write(d[:8])"
|
|
223
|
+
sol = write_solution(tmp_path, code)
|
|
224
|
+
|
|
225
|
+
runner = TestRunner(
|
|
226
|
+
solution=sol,
|
|
227
|
+
working_dir=str(tmp_path),
|
|
228
|
+
test_is_binary=True,
|
|
229
|
+
ans_is_binary=True,
|
|
230
|
+
)
|
|
231
|
+
runner.test(input_bytes)
|
|
232
|
+
|
|
233
|
+
inputs = get_input_files(tmp_path)
|
|
234
|
+
assert inputs[0].read_bytes() == input_bytes
|
|
235
|
+
assert get_answer_file(inputs[0]).read_bytes() == png_header
|
|
236
|
+
|
|
237
|
+
def test_binary_passthrough_large(self, tmp_path):
|
|
238
|
+
"""Binary in/out: 1 KB of non-whitespace bytes are faithfully preserved."""
|
|
239
|
+
# Avoid trailing whitespace bytes (0x00–0x08, 0x0e–0x1f, 0x21–0xff are safe)
|
|
240
|
+
input_bytes = bytes([((i * 7 + 33) % 223) + 33 for i in range(1024)])
|
|
241
|
+
code = "import sys; sys.stdout.buffer.write(sys.stdin.buffer.read())"
|
|
242
|
+
sol = write_solution(tmp_path, code)
|
|
243
|
+
runner = TestRunner(
|
|
244
|
+
solution=sol,
|
|
245
|
+
working_dir=str(tmp_path),
|
|
246
|
+
test_is_binary=True,
|
|
247
|
+
ans_is_binary=True,
|
|
248
|
+
)
|
|
249
|
+
runner.test(input_bytes)
|
|
250
|
+
|
|
251
|
+
inputs = get_input_files(tmp_path)
|
|
252
|
+
assert inputs[0].read_bytes() == input_bytes
|
|
253
|
+
assert get_answer_file(inputs[0]).read_bytes() == input_bytes
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ---------------------------------------------------------------------------
|
|
257
|
+
# use_WSL=True ignored on non-Windows
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
# tests_dir
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
class TestTestsDir:
|
|
265
|
+
def test_default_tests_dir_equals_working_dir(self, tmp_path):
|
|
266
|
+
"""Default tests_dir='.' stores files directly in working_dir."""
|
|
267
|
+
sol = write_solution(tmp_path, "print(int(input()) + 1)")
|
|
268
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path))
|
|
269
|
+
runner.test('9')
|
|
270
|
+
|
|
271
|
+
inputs = get_input_files(tmp_path)
|
|
272
|
+
assert len(inputs) == 1
|
|
273
|
+
assert get_answer_file(inputs[0]).read_text().strip() == '10'
|
|
274
|
+
|
|
275
|
+
def test_relative_tests_dir_created_automatically(self, tmp_path):
|
|
276
|
+
"""Relative tests_dir is created under working_dir automatically."""
|
|
277
|
+
sol = write_solution(tmp_path, "print(int(input()) * 3)")
|
|
278
|
+
subdir = tmp_path / 'generated'
|
|
279
|
+
assert not subdir.exists()
|
|
280
|
+
|
|
281
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), tests_dir='generated')
|
|
282
|
+
assert subdir.exists()
|
|
283
|
+
|
|
284
|
+
runner.test('7')
|
|
285
|
+
inputs = get_input_files(subdir)
|
|
286
|
+
assert len(inputs) == 1
|
|
287
|
+
assert get_answer_file(inputs[0]).read_text().strip() == '21'
|
|
288
|
+
# Root working_dir must stay clean of test files
|
|
289
|
+
assert get_input_files(tmp_path) == []
|
|
290
|
+
|
|
291
|
+
def test_nested_relative_tests_dir_created(self, tmp_path):
|
|
292
|
+
"""Multi-level relative tests_dir is created with makedirs."""
|
|
293
|
+
sol = write_solution(tmp_path, "print('hi')")
|
|
294
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), tests_dir='a/b/c')
|
|
295
|
+
nested = tmp_path / 'a' / 'b' / 'c'
|
|
296
|
+
assert nested.exists()
|
|
297
|
+
|
|
298
|
+
runner.test('x')
|
|
299
|
+
assert len(get_input_files(nested)) == 1
|
|
300
|
+
|
|
301
|
+
def test_absolute_tests_dir(self, tmp_path):
|
|
302
|
+
"""Absolute tests_dir path is used as-is regardless of working_dir."""
|
|
303
|
+
sol_dir = tmp_path / 'sol'
|
|
304
|
+
sol_dir.mkdir()
|
|
305
|
+
out_dir = tmp_path / 'out'
|
|
306
|
+
# out_dir does not exist yet
|
|
307
|
+
|
|
308
|
+
sol = write_solution(sol_dir, "print(42)")
|
|
309
|
+
runner = TestRunner(
|
|
310
|
+
solution=str(sol),
|
|
311
|
+
working_dir=str(sol_dir),
|
|
312
|
+
tests_dir=str(out_dir), # absolute
|
|
313
|
+
)
|
|
314
|
+
assert out_dir.exists()
|
|
315
|
+
|
|
316
|
+
runner.test('ignored')
|
|
317
|
+
assert len(get_input_files(out_dir)) == 1
|
|
318
|
+
assert get_input_files(sol_dir) == []
|
|
319
|
+
|
|
320
|
+
def test_existing_tests_dir_ok(self, tmp_path):
|
|
321
|
+
"""If tests_dir already exists, no error is raised."""
|
|
322
|
+
subdir = tmp_path / 'tests'
|
|
323
|
+
subdir.mkdir()
|
|
324
|
+
sol = write_solution(tmp_path, "print('x')")
|
|
325
|
+
# Should not raise even though the directory exists
|
|
326
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), tests_dir='tests')
|
|
327
|
+
runner.test('y')
|
|
328
|
+
assert len(get_input_files(subdir)) == 1
|
|
329
|
+
|
|
330
|
+
def test_cleanup_only_affects_tests_dir(self, tmp_path):
|
|
331
|
+
"""_clean_up removes old test files only from tests_dir, not working_dir root."""
|
|
332
|
+
sol = write_solution(tmp_path, "print('ok')")
|
|
333
|
+
subdir = tmp_path / 'tests'
|
|
334
|
+
|
|
335
|
+
# Pre-populate tests_dir with stale test files
|
|
336
|
+
subdir.mkdir()
|
|
337
|
+
(subdir / '01').write_text('old input')
|
|
338
|
+
(subdir / '01.a').write_text('old answer')
|
|
339
|
+
|
|
340
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), tests_dir='tests')
|
|
341
|
+
# _clean_up is called in __init__; stale files should be gone
|
|
342
|
+
assert not (subdir / '01').exists()
|
|
343
|
+
assert not (subdir / '01.a').exists()
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
# working_dir resolution
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
class TestWorkingDir:
|
|
351
|
+
def test_default_working_dir_is_creator_script_dir(self, tmp_path):
|
|
352
|
+
"""Without working_dir, resolve sol.py and tests_dir relative to gen.py."""
|
|
353
|
+
src_path = Path(__file__).parent.parent / 'src'
|
|
354
|
+
task_dir = tmp_path / 'task'
|
|
355
|
+
task_dir.mkdir()
|
|
356
|
+
(task_dir / 'sol.py').write_text("print(input())\n", encoding='utf-8')
|
|
357
|
+
(task_dir / 'gen.py').write_text(
|
|
358
|
+
"import sys\n"
|
|
359
|
+
f"sys.path.insert(0, {str(src_path)!r})\n"
|
|
360
|
+
"from ej_test_gen import TestRunner\n"
|
|
361
|
+
"runner = TestRunner(solution='sol.py', tests_dir='tests')\n"
|
|
362
|
+
"runner.test('hello')\n",
|
|
363
|
+
encoding='utf-8',
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
result = subprocess.run(
|
|
367
|
+
[sys.executable, str(task_dir / 'gen.py')],
|
|
368
|
+
cwd=str(tmp_path),
|
|
369
|
+
capture_output=True,
|
|
370
|
+
text=True,
|
|
371
|
+
check=False,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
assert result.returncode == 0, result.stderr
|
|
375
|
+
assert (task_dir / 'tests' / '01').read_text(encoding='utf-8') == 'hello\n'
|
|
376
|
+
assert (task_dir / 'tests' / '01.a').read_text(encoding='utf-8').strip() == 'hello'
|
|
377
|
+
assert not (tmp_path / 'tests').exists()
|
|
378
|
+
|
|
379
|
+
def test_explicit_relative_working_dir_uses_process_cwd(self, tmp_path):
|
|
380
|
+
"""Explicit working_dir='.' keeps the old cwd-based behavior."""
|
|
381
|
+
src_path = Path(__file__).parent.parent / 'src'
|
|
382
|
+
cwd_dir = tmp_path / 'cwd'
|
|
383
|
+
task_dir = tmp_path / 'task'
|
|
384
|
+
cwd_dir.mkdir()
|
|
385
|
+
task_dir.mkdir()
|
|
386
|
+
(cwd_dir / 'sol.py').write_text("print(input()[::-1])\n", encoding='utf-8')
|
|
387
|
+
(task_dir / 'gen.py').write_text(
|
|
388
|
+
"import sys\n"
|
|
389
|
+
f"sys.path.insert(0, {str(src_path)!r})\n"
|
|
390
|
+
"from ej_test_gen import TestRunner\n"
|
|
391
|
+
"runner = TestRunner(solution='sol.py', working_dir='.', tests_dir='tests')\n"
|
|
392
|
+
"runner.test('abc')\n",
|
|
393
|
+
encoding='utf-8',
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
result = subprocess.run(
|
|
397
|
+
[sys.executable, str(task_dir / 'gen.py')],
|
|
398
|
+
cwd=str(cwd_dir),
|
|
399
|
+
capture_output=True,
|
|
400
|
+
text=True,
|
|
401
|
+
check=False,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
assert result.returncode == 0, result.stderr
|
|
405
|
+
assert (cwd_dir / 'tests' / '01').read_text(encoding='utf-8') == 'abc\n'
|
|
406
|
+
assert (cwd_dir / 'tests' / '01.a').read_text(encoding='utf-8').strip() == 'cba'
|
|
407
|
+
assert not (task_dir / 'tests').exists()
|
|
408
|
+
|
|
409
|
+
def test_constructor_does_not_change_process_cwd(self, tmp_path):
|
|
410
|
+
"""TestRunner should use subprocess cwd without changing the caller's cwd."""
|
|
411
|
+
cwd_before = Path.cwd()
|
|
412
|
+
sol = write_solution(tmp_path, "print(input())")
|
|
413
|
+
|
|
414
|
+
TestRunner(solution=sol, working_dir=tmp_path)
|
|
415
|
+
|
|
416
|
+
assert Path.cwd() == cwd_before
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# ---------------------------------------------------------------------------
|
|
420
|
+
# on_error behaviour
|
|
421
|
+
# ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
class TestOnError:
|
|
424
|
+
def test_default_on_nonzero_exit(self, tmp_path):
|
|
425
|
+
"""on_error='default' raises RuntimeError on non-zero returncode."""
|
|
426
|
+
sol = write_solution(tmp_path, "raise ValueError('boom')")
|
|
427
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path))
|
|
428
|
+
|
|
429
|
+
with pytest.raises(RuntimeError, match='returncode'):
|
|
430
|
+
runner.test('anything')
|
|
431
|
+
|
|
432
|
+
def test_default_on_stderr(self, tmp_path):
|
|
433
|
+
"""on_error='default' raises RuntimeError when solution writes to stderr."""
|
|
434
|
+
code = "import sys; sys.stderr.write('oops\\n'); print('ok')"
|
|
435
|
+
sol = write_solution(tmp_path, code)
|
|
436
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path))
|
|
437
|
+
|
|
438
|
+
with pytest.raises(RuntimeError):
|
|
439
|
+
runner.test('anything')
|
|
440
|
+
|
|
441
|
+
def test_default_no_files_created(self, tmp_path):
|
|
442
|
+
"""When on_error='default', no test files are written before the exception."""
|
|
443
|
+
sol = write_solution(tmp_path, "raise RuntimeError('x')")
|
|
444
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path))
|
|
445
|
+
|
|
446
|
+
with pytest.raises(RuntimeError):
|
|
447
|
+
runner.test('x')
|
|
448
|
+
|
|
449
|
+
assert get_input_files(tmp_path) == []
|
|
450
|
+
|
|
451
|
+
def test_ignore_on_nonzero_exit(self, tmp_path):
|
|
452
|
+
"""on_error='ignore' silently skips tests where solution crashes."""
|
|
453
|
+
sol = write_solution(tmp_path, "raise ValueError('boom')")
|
|
454
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), on_error='ignore')
|
|
455
|
+
|
|
456
|
+
# Must not raise
|
|
457
|
+
runner.test('anything')
|
|
458
|
+
assert get_input_files(tmp_path) == []
|
|
459
|
+
|
|
460
|
+
def test_ignore_does_not_advance_counter(self, tmp_path):
|
|
461
|
+
"""Ignored tests don't consume a test number slot."""
|
|
462
|
+
code = """\
|
|
463
|
+
import sys
|
|
464
|
+
n = int(input())
|
|
465
|
+
if n < 0:
|
|
466
|
+
raise ValueError('negative')
|
|
467
|
+
print(n * 2)
|
|
468
|
+
"""
|
|
469
|
+
sol = write_solution(tmp_path, code)
|
|
470
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), on_error='ignore')
|
|
471
|
+
|
|
472
|
+
runner.test('3') # succeeds → file '01' (or some N)
|
|
473
|
+
runner.test('-1') # crashes → skipped, counter not advanced
|
|
474
|
+
runner.test('5') # succeeds → file 'N+1'
|
|
475
|
+
|
|
476
|
+
inputs = get_input_files(tmp_path)
|
|
477
|
+
assert len(inputs) == 2
|
|
478
|
+
answers = [get_answer_file(f).read_text().strip() for f in inputs]
|
|
479
|
+
assert answers == ['6', '10']
|
|
480
|
+
# The two files must be consecutive
|
|
481
|
+
nums = [int(f.name) for f in inputs]
|
|
482
|
+
assert nums[1] == nums[0] + 1
|
|
483
|
+
|
|
484
|
+
def test_ignore_on_stderr(self, tmp_path):
|
|
485
|
+
"""on_error='ignore' skips tests that produce stderr output."""
|
|
486
|
+
code = "import sys; sys.stderr.write('warn\\n'); print('result')"
|
|
487
|
+
sol = write_solution(tmp_path, code)
|
|
488
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), on_error='ignore')
|
|
489
|
+
|
|
490
|
+
runner.test('x')
|
|
491
|
+
assert get_input_files(tmp_path) == []
|
|
492
|
+
|
|
493
|
+
def test_output_uses_stderr_as_answer(self, tmp_path):
|
|
494
|
+
"""on_error='output' writes traceback/stderr as the answer file."""
|
|
495
|
+
sol = write_solution(tmp_path, "raise ValueError('boom')")
|
|
496
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), on_error='output')
|
|
497
|
+
|
|
498
|
+
runner.test('anything')
|
|
499
|
+
|
|
500
|
+
inputs = get_input_files(tmp_path)
|
|
501
|
+
assert len(inputs) == 1
|
|
502
|
+
assert inputs[0].read_text(encoding='utf-8') == 'anything\n'
|
|
503
|
+
answer = get_answer_file(inputs[0]).read_text(encoding='utf-8')
|
|
504
|
+
assert 'Traceback' in answer
|
|
505
|
+
assert 'ValueError: boom' in answer
|
|
506
|
+
|
|
507
|
+
def test_output_without_stderr_uses_returncode_message(self, tmp_path):
|
|
508
|
+
"""on_error='output' has deterministic output even without stderr."""
|
|
509
|
+
sol = write_solution(tmp_path, "import os; os._exit(7)")
|
|
510
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), on_error='output')
|
|
511
|
+
|
|
512
|
+
runner.test('anything')
|
|
513
|
+
|
|
514
|
+
inputs = get_input_files(tmp_path)
|
|
515
|
+
assert get_answer_file(inputs[0]).read_text(encoding='utf-8') == 'Solution exited with returncode=7\n'
|
|
516
|
+
|
|
517
|
+
def test_old_raise_alias_still_works(self, tmp_path):
|
|
518
|
+
"""on_error='raise' remains an alias for on_error='default'."""
|
|
519
|
+
sol = write_solution(tmp_path, "raise ValueError('boom')")
|
|
520
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), on_error='raise')
|
|
521
|
+
|
|
522
|
+
with pytest.raises(RuntimeError, match='returncode'):
|
|
523
|
+
runner.test('anything')
|
|
524
|
+
|
|
525
|
+
def test_old_skip_alias_still_works(self, tmp_path):
|
|
526
|
+
"""on_error='skip' remains an alias for on_error='ignore'."""
|
|
527
|
+
sol = write_solution(tmp_path, "raise ValueError('boom')")
|
|
528
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), on_error='skip')
|
|
529
|
+
|
|
530
|
+
runner.test('anything')
|
|
531
|
+
assert get_input_files(tmp_path) == []
|
|
532
|
+
|
|
533
|
+
def test_invalid_on_error(self, tmp_path):
|
|
534
|
+
"""Unknown on_error values fail fast in TestRunner construction."""
|
|
535
|
+
sol = write_solution(tmp_path, "print('ok')")
|
|
536
|
+
|
|
537
|
+
with pytest.raises(ValueError, match='on_error'):
|
|
538
|
+
TestRunner(solution=sol, working_dir=str(tmp_path), on_error='unknown')
|
|
539
|
+
|
|
540
|
+
def test_clean_run_not_affected(self, tmp_path):
|
|
541
|
+
"""on_error='default' must not interfere with completely clean solutions."""
|
|
542
|
+
sol = write_solution(tmp_path, "print(input()[::-1])")
|
|
543
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), on_error='default')
|
|
544
|
+
|
|
545
|
+
runner.test('abc')
|
|
546
|
+
inputs = get_input_files(tmp_path)
|
|
547
|
+
assert get_answer_file(inputs[0]).read_text().strip() == 'cba'
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
# ---------------------------------------------------------------------------
|
|
551
|
+
# use_WSL=True ignored on non-Windows
|
|
552
|
+
# ---------------------------------------------------------------------------
|
|
553
|
+
|
|
554
|
+
class TestWSLOnNonWindows:
|
|
555
|
+
def test_use_wsl_ignored(self, tmp_path):
|
|
556
|
+
"""use_WSL=True must not crash or break execution on non-Windows."""
|
|
557
|
+
if platform.system() == 'Windows':
|
|
558
|
+
pytest.skip('WSL is meaningful on Windows; skip non-Windows check')
|
|
559
|
+
|
|
560
|
+
sol = write_solution(tmp_path, "print(int(input()) + 1)")
|
|
561
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), use_WSL=True)
|
|
562
|
+
runner.test('41')
|
|
563
|
+
|
|
564
|
+
inputs = get_input_files(tmp_path)
|
|
565
|
+
assert get_answer_file(inputs[0]).read_text().strip() == '42'
|
|
566
|
+
|
|
567
|
+
def test_compile_sol_use_wsl_ignored(self, tmp_path):
|
|
568
|
+
"""compile_sol() must not crash with use_WSL=True on non-Windows."""
|
|
569
|
+
if platform.system() == 'Windows':
|
|
570
|
+
pytest.skip('WSL is meaningful on Windows; skip non-Windows check')
|
|
571
|
+
|
|
572
|
+
# A Python solution — compile_sol() returns early for .py, but the
|
|
573
|
+
# TestRunner constructor must succeed without errors.
|
|
574
|
+
sol = write_solution(tmp_path, "print('ok')")
|
|
575
|
+
runner = TestRunner(solution=sol, working_dir=str(tmp_path), use_WSL=True)
|
|
576
|
+
runner.test('x')
|
|
577
|
+
|
|
578
|
+
inputs = get_input_files(tmp_path)
|
|
579
|
+
assert get_answer_file(inputs[0]).read_text().strip() == 'ok'
|