tests: proper handling of event loops

Event loops have to be closed properly, otherwise pytest will throw a
warning / error. Disconnecting clients and closing the server is
important for that, as well as stopping tasks.
Additionally, I had to add loop_scope parameters to share the event loop
throughout modules which all use the same pydase.Server instance.
This commit is contained in:
Mose Müller
2025-05-20 13:54:37 +02:00
parent 57cfe45c76
commit c32b6a8694
7 changed files with 84 additions and 54 deletions

View File

@ -2,8 +2,9 @@ import threading
from collections.abc import Generator from collections.abc import Generator
from typing import Any from typing import Any
import pydase
import pytest import pytest
import pydase
from pydase.client.proxy_loader import ProxyAttributeError from pydase.client.proxy_loader import ProxyAttributeError
@ -52,6 +53,7 @@ def pydase_client() -> Generator[pydase.Client, None, Any]:
yield client yield client
client.disconnect()
server.handle_exit() server.handle_exit()
thread.join() thread.join()

View File

@ -2,27 +2,26 @@ import threading
from collections.abc import Callable, Generator from collections.abc import Callable, Generator
from typing import Any from typing import Any
import pydase
import pytest import pytest
import socketio.exceptions import socketio.exceptions
import pydase
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def pydase_restartable_server() -> ( def pydase_restartable_server() -> Generator[
Generator[ tuple[
tuple[ pydase.Server,
pydase.Server, threading.Thread,
threading.Thread, pydase.DataService,
pydase.DataService, Callable[
Callable[ [pydase.Server, threading.Thread, pydase.DataService],
[pydase.Server, threading.Thread, pydase.DataService], tuple[pydase.Server, threading.Thread],
tuple[pydase.Server, threading.Thread],
],
], ],
None, ],
Any, None,
] Any,
): ]:
class MyService(pydase.DataService): class MyService(pydase.DataService):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@ -62,9 +61,6 @@ def pydase_restartable_server() -> (
yield server, thread, service_instance, restart yield server, thread, service_instance, restart
server.handle_exit()
thread.join()
def test_reconnection( def test_reconnection(
pydase_restartable_server: tuple[ pydase_restartable_server: tuple[
@ -105,3 +101,6 @@ def test_reconnection(
# the service proxies successfully reconnect and get the new service name # the service proxies successfully reconnect and get the new service name
assert client.proxy.name == "New service name" assert client.proxy.name == "New service name"
assert client_2.proxy.name == "New service name" assert client_2.proxy.name == "New service name"
server.handle_exit()
thread.join()

View File

@ -7,7 +7,7 @@ from pydase.task.autostart import autostart_service_tasks
from pytest import LogCaptureFixture from pytest import LogCaptureFixture
@pytest.mark.asyncio(scope="function") @pytest.mark.asyncio(loop_scope="function")
async def test_reconnection(caplog: LogCaptureFixture) -> None: async def test_reconnection(caplog: LogCaptureFixture) -> None:
class MyService(pydase.components.device_connection.DeviceConnection): class MyService(pydase.components.device_connection.DeviceConnection):
def __init__( def __init__(

View File

@ -4,12 +4,13 @@ from collections.abc import Generator
from typing import Any from typing import Any
import aiohttp import aiohttp
import pydase
import pytest import pytest
import pydase
from pydase.utils.serialization.deserializer import Deserializer from pydase.utils.serialization.deserializer import Deserializer
@pytest.fixture() @pytest.fixture(scope="module")
def pydase_server() -> Generator[None, None, None]: def pydase_server() -> Generator[None, None, None]:
class SubService(pydase.DataService): class SubService(pydase.DataService):
name = "SubService" name = "SubService"
@ -52,6 +53,9 @@ def pydase_server() -> Generator[None, None, None]:
yield yield
server.handle_exit()
thread.join()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"access_path, expected", "access_path, expected",
@ -107,7 +111,7 @@ def pydase_server() -> Generator[None, None, None]:
), ),
], ],
) )
@pytest.mark.asyncio() @pytest.mark.asyncio(loop_scope="module")
async def test_get_value( async def test_get_value(
access_path: str, access_path: str,
expected: dict[str, Any], expected: dict[str, Any],
@ -179,7 +183,7 @@ async def test_get_value(
), ),
], ],
) )
@pytest.mark.asyncio() @pytest.mark.asyncio(loop_scope="module")
async def test_update_value( async def test_update_value(
access_path: str, access_path: str,
new_value: dict[str, Any], new_value: dict[str, Any],
@ -219,7 +223,7 @@ async def test_update_value(
), ),
], ],
) )
@pytest.mark.asyncio() @pytest.mark.asyncio(loop_scope="module")
async def test_trigger_method( async def test_trigger_method(
access_path: str, access_path: str,
expected: Any, expected: Any,
@ -278,7 +282,7 @@ async def test_trigger_method(
), ),
], ],
) )
@pytest.mark.asyncio() @pytest.mark.asyncio(loop_scope="module")
async def test_client_information_logging( async def test_client_information_logging(
headers: dict[str, str], headers: dict[str, str],
log_id: str, log_id: str,

View File

@ -2,13 +2,14 @@ import threading
from collections.abc import Generator from collections.abc import Generator
from typing import Any from typing import Any
import pydase
import pytest import pytest
import socketio import socketio
import pydase
from pydase.utils.serialization.deserializer import Deserializer from pydase.utils.serialization.deserializer import Deserializer
@pytest.fixture() @pytest.fixture(scope="module")
def pydase_server() -> Generator[None, None, None]: def pydase_server() -> Generator[None, None, None]:
class SubService(pydase.DataService): class SubService(pydase.DataService):
name = "SubService" name = "SubService"
@ -51,6 +52,9 @@ def pydase_server() -> Generator[None, None, None]:
yield yield
server.handle_exit()
thread.join()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"access_path, expected", "access_path, expected",
@ -106,7 +110,7 @@ def pydase_server() -> Generator[None, None, None]:
), ),
], ],
) )
@pytest.mark.asyncio() @pytest.mark.asyncio(loop_scope="module")
async def test_get_value( async def test_get_value(
access_path: str, access_path: str,
expected: dict[str, Any], expected: dict[str, Any],
@ -181,7 +185,7 @@ async def test_get_value(
), ),
], ],
) )
@pytest.mark.asyncio() @pytest.mark.asyncio(loop_scope="module")
async def test_update_value( async def test_update_value(
access_path: str, access_path: str,
new_value: dict[str, Any], new_value: dict[str, Any],
@ -226,7 +230,7 @@ async def test_update_value(
), ),
], ],
) )
@pytest.mark.asyncio() @pytest.mark.asyncio(loop_scope="module")
async def test_trigger_method( async def test_trigger_method(
access_path: str, access_path: str,
expected: Any, expected: Any,
@ -291,7 +295,7 @@ async def test_trigger_method(
), ),
], ],
) )
@pytest.mark.asyncio() @pytest.mark.asyncio(loop_scope="module")
async def test_client_information_logging( async def test_client_information_logging(
headers: dict[str, str], headers: dict[str, str],
log_id: str, log_id: str,

View File

@ -1,19 +1,20 @@
import asyncio import asyncio
import logging import logging
import pydase
import pytest import pytest
from pytest import LogCaptureFixture
import pydase
from pydase.data_service.data_service_observer import DataServiceObserver from pydase.data_service.data_service_observer import DataServiceObserver
from pydase.data_service.state_manager import StateManager from pydase.data_service.state_manager import StateManager
from pydase.task.autostart import autostart_service_tasks from pydase.task.autostart import autostart_service_tasks
from pydase.task.decorator import task from pydase.task.decorator import task
from pydase.task.task_status import TaskStatus from pydase.task.task_status import TaskStatus
from pytest import LogCaptureFixture
logger = logging.getLogger("pydase") logger = logging.getLogger("pydase")
@pytest.mark.asyncio(scope="function") @pytest.mark.asyncio()
async def test_start_and_stop_task(caplog: LogCaptureFixture) -> None: async def test_start_and_stop_task(caplog: LogCaptureFixture) -> None:
class MyService(pydase.DataService): class MyService(pydase.DataService):
@task() @task()
@ -28,11 +29,11 @@ async def test_start_and_stop_task(caplog: LogCaptureFixture) -> None:
DataServiceObserver(state_manager) DataServiceObserver(state_manager)
autostart_service_tasks(service_instance) autostart_service_tasks(service_instance)
await asyncio.sleep(0.1) await asyncio.sleep(0.01)
assert service_instance.my_task.status == TaskStatus.NOT_RUNNING assert service_instance.my_task.status == TaskStatus.NOT_RUNNING
service_instance.my_task.start() service_instance.my_task.start()
await asyncio.sleep(0.1) await asyncio.sleep(0.01)
assert service_instance.my_task.status == TaskStatus.RUNNING assert service_instance.my_task.status == TaskStatus.RUNNING
assert "'my_task.status' changed to 'TaskStatus.RUNNING'" in caplog.text assert "'my_task.status' changed to 'TaskStatus.RUNNING'" in caplog.text
@ -40,12 +41,12 @@ async def test_start_and_stop_task(caplog: LogCaptureFixture) -> None:
caplog.clear() caplog.clear()
service_instance.my_task.stop() service_instance.my_task.stop()
await asyncio.sleep(0.1) await asyncio.sleep(0.01)
assert service_instance.my_task.status == TaskStatus.NOT_RUNNING assert service_instance.my_task.status == TaskStatus.NOT_RUNNING
assert "Task 'my_task' was cancelled" in caplog.text assert "Task 'my_task' was cancelled" in caplog.text
@pytest.mark.asyncio(scope="function") @pytest.mark.asyncio()
async def test_autostart_task(caplog: LogCaptureFixture) -> None: async def test_autostart_task(caplog: LogCaptureFixture) -> None:
class MyService(pydase.DataService): class MyService(pydase.DataService):
@task(autostart=True) @task(autostart=True)
@ -61,13 +62,16 @@ async def test_autostart_task(caplog: LogCaptureFixture) -> None:
autostart_service_tasks(service_instance) autostart_service_tasks(service_instance)
await asyncio.sleep(0.1) await asyncio.sleep(0.01)
assert service_instance.my_task.status == TaskStatus.RUNNING assert service_instance.my_task.status == TaskStatus.RUNNING
assert "'my_task.status' changed to 'TaskStatus.RUNNING'" in caplog.text assert "'my_task.status' changed to 'TaskStatus.RUNNING'" in caplog.text
service_instance.my_task.stop()
await asyncio.sleep(0.01)
@pytest.mark.asyncio(scope="function")
@pytest.mark.asyncio()
async def test_nested_list_autostart_task( async def test_nested_list_autostart_task(
caplog: LogCaptureFixture, caplog: LogCaptureFixture,
) -> None: ) -> None:
@ -86,7 +90,7 @@ async def test_nested_list_autostart_task(
DataServiceObserver(state_manager) DataServiceObserver(state_manager)
autostart_service_tasks(service_instance) autostart_service_tasks(service_instance)
await asyncio.sleep(0.1) await asyncio.sleep(0.01)
assert service_instance.sub_services_list[0].my_task.status == TaskStatus.RUNNING assert service_instance.sub_services_list[0].my_task.status == TaskStatus.RUNNING
assert service_instance.sub_services_list[1].my_task.status == TaskStatus.RUNNING assert service_instance.sub_services_list[1].my_task.status == TaskStatus.RUNNING
@ -99,8 +103,12 @@ async def test_nested_list_autostart_task(
in caplog.text in caplog.text
) )
service_instance.sub_services_list[0].my_task.stop()
service_instance.sub_services_list[1].my_task.stop()
await asyncio.sleep(0.01)
@pytest.mark.asyncio(scope="function")
@pytest.mark.asyncio()
async def test_nested_dict_autostart_task( async def test_nested_dict_autostart_task(
caplog: LogCaptureFixture, caplog: LogCaptureFixture,
) -> None: ) -> None:
@ -120,7 +128,7 @@ async def test_nested_dict_autostart_task(
autostart_service_tasks(service_instance) autostart_service_tasks(service_instance)
await asyncio.sleep(0.1) await asyncio.sleep(0.01)
assert ( assert (
service_instance.sub_services_dict["first"].my_task.status == TaskStatus.RUNNING service_instance.sub_services_dict["first"].my_task.status == TaskStatus.RUNNING
@ -139,8 +147,12 @@ async def test_nested_dict_autostart_task(
in caplog.text in caplog.text
) )
service_instance.sub_services_dict["first"].my_task.stop()
service_instance.sub_services_dict["second"].my_task.stop()
await asyncio.sleep(0.01)
@pytest.mark.asyncio(scope="function")
@pytest.mark.asyncio()
async def test_manual_start_with_multiple_service_instances( async def test_manual_start_with_multiple_service_instances(
caplog: LogCaptureFixture, caplog: LogCaptureFixture,
) -> None: ) -> None:
@ -161,7 +173,7 @@ async def test_manual_start_with_multiple_service_instances(
autostart_service_tasks(service_instance) autostart_service_tasks(service_instance)
await asyncio.sleep(0.1) await asyncio.sleep(0.01)
assert ( assert (
service_instance.sub_services_list[0].my_task.status == TaskStatus.NOT_RUNNING service_instance.sub_services_list[0].my_task.status == TaskStatus.NOT_RUNNING
@ -291,7 +303,7 @@ async def test_manual_start_with_multiple_service_instances(
assert "Task 'my_task' was cancelled" in caplog.text assert "Task 'my_task' was cancelled" in caplog.text
@pytest.mark.asyncio(scope="function") @pytest.mark.asyncio()
async def test_restart_on_exception(caplog: LogCaptureFixture) -> None: async def test_restart_on_exception(caplog: LogCaptureFixture) -> None:
class MyService(pydase.DataService): class MyService(pydase.DataService):
@task(restart_on_exception=True, restart_sec=0.1) @task(restart_on_exception=True, restart_sec=0.1)
@ -312,8 +324,11 @@ async def test_restart_on_exception(caplog: LogCaptureFixture) -> None:
assert "Task 'my_task' encountered an exception" in caplog.text assert "Task 'my_task' encountered an exception" in caplog.text
assert "Triggered task." in caplog.text assert "Triggered task." in caplog.text
service_instance.my_task.stop()
await asyncio.sleep(0.01)
@pytest.mark.asyncio(scope="function")
@pytest.mark.asyncio()
async def test_restart_sec(caplog: LogCaptureFixture) -> None: async def test_restart_sec(caplog: LogCaptureFixture) -> None:
class MyService(pydase.DataService): class MyService(pydase.DataService):
@task(restart_on_exception=True, restart_sec=0.1) @task(restart_on_exception=True, restart_sec=0.1)
@ -334,8 +349,11 @@ async def test_restart_sec(caplog: LogCaptureFixture) -> None:
await asyncio.sleep(0.05) await asyncio.sleep(0.05)
assert "Triggered task." in caplog.text # Ensures the task restarted after 0.2s assert "Triggered task." in caplog.text # Ensures the task restarted after 0.2s
service_instance.my_task.stop()
await asyncio.sleep(0.01)
@pytest.mark.asyncio(scope="function")
@pytest.mark.asyncio()
async def test_exceeding_start_limit_interval_sec_and_burst( async def test_exceeding_start_limit_interval_sec_and_burst(
caplog: LogCaptureFixture, caplog: LogCaptureFixture,
) -> None: ) -> None:
@ -359,7 +377,7 @@ async def test_exceeding_start_limit_interval_sec_and_burst(
assert service_instance.my_task.status == TaskStatus.NOT_RUNNING assert service_instance.my_task.status == TaskStatus.NOT_RUNNING
@pytest.mark.asyncio(scope="function") @pytest.mark.asyncio()
async def test_non_exceeding_start_limit_interval_sec_and_burst( async def test_non_exceeding_start_limit_interval_sec_and_burst(
caplog: LogCaptureFixture, caplog: LogCaptureFixture,
) -> None: ) -> None:
@ -382,8 +400,11 @@ async def test_non_exceeding_start_limit_interval_sec_and_burst(
assert "Task 'my_task' exceeded restart burst limit" not in caplog.text assert "Task 'my_task' exceeded restart burst limit" not in caplog.text
assert service_instance.my_task.status == TaskStatus.RUNNING assert service_instance.my_task.status == TaskStatus.RUNNING
service_instance.my_task.stop()
await asyncio.sleep(0.01)
@pytest.mark.asyncio(scope="function")
@pytest.mark.asyncio()
async def test_exit_on_failure( async def test_exit_on_failure(
monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture
) -> None: ) -> None:
@ -408,7 +429,7 @@ async def test_exit_on_failure(
assert "Task 'my_task' encountered an exception" in caplog.text assert "Task 'my_task' encountered an exception" in caplog.text
@pytest.mark.asyncio(scope="function") @pytest.mark.asyncio()
async def test_exit_on_failure_exceeding_rate_limit( async def test_exit_on_failure_exceeding_rate_limit(
monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture
) -> None: ) -> None:
@ -438,7 +459,7 @@ async def test_exit_on_failure_exceeding_rate_limit(
assert "Task 'my_task' encountered an exception" in caplog.text assert "Task 'my_task' encountered an exception" in caplog.text
@pytest.mark.asyncio(scope="function") @pytest.mark.asyncio()
async def test_gracefully_finishing_task( async def test_gracefully_finishing_task(
monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture
) -> None: ) -> None:

View File

@ -207,7 +207,7 @@ def test_ColouredEnum_serialize() -> None:
} }
@pytest.mark.asyncio(scope="module") @pytest.mark.asyncio(loop_scope="module")
async def test_method_serialization() -> None: async def test_method_serialization() -> None:
class ClassWithMethod(pydase.DataService): class ClassWithMethod(pydase.DataService):
def some_method(self) -> str: def some_method(self) -> str: