140 lines
4.4 KiB
Python
140 lines
4.4 KiB
Python
import os
|
||
import sys
|
||
from multiprocessing import Process
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from pygame.time import Clock
|
||
import pytest
|
||
import pytest_timeout
|
||
|
||
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
|
||
sys.path.append(str(BASE_DIR))
|
||
|
||
# Hide the pygame screen
|
||
os.environ['SDL_VIDEODRIVER'] = 'dummy'
|
||
|
||
TIMEOUT_ASSERT_MSG = (
|
||
'Проект работает некорректно, проверка прервана.\n'
|
||
'Вероятные причины ошибки:\n'
|
||
'1. Исполняемый код (например, вызов функции `main()`) оказался в '
|
||
'глобальной зоне видимости. Как исправить: вызов функции `main` поместите '
|
||
'внутрь конструкции `if __name__ == "__main__":`.\n'
|
||
'2. В цикле `while True` внутри функции `main` отсутствует вызов метода '
|
||
'`tick` объекта `clock`. Не изменяйте прекод в этой части.'
|
||
)
|
||
|
||
|
||
def import_the_snake():
|
||
import the_snake # noqa
|
||
|
||
|
||
@pytest.fixture(scope='session')
|
||
def snake_import_test():
|
||
check_import_process = Process(target=import_the_snake)
|
||
check_import_process.start()
|
||
pid = check_import_process.pid
|
||
check_import_process.join(timeout=1)
|
||
if check_import_process.is_alive():
|
||
os.kill(pid, 9)
|
||
raise AssertionError(TIMEOUT_ASSERT_MSG)
|
||
|
||
|
||
@pytest.fixture(scope='session')
|
||
def _the_snake(snake_import_test):
|
||
try:
|
||
import the_snake
|
||
except ImportError as error:
|
||
raise AssertionError(
|
||
'При импорте модуль `the_snake` произошла ошибка:\n'
|
||
f'{type(error).__name__}: {error}'
|
||
)
|
||
for class_name in ('GameObject', 'Snake', 'Apple'):
|
||
assert hasattr(the_snake, class_name), (
|
||
f'Убедитесь, что в модуле `the_snake` определен класс `{class_name}`.'
|
||
)
|
||
return the_snake
|
||
|
||
|
||
def write_timeout_reasons(text, stream=None):
|
||
"""Write possible reasons of tests timeout to stream.
|
||
|
||
The function to replace pytest_timeout traceback output with possible
|
||
reasons of tests timeout.
|
||
Appears only when `thread` method is used.
|
||
"""
|
||
if stream is None:
|
||
stream = sys.stderr
|
||
text = TIMEOUT_ASSERT_MSG
|
||
stream.write(text)
|
||
|
||
|
||
pytest_timeout.write = write_timeout_reasons
|
||
|
||
|
||
def _create_game_object(class_name, module):
|
||
try:
|
||
return getattr(module, class_name)()
|
||
except TypeError as error:
|
||
raise AssertionError(
|
||
f'При создании объекта класса `{class_name}` произошла ошибка:\n'
|
||
f'`{type(error).__name__}: {error}`\n'
|
||
f'Если в конструктор класса `{class_name}` помимо параметра '
|
||
'`self` передаются какие-то ещё параметры - убедитесь, что для '
|
||
'них установлены значения по умолчанию. Например:\n'
|
||
'`def __init__(self, <параметр>=<значение_по_умолчанию>):`'
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def game_object(_the_snake):
|
||
return _create_game_object('GameObject', _the_snake)
|
||
|
||
|
||
@pytest.fixture
|
||
def snake(_the_snake):
|
||
return _create_game_object('Snake', _the_snake)
|
||
|
||
|
||
@pytest.fixture
|
||
def apple(_the_snake):
|
||
return _create_game_object('Apple', _the_snake)
|
||
|
||
|
||
class StopInfiniteLoop(Exception):
|
||
pass
|
||
|
||
|
||
def loop_breaker_decorator(func):
|
||
call_counter = 0
|
||
|
||
def wrapper(*args, **kwargs):
|
||
result = func(*args, **kwargs)
|
||
nonlocal call_counter
|
||
call_counter += 1
|
||
if call_counter > 1:
|
||
raise StopInfiniteLoop
|
||
return result
|
||
return wrapper
|
||
|
||
|
||
@pytest.fixture
|
||
def modified_clock(_the_snake):
|
||
class _Clock:
|
||
def __init__(self, clock_obj: Clock) -> None:
|
||
self.clock = clock_obj
|
||
|
||
@loop_breaker_decorator
|
||
def tick(self, *args, **kwargs):
|
||
return self.clock.tick(*args, **kwargs)
|
||
|
||
def __getattribute__(self, name: str) -> Any:
|
||
if name in ['tick', 'clock']:
|
||
return super().__getattribute__(name)
|
||
return self.clock.__getattribute__(name)
|
||
|
||
original_clock = _the_snake.clock
|
||
modified_clock_obj = _Clock(original_clock)
|
||
_the_snake.clock = modified_clock_obj
|
||
yield
|
||
_the_snake.clock = original_clock
|