Skip to content

Commit 6bff07c

Browse files
committed
Initial commit
0 parents  commit 6bff07c

14 files changed

Lines changed: 546 additions & 0 deletions

.gitignore

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# JetBrains IDE specific files:
2+
.idea/
3+
*.iml
4+
*.ipr
5+
*.iws
6+
7+
# Python specific:
8+
__pycache__/
9+
*.py[cod]
10+
*$py.class
11+
12+
# Virtual environment:
13+
venv/
14+
env/
15+
.venv/
16+
17+
# Environments:
18+
.env
19+
.venv
20+
21+
# VS Code:
22+
.vscode/
23+
24+
# Database files:
25+
*.db
26+
*.sqlite
27+
28+
# Log files:
29+
*.log

main.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from settings import TEST_TYPE
2+
from testing.general import testing
3+
from testing.test_type import TestType
4+
5+
6+
def main():
7+
"""
8+
Тесты должны быть разделены хотя бы одной пустой строкой.
9+
Первые N строк теста – входные параметры тестируемой функции.
10+
(N + 1)-я строка содержит ожидаемое возвращаемое значение
11+
тестируемой функции (необязательно).
12+
"""
13+
with open('tests.txt', 'r', encoding='utf-8') as f:
14+
testing(getattr(TestType, TEST_TYPE.upper()), f.read())
15+
16+
17+
if __name__ == '__main__':
18+
main()

settings.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Тип тестирования: классический или через команды.
2+
# Значения: 'classic', 'commands'.
3+
TEST_TYPE = 'classic'
4+
5+
# Показать время выполнения каждого теста.
6+
SHOW_EXECUTION_TIME = False
7+
8+
# Выводить результат выполнения каждой команды в командном типе тестирования.
9+
DEBUG_COMMANDS = False
10+
11+
# Указание ожидаемого результата теста обязательно.
12+
REQUIRED_EXPECTED_RESULT = False

solution.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import List
2+
3+
4+
class Solution:
5+
def twoSum(self, nums: List[int], target: int) -> List[int]:
6+
index_map = {}
7+
8+
for i, num in enumerate(nums):
9+
complement = target - num
10+
if complement in index_map:
11+
return [index_map[complement], i]
12+
13+
index_map[num] = i

testing/abstract_test.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from abc import abstractmethod
2+
from typing import Sequence
3+
4+
from testing.result import Result
5+
6+
7+
class AbstractTest:
8+
"""
9+
Абстрактный класс типа тестирования.
10+
"""
11+
12+
def __init__(self, *args, **kwargs):
13+
pass
14+
15+
@classmethod
16+
@abstractmethod
17+
def parse(cls, lines: Sequence) -> tuple['AbstractTest', list]:
18+
"""
19+
:param lines: Строки данных одного теста.
20+
Каждый элемент перечисляется через запятую.
21+
"""
22+
pass
23+
24+
@abstractmethod
25+
def run(self) -> Result:
26+
"""
27+
Выполнить тест.
28+
29+
:return: Объект результата теста.
30+
"""
31+
pass

testing/classic_test.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import copy
2+
import time
3+
from typing import Sequence
4+
5+
from settings import REQUIRED_EXPECTED_RESULT
6+
from testing.abstract_test import AbstractTest
7+
from testing.result import Result
8+
from testing.general import proc_args_by_func, Solution, proc_result, solution_method_names, get_params_signature, \
9+
TestDataError
10+
11+
12+
class ClassicTest(AbstractTest):
13+
"""
14+
Классический класс тестирования.
15+
"""
16+
17+
def __init__(self, *args, **kwargs):
18+
super().__init__(*args, **kwargs)
19+
self.__args = args
20+
self.__kwargs = kwargs
21+
22+
@classmethod
23+
def parse(cls, lines: Sequence) -> tuple[AbstractTest, list]:
24+
"""
25+
1..N строки: Аргументы (в каждой строке свой аргумент).
26+
N+1 строка: Ожидаемое значение (необязательно).
27+
N – количество параметров тестируемой функции.
28+
"""
29+
func = getattr(Solution(), solution_method_names[0])
30+
count_params = len(get_params_signature(func))
31+
if len(lines) < count_params:
32+
raise TestDataError('Incorrect number of arguments')
33+
34+
args = lines[:count_params]
35+
expected = lines[count_params] if len(lines) > count_params else None
36+
37+
if REQUIRED_EXPECTED_RESULT and not expected:
38+
raise TestDataError('Expected result not specified')
39+
40+
args = proc_args_by_func(args, func)
41+
42+
return cls(*args), expected
43+
44+
def run(self):
45+
args = copy.deepcopy(self.__args)
46+
kwargs = copy.deepcopy(self.__kwargs)
47+
48+
start_time = time.time()
49+
solution = Solution()
50+
func = getattr(solution, solution_method_names[0])
51+
result = func(*args, **kwargs)
52+
53+
return Result(
54+
proc_result(result),
55+
time.time() - start_time,
56+
*self.__args,
57+
**self.__kwargs
58+
)

testing/command_test.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import copy
2+
import sys
3+
import time
4+
from typing import Sequence
5+
6+
from settings import DEBUG_COMMANDS, REQUIRED_EXPECTED_RESULT
7+
from testing.abstract_test import AbstractTest
8+
from testing.result import Result
9+
from testing.general import TestDataError, proc_args_by_func
10+
from utils.style import Style
11+
12+
13+
class CommandTest(AbstractTest):
14+
"""
15+
Класс тестирования для команд
16+
(когда нужно создать объект тестируемого класса и вызывать его методы).
17+
"""
18+
19+
def __init__(self, commands, args_list, *args, **kwargs):
20+
super().__init__(*args, **kwargs)
21+
self.__commands = commands
22+
self.__args_list = args_list
23+
24+
@classmethod
25+
def parse(cls, lines: Sequence) -> tuple[AbstractTest, list]:
26+
"""
27+
1 строка: Список строковых команд
28+
(первая – название класса из файла solution.py, остальные – названия методов).
29+
2 строка: Списки аргументов каждой команды.
30+
3 строка: Список ожидаемых значений для каждой команды (необязательно).
31+
"""
32+
if len(lines) not in (2, 3):
33+
raise TestDataError('Incorrect data format')
34+
35+
commands = lines[0]
36+
args_list = lines[1]
37+
expectations = lines[2] if len(lines) >= 3 else None
38+
39+
if REQUIRED_EXPECTED_RESULT and not expectations:
40+
raise TestDataError('Expected result not specified')
41+
42+
return cls(commands, args_list), expectations
43+
44+
def run(self):
45+
if (not isinstance(self.__commands, list) or
46+
tuple(x for x in self.__commands if not isinstance(x, str))):
47+
raise TestDataError('Incorrect command format: commands must be a list of strings')
48+
49+
if not isinstance(self.__args_list, list):
50+
raise TestDataError('Incorrect command argument format: arguments must be in a list')
51+
52+
try:
53+
cls = getattr(sys.modules['solution'], self.__commands[0])
54+
except AttributeError:
55+
raise NameError(self.__commands[0])
56+
57+
commands = copy.deepcopy(self.__commands)
58+
args_list = copy.deepcopy(self.__args_list)
59+
60+
start_time = time.time()
61+
obj = cls(*self.__args_list[0])
62+
results = [None]
63+
64+
for command, args in zip(commands[1:], args_list[1:]):
65+
func = getattr(obj, command)
66+
args = proc_args_by_func(args, func)
67+
result = func(*args)
68+
69+
if DEBUG_COMMANDS:
70+
print(Style.BLUE + f'{command}({", ".join(map(str, args))}): {result}')
71+
72+
results.append(result)
73+
74+
return Result(
75+
results,
76+
time.time() - start_time,
77+
self.__commands,
78+
self.__args_list
79+
)

