diff --git a/pyproject.toml b/pyproject.toml index 59ed9e85..fa5ecc3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,10 +168,6 @@ markers = [ "functional: marks tests as functional tests", "unit: marks tests as unit tests", "serializer: marks serializers tests", - # Platform-specific tests - "platform_win32: marks tests as runnable on win32 (Windows)", - "platform_darwin: marks tests as runnable on darwin (macOS)", - "platform_linux: marks tests as runnable on linux", ] [tool.coverage.run] @@ -181,6 +177,9 @@ source_pkgs = [ "easynetwork_asyncio", ] relative_files = true +disable_warnings = [ + "module-not-measured", # Happening when using pytest-xdist. +] [tool.coverage.paths] source = [ diff --git a/tests/conftest.py b/tests/conftest.py index 5611bb48..659bd21a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,7 @@ def pytest_report_header() -> list[str]: pytest_plugins = [ f"{PYTEST_PLUGINS_PACKAGE}.asyncio_event_loop", + f"{PYTEST_PLUGINS_PACKAGE}.auto_markers", f"{PYTEST_PLUGINS_PACKAGE}.extra_features", - f"{PYTEST_PLUGINS_PACKAGE}.session_exit_code", f"{PYTEST_PLUGINS_PACKAGE}.ssl_module", ] diff --git a/tests/functional_test/conftest.py b/tests/functional_test/conftest.py deleted file mode 100644 index 3b657258..00000000 --- a/tests/functional_test/conftest.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import annotations - -import pathlib - -import pytest - - -def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: - directory = pathlib.PurePath(__file__).parent - for item in items: - if pathlib.PurePath(item.fspath).is_relative_to(directory): - item.add_marker(pytest.mark.functional) diff --git a/tests/functional_test/test_communication/test_async/test_client/test_udp.py b/tests/functional_test/test_communication/test_async/test_client/test_udp.py index 901bff73..9da8b09c 100644 --- a/tests/functional_test/test_communication/test_async/test_client/test_udp.py +++ b/tests/functional_test/test_communication/test_async/test_client/test_udp.py @@ -15,6 +15,7 @@ import pytest import pytest_asyncio +from .....tools import PlatformMarkers from .._utils import delay from ..conftest import use_asyncio_transport_xfail_uvloop @@ -113,7 +114,9 @@ async def test____send_packet____default(self, client: AsyncUDPNetworkClient[str async with asyncio.timeout(3): assert await server.recvfrom() == (b"ABCDEF", client.get_local_address()) - @pytest.mark.platform_linux # Windows and MacOS do not raise error + # Windows and MacOS do not raise error + @PlatformMarkers.skipif_platform_win32 + @PlatformMarkers.skipif_platform_macOS async def test____send_packet____connection_refused( self, client: AsyncUDPNetworkClient[str, str], @@ -123,7 +126,9 @@ async def test____send_packet____connection_refused( with pytest.raises(ConnectionRefusedError): await client.send_packet("ABCDEF") - @pytest.mark.platform_linux # Windows and MacOS do not raise error + # Windows and MacOS do not raise error + @PlatformMarkers.skipif_platform_win32 + @PlatformMarkers.skipif_platform_macOS async def test____send_packet____connection_refused____after_previous_successful_try( self, client: AsyncUDPNetworkClient[str, str], @@ -563,7 +568,9 @@ async def test____send_packet_to____invalid_address( with pytest.raises(ValueError): await client.send_packet_to("ABCDEF", other_client_address) - @pytest.mark.platform_linux # Windows and MacOS do not raise error + # Windows and MacOS do not raise error + @PlatformMarkers.skipif_platform_win32 + @PlatformMarkers.skipif_platform_macOS @pytest.mark.parametrize("client", ["WITH_REMOTE"], indirect=True) async def test____send_packet_to____connection_refused( self, @@ -575,7 +582,9 @@ async def test____send_packet_to____connection_refused( with pytest.raises(ConnectionRefusedError): await client.send_packet_to("ABCDEF", None) - @pytest.mark.platform_linux # Windows and MacOS do not raise error + # Windows and MacOS do not raise error + @PlatformMarkers.skipif_platform_win32 + @PlatformMarkers.skipif_platform_macOS @pytest.mark.parametrize("client", ["WITH_REMOTE"], indirect=True) async def test____send_packet_to____connection_refused____after_previous_successful_try( self, diff --git a/tests/functional_test/test_communication/test_sync/test_client/test_udp.py b/tests/functional_test/test_communication/test_sync/test_client/test_udp.py index 121cb801..3625e49b 100644 --- a/tests/functional_test/test_communication/test_sync/test_client/test_udp.py +++ b/tests/functional_test/test_communication/test_sync/test_client/test_udp.py @@ -11,6 +11,8 @@ import pytest +from .....tools import PlatformMarkers + @pytest.fixture def udp_socket_factory(request: pytest.FixtureRequest, localhost_ip: str) -> Callable[[], Socket]: @@ -52,13 +54,17 @@ def test____send_packet____default(self, client: UDPNetworkClient[str, str], ser client.send_packet("ABCDEF") assert server.recvfrom(1024) == (b"ABCDEF", client.get_local_address()) - @pytest.mark.platform_linux # Windows and MacOS do not raise error + # Windows and MacOS do not raise error + @PlatformMarkers.skipif_platform_win32 + @PlatformMarkers.skipif_platform_macOS def test____send_packet____connection_refused(self, client: UDPNetworkClient[str, str], server: Socket) -> None: server.close() with pytest.raises(ConnectionRefusedError): client.send_packet("ABCDEF") - @pytest.mark.platform_linux # Windows and MacOS do not raise error + # Windows and MacOS do not raise error + @PlatformMarkers.skipif_platform_win32 + @PlatformMarkers.skipif_platform_macOS def test____send_packet____connection_refused____after_previous_successful_try( self, client: UDPNetworkClient[str, str], @@ -236,7 +242,9 @@ def test____send_packet_to____invalid_address( with pytest.raises(ValueError): client.send_packet_to("ABCDEF", other_client_address) - @pytest.mark.platform_linux # Windows and MacOS do not raise error + # Windows and MacOS do not raise error + @PlatformMarkers.skipif_platform_win32 + @PlatformMarkers.skipif_platform_macOS @pytest.mark.parametrize("client", ["WITH_REMOTE"], indirect=True) def test____send_packet_to____connection_refused(self, client: UDPNetworkEndpoint[str, str], server: Socket) -> None: address = server.getsockname() @@ -244,7 +252,9 @@ def test____send_packet_to____connection_refused(self, client: UDPNetworkEndpoin with pytest.raises(ConnectionRefusedError): client.send_packet_to("ABCDEF", address) - @pytest.mark.platform_linux # Windows and MacOS do not raise error + # Windows and MacOS do not raise error + @PlatformMarkers.skipif_platform_win32 + @PlatformMarkers.skipif_platform_macOS @pytest.mark.parametrize("client", ["WITH_REMOTE"], indirect=True) def test____send_packet____connection_refused____after_previous_successful_try( self, diff --git a/tests/functional_test/test_serializers/conftest.py b/tests/functional_test/test_serializers/conftest.py deleted file mode 100644 index 4b8fcf3d..00000000 --- a/tests/functional_test/test_serializers/conftest.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -import pytest - - -def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: - from .base import BaseTestSerializer - - for item in items: - class_node = item.getparent(pytest.Class) - if class_node is None: - continue - if issubclass(class_node.cls, BaseTestSerializer): - item.add_marker(pytest.mark.serializer) diff --git a/tests/pytest_plugins/auto_markers.py b/tests/pytest_plugins/auto_markers.py new file mode 100644 index 00000000..4aafcb89 --- /dev/null +++ b/tests/pytest_plugins/auto_markers.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import pathlib + +import pytest + + +def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: + root_tests_directory = pathlib.PurePath(__file__).parent.parent + + unit_tests_directory = root_tests_directory / "unit_test" + functional_tests_directory = root_tests_directory / "functional_test" + serializer_tests_directories = { + unit_tests_directory / "test_serializers", + functional_tests_directory / "test_serializers", + } + + for item in items: + fspath = pathlib.PurePath(item.fspath) + + if any(fspath.is_relative_to(p) for p in serializer_tests_directories): + item.add_marker(pytest.mark.serializer, append=False) + + if fspath.is_relative_to(unit_tests_directory): + item.add_marker(pytest.mark.unit, append=False) + elif fspath.is_relative_to(functional_tests_directory): + item.add_marker(pytest.mark.functional, append=False) diff --git a/tests/pytest_plugins/extra_features.py b/tests/pytest_plugins/extra_features.py index 4a4390da..1538d69b 100644 --- a/tests/pytest_plugins/extra_features.py +++ b/tests/pytest_plugins/extra_features.py @@ -1,7 +1,6 @@ from __future__ import annotations import functools -import sys import pytest @@ -10,9 +9,7 @@ def _get_package_extra_features() -> frozenset[str]: from importlib.metadata import metadata - extra_to_exclude = {"uvloop"} - - return frozenset(metadata("easynetwork").get_all("Provides-Extra", ())).difference(extra_to_exclude) + return frozenset(metadata("easynetwork").get_all("Provides-Extra", ())) def pytest_configure(config: pytest.Config) -> None: @@ -21,30 +18,12 @@ def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line("markers", f"feature_{name}: mark test dealing with {name!r} extra") -def _get_markers_starting_with_prefix(item: pytest.Item, prefix: str, *, remove: bool) -> set[str]: - markers = {mark.name for mark in item.iter_markers() if mark.name.startswith(prefix)} - if remove: - markers = {m.removeprefix(prefix) for m in markers} - return markers - - -def _skip_if_platform_is_not_supported(item: pytest.Item) -> None: - actual_platform = sys.platform - - # Skip with specific declared platforms - supported_platforms = _get_markers_starting_with_prefix(item, "platform_", remove=True) - if supported_platforms and actual_platform not in supported_platforms: - item.add_marker(pytest.mark.skip(f"cannot run on platform {actual_platform}")) - - def _auto_add_feature_marker(item: pytest.Item) -> None: - required_features = _get_markers_starting_with_prefix(item, "feature_", remove=False) - - if required_features and item.get_closest_marker("feature") is None: + feature_markers = {mark.name for mark in item.iter_markers() if mark.name.startswith("feature_") or mark.name == "feature"} + if feature_markers and "feature" not in feature_markers: item.add_marker(pytest.mark.feature) def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: for item in items: - _skip_if_platform_is_not_supported(item) _auto_add_feature_marker(item) diff --git a/tests/pytest_plugins/session_exit_code.py b/tests/pytest_plugins/session_exit_code.py deleted file mode 100644 index fdd19ecc..00000000 --- a/tests/pytest_plugins/session_exit_code.py +++ /dev/null @@ -1,19 +0,0 @@ -from __future__ import annotations - -import pytest - - -def pytest_addoption(parser: pytest.Parser) -> None: - parser.addoption( - "--suppress-no-test-exit-code", - action="store_true", - default=False, - help='Suppress the "no tests collected" exit code.', - ) - - -@pytest.hookimpl(trylast=True) # type: ignore[misc] -def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: - if session.config.getoption("--suppress-no-test-exit-code"): - if exitstatus == pytest.ExitCode.NO_TESTS_COLLECTED: - session.exitstatus = pytest.ExitCode.OK diff --git a/tests/tools.py b/tests/tools.py index 4909f986..513501d4 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys import time from collections.abc import Generator from typing import Any, TypeVar, final @@ -10,6 +11,17 @@ _V_co = TypeVar("_V_co", covariant=True) +def _make_skipif_platform(platform: str) -> pytest.MarkDecorator: + return pytest.mark.skipif(sys.platform.startswith(platform), reason=f"cannot run on platform {platform!r}") + + +@final +class PlatformMarkers: + skipif_platform_win32 = _make_skipif_platform("win32") + skipif_platform_macOS = _make_skipif_platform("darwin") + skipif_platform_linux = _make_skipif_platform("linux") + + def send_return(gen: Generator[Any, _T_contra, _V_co], value: _T_contra, /) -> _V_co: with pytest.raises(StopIteration) as exc_info: gen.send(value) diff --git a/tests/unit_test/conftest.py b/tests/unit_test/conftest.py index 0a5a2e66..1ee020de 100644 --- a/tests/unit_test/conftest.py +++ b/tests/unit_test/conftest.py @@ -1,6 +1,5 @@ from __future__ import annotations -import pathlib from collections.abc import Callable from socket import AF_INET, IPPROTO_TCP, IPPROTO_UDP, SOCK_DGRAM, SOCK_STREAM, socket as Socket from ssl import SSLContext, SSLSocket @@ -19,13 +18,6 @@ from pytest_mock import MockerFixture -def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: - directory = pathlib.PurePath(__file__).parent - for item in items: - if pathlib.PurePath(item.fspath).is_relative_to(directory): - item.add_marker(pytest.mark.unit) - - @pytest.fixture def SO_REUSEPORT(monkeypatch: pytest.MonkeyPatch) -> int: import socket diff --git a/tests/unit_test/test_serializers/conftest.py b/tests/unit_test/test_serializers/conftest.py deleted file mode 100644 index bf1ed632..00000000 --- a/tests/unit_test/test_serializers/conftest.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import annotations - -import pathlib - -import pytest - - -def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: - directory = pathlib.PurePath(__file__).parent - for item in items: - if pathlib.PurePath(item.fspath).is_relative_to(directory): - item.add_marker(pytest.mark.serializer) diff --git a/tox.ini b/tox.ini index 3a48cefb..a039cd3e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,13 @@ [tox] minversion = 4.0 -envlist = build, py311-other, py311-{unit,functional}-{__standard__,asyncio-proactor,cbor,msgpack,encryption,sniffio,uvloop}, coverage, mypy-{full,test}, pre-commit +envlist = + build + py311-other + py311-{unit,functional}-{__standard__,cbor,msgpack,encryption,sniffio} + py311-functional-{asyncio_proactor,uvloop} + coverage + mypy-{full,test} + pre-commit skip_missing_interpreters = true [base] @@ -14,59 +21,86 @@ all_extras = encryption sniffio ; uvloop is expressly not added -pdm-deps = - pdm == 2.8.2 +allowlist_externals = + pdm + +[base-pytest] +setenv = + PYTHONASYNCIODEBUG = 1 +addopts = -p "no:cacheprovider" {tty:--color=yes} +unit_tests_rootdir = tests{/}unit_test +functional_tests_rootdir = tests{/}functional_test +xdist_dist = worksteal [testenv:py311-other] package = wheel -deps = - {[base]pdm-deps} +allowlist_externals = + {[base]allowlist_externals} setenv = {[base]setenv} - PYTHONASYNCIODEBUG = 1 - PYTEST_ADDOPTS = -p "no:cacheprovider" {tty:--color=yes} {posargs} + {[base-pytest]setenv} + PYTEST_ADDOPTS = {[base-pytest]addopts} {posargs} commands_pre = pdm sync --no-self --dev --group=test commands = pytest -m "not unit and not functional" --no-cov - -[testenv:py311-{unit,functional}-{__standard__,asyncio-proactor,cbor,msgpack,encryption,sniffio,uvloop}] +[testenv:py311-{unit,functional}-{__standard__,cbor,msgpack,encryption,sniffio}] package = wheel -platform = - asyncio-proactor: win32 - uvloop: linux|darwin extras = cbor: cbor msgpack: msgpack encryption: encryption sniffio: sniffio +allowlist_externals = + {[base]allowlist_externals} +setenv = + {[base]setenv} + {[base-pytest]setenv} + PYTEST_ADDOPTS = {[base-pytest]addopts} --cov --cov-report='' {posargs} + COVERAGE_FILE = .coverage.{envname} + unit: TESTS_ROOTDIR = {[base-pytest]unit_tests_rootdir} + functional: TESTS_ROOTDIR = {[base-pytest]functional_tests_rootdir} +passenv = + PYTEST_MAX_WORKERS +commands_pre = + pdm sync --no-self --dev --group=test +commands = + __standard__: pytest -n "{env:PYTEST_MAX_WORKERS:auto}" --dist={[base-pytest]xdist_dist} -m "not feature" {env:TESTS_ROOTDIR} + cbor: pytest -m "feature_cbor" {env:TESTS_ROOTDIR} + msgpack: pytest -m "feature_msgpack" {env:TESTS_ROOTDIR} + encryption: pytest -m "feature_encryption" {env:TESTS_ROOTDIR} + sniffio: pytest -m "feature_sniffio" {env:TESTS_ROOTDIR} + +[testenv:py311-functional-{asyncio_proactor,uvloop}] +package = wheel +platform = + asyncio_proactor: win32 + uvloop: linux|darwin +extras = uvloop: uvloop -deps = - {[base]pdm-deps} +allowlist_externals = + {[base]allowlist_externals} setenv = {[base]setenv} - PYTHONASYNCIODEBUG = 1 + {[base-pytest]setenv} + PYTEST_ADDOPTS = {[base-pytest]addopts} --cov --cov-report='' {posargs} COVERAGE_FILE = .coverage.{envname} - PYTEST_ADDOPTS = -p "no:cacheprovider" {tty:--color=yes} --cov --cov-report='' {posargs} - unit: _PYTEST_CTX = unit - functional: _PYTEST_CTX = functional + TESTS_ROOTDIR = {[base-pytest]functional_tests_rootdir} + asyncio_proactor: ASYNCIO_EVENTLOOP = asyncio-proactor + uvloop: ASYNCIO_EVENTLOOP = uvloop passenv = PYTEST_MAX_WORKERS commands_pre = pdm sync --no-self --dev --group=test commands = - __standard__: pytest -n "{env:PYTEST_MAX_WORKERS:auto}" -m "{env:_PYTEST_CTX} and not feature" - asyncio-proactor: pytest -n "{env:PYTEST_MAX_WORKERS:auto}" --asyncio-event-loop="asyncio-proactor" -m "{env:_PYTEST_CTX} and functional and asyncio and not feature" --suppress-no-test-exit-code - cbor: pytest -m "{env:_PYTEST_CTX} and feature_cbor" - msgpack: pytest -m "{env:_PYTEST_CTX} and feature_msgpack" - encryption: pytest -m "{env:_PYTEST_CTX} and feature_encryption" - sniffio: pytest -m "{env:_PYTEST_CTX} and feature_sniffio" - uvloop: pytest -n "{env:PYTEST_MAX_WORKERS:auto}" --asyncio-event-loop="uvloop" -m "{env:_PYTEST_CTX} and functional and asyncio and not feature" --suppress-no-test-exit-code + pytest -n "{env:PYTEST_MAX_WORKERS:auto}" --dist={[base-pytest]xdist_dist} --asyncio-event-loop="{env:ASYNCIO_EVENTLOOP}" -m "asyncio and not feature" {env:TESTS_ROOTDIR} [testenv:coverage] skip_install = True -depends = py311-{unit,functional}-{__standard__,asyncio-proactor,cbor,msgpack,encryption,sniffio,uvloop} +depends = + py311-{unit,functional}-{__standard__,cbor,msgpack,encryption,sniffio} + py311-functional-{asyncio_proactor,uvloop} parallel_show_output = True deps = coverage @@ -101,13 +135,11 @@ commands = package = wheel extras = {[base]all_extras} -deps = - {[base]pdm-deps} allowlist_externals = - rm + {[base]allowlist_externals} setenv = {[base]setenv} - MYPY_CACHE_DIR = {envdir}{/}.mypy_cache + MYPY_CACHE_DIR = {envtmpdir}{/}.mypy_cache MYPY_OPTS = --no-incremental --config-file {toxinidir}{/}pyproject.toml commands_pre = pdm sync --no-self --dev --group=mypy @@ -115,8 +147,6 @@ commands_pre = commands = full: mypy {env:MYPY_OPTS} -p easynetwork -p easynetwork_asyncio test: mypy {env:MYPY_OPTS} {toxinidir}{/}tests -commands_post = - rm -rf {env:MYPY_CACHE_DIR} [testenv:pre-commit] skip_install = true