testing/general.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import importlib
2+
import json
3+
from inspect import signature
4+
from types import FunctionType
5+
from typing import Type, Iterable
6+
7+
from testing.abstract_test import AbstractTest
8+
from testing.result import Result
9+
from utils.list_node import ListNode, list_to_linked_list, linked_list_to_list
10+
from utils.style import Style
11+
from utils.tree_node import TreeNode, list_to_binary_tree, binary_tree_to_list
12+
13+
14+
SOLUTION_MODULE_NAME = 'solution'
15+
solution_module = importlib.import_module(SOLUTION_MODULE_NAME)
16+
17+
Solution = tuple(v for v in vars(solution_module).values() if type(v) is type and v.__module__ == SOLUTION_MODULE_NAME)
18+
if not Solution:
19+
raise NotImplementedError(f'Module {SOLUTION_MODULE_NAME} has no class')
20+
21+
Solution = Solution[0]
22+
solution_method_names = tuple(
23+
v.__name__ for v in Solution.__dict__.values()
24+
if type(v) == FunctionType and v.__name__[0] != '_'
25+
)
26+
if not solution_method_names:
27+
raise NotImplementedError(f'Class {Solution.__name__} from module {SOLUTION_MODULE_NAME} has no methods')
28+
29+
30+
class TestDataError(Exception):
31+
def __init__(self, msg: str):
32+
self.msg = msg
33+
super().__init__()
34+
35+
def __str__(self):
36+
return self.msg
37+
38+
39+
def get_params_signature(func):
40+
return tuple(signature(func).parameters.items())
41+
42+
43+
def proc_args_by_func(args: Iterable, func) -> list:
44+
proc_args = []
45+
signatures = get_params_signature(func)
46+
47+
for arg, (_, param_type) in zip(args, signatures):
48+
if ListNode.__name__ in str(param_type):
49+
if isinstance(arg, list):
50+
if len(arg) and isinstance(arg[0], list):
51+
arg = [list_to_linked_list(el) or [] for el in arg]
52+
else:
53+
arg = list_to_linked_list(arg) or []
54+
55+
if TreeNode.__name__ in str(param_type):
56+
if isinstance(arg, list):
57+
if len(arg) and isinstance(arg[0], list):
58+
arg = [list_to_binary_tree(el) or [] for el in arg]
59+
else:
60+
arg = list_to_binary_tree(arg) or []
61+
62+
proc_args.append(arg)
63+
64+
return proc_args
65+
66+
67+
def proc_result(result):
68+
if isinstance(result, ListNode):
69+
return linked_list_to_list(result) or []
70+
71+
if isinstance(result, TreeNode):
72+
return binary_tree_to_list(result) or []
73+
74+
return result
75+
76+
77+
def print_test_results():
78+
style = Style.BOLD + Style.UNDERLINE
79+
if Result.count_passed() == Result.count_runs():
80+
style += Style.GREEN
81+
else:
82+
style += Style.YELLOW
83+
84+
print(f'{style}Tests passed: {Result.count_passed()}/{Result.count_runs()}')
85+
86+
87+
def testing(
88+
cls: Type[AbstractTest],
89+
tests_data: str
90+
):
91+
"""
92+
Тестирование класса, находящегося в solution.py.
93+
94+
:param cls: Тестирующий класс.
95+
:param tests_data: Текст с тестовыми данными.
96+
Его парсинг определяется тестирующим классом.
97+
"""
98+
lines = [line.strip() for line in tests_data.strip().splitlines()]
99+
lines = [lines[i] for i in range(len(lines))
100+
if i == 0 or not(lines[i] == '' and lines[i-1] == '')]
101+
102+
tests_data = '\n'.join(lines).split('\n\n')
103+
for data in tests_data:
104+
lines = [json.loads(line) for line in data.splitlines()]
105+
obj, expected = cls.parse(lines)
106+
obj.run().validate(expected)
107+
108+
print_test_results()
109+
110+
111+
def generate_and_testing(
112+
cls: Type[AbstractTest],
113+
generate_args_func,
114+
validation_func,
115+
count: int
116+
):
117+
"""
118+
Генерация тестовых данных и тестирование ими класса,
119+
находящегося в solution.py.
120+
121+
:param cls: Тестирующий класс.
122+
:param generate_args_func: Функция генерации аргументов.
123+
Ничего не принимает, возвращает итерируемый объект с аргументами.
124+
:param validation_func: Функция валидации возвращенного результата.
125+
Принимает результат работы функции, возвращает логическое значение.
126+
:param count: Количество тестов.
127+
"""
128+
for _ in range(count):
129+
args = generate_args_func()
130+
cls(*args).run().validate(validation_func)
131+
132+
print_test_results()

0 commit comments

Comments
 (0)