From ecb5069dde67b9116d81245642caea60f3aa0bdf Mon Sep 17 00:00:00 2001 From: Denis Laxalde Date: Thu, 8 Sep 2022 19:01:19 +0200 Subject: [PATCH] test: allow to select async backend and implementation We add a --anyio={asyncio,trio} option to pytest to select the AnyIO backend to run async tests. When this option is set, we'll use the AnyIO implementations of psycopg API (i.e. AnyIOConnection, resp. waiting functions, etc.); otherwise (the default), we'll use plain asyncio implementations (i.e. AsyncConnection). This way, we can now run the test suite with: - plain asyncio implementations (previously achieved through the asyncio backend for AnyIO pytest plugin), - AnyIO implementations, with asyncio backend (new from this commit), - AnyIO implementations, with trio backend (previously achieved through the Trio backend for AnyIO pytest plugin). Accordingly, the anyio_backend fixture is no longer parametrized and its value depends on the --anyio option. Selection of whether to use AnyIO or plain asyncio implementations is done through the new 'use_anyio' fixture, which simply detects if no --anyio option got specified. This new fixture is used everywhere we used anyio_backend_name fixture previously to select either plain asyncio or anyio variants of test cases. The fixture pulls (but does not use) anyio_backend so that all tests using it will be detected as async. In CI, we add a new pytest_opts axis to the matrix to cover all configurations in various environments. --- .github/workflows/tests.yml | 74 ++++++++++++++++++---------------- tests/conftest.py | 45 +++++++++++++++------ tests/fix_db.py | 14 ++----- tests/pool/conftest.py | 9 ++--- tests/test_connection_async.py | 4 +- tests/test_waiting.py | 14 ++----- 6 files changed, 85 insertions(+), 75 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0d29bdb3e..c02fe59d0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,28 +26,29 @@ jobs: matrix: include: # Test different combinations of Python, Postgres, libpq. - - {impl: python, python: "3.7", postgres: "postgres:10", libpq: newest} - - {impl: python, python: "3.8", postgres: "postgres:12"} - - {impl: python, python: "3.9", postgres: "postgres:13"} - - {impl: python, python: "3.10", postgres: "postgres:14"} - - {impl: python, python: "3.11", postgres: "postgres:15", libpq: oldest} + - {impl: python, python: "3.7", postgres: "postgres:10", libpq: newest, pytest_opts: "--anyio=asyncio"} + - {impl: python, python: "3.8", postgres: "postgres:12", pytest_opts: ""} + - {impl: python, python: "3.9", postgres: "postgres:13", pytest_opts: "--anyio=trio"} + - {impl: python, python: "3.10", postgres: "postgres:14", pytest_opts: ""} + - {impl: python, python: "3.11", postgres: "postgres:15", libpq: oldest, pytest_opts: ""} - - {impl: c, python: "3.7", postgres: "postgres:15", libpq: newest} - - {impl: c, python: "3.8", postgres: "postgres:13"} - - {impl: c, python: "3.9", postgres: "postgres:14"} - - {impl: c, python: "3.10", postgres: "postgres:11", libpq: oldest} - - {impl: c, python: "3.11", postgres: "postgres:10", libpq: newest} + - {impl: c, python: "3.7", postgres: "postgres:15", libpq: newest, pytest_opts: ""} + - {impl: c, python: "3.8", postgres: "postgres:13", pytest_opts: "--anyio=asyncio"} + - {impl: c, python: "3.9", postgres: "postgres:14", pytest_opts: ""} + - {impl: c, python: "3.10", postgres: "postgres:11", libpq: oldest, pytest_opts: "--anyio=trio"} + - {impl: c, python: "3.11", postgres: "postgres:10", libpq: newest, pytest_opts: ""} - - {impl: python, python: "3.9", ext: dns, postgres: "postgres:14"} - - {impl: python, python: "3.9", ext: postgis, postgres: "postgis/postgis"} + - {impl: python, python: "3.9", ext: dns, postgres: "postgres:14", pytest_opts: ""} + - {impl: python, python: "3.9", ext: postgis, postgres: "postgis/postgis", pytest_opts: ""} # Test with minimum dependencies versions - - {impl: c, python: "3.7", ext: min, postgres: "postgres:15"} + - {impl: c, python: "3.7", ext: min, postgres: "postgres:15", pytest_opts: ""} env: PSYCOPG_IMPL: ${{ matrix.impl }} DEPS: ./psycopg[test] ./psycopg_pool PSYCOPG_TEST_DSN: "host=127.0.0.1 user=postgres password=password" + PYTEST_ADDOPTS: ${{ matrix.pytest_opts }} MARKERS: "" steps: @@ -110,21 +111,22 @@ jobs: fail-fast: false matrix: include: - - {impl: python, python: "3.7"} - - {impl: python, python: "3.8"} - - {impl: python, python: "3.9"} - - {impl: python, python: "3.10"} - - {impl: python, python: "3.11"} - - {impl: c, python: "3.7"} - - {impl: c, python: "3.8"} - - {impl: c, python: "3.9"} - - {impl: c, python: "3.10"} - - {impl: c, python: "3.11"} + - {impl: python, python: "3.7", pytest_opts: ""} + - {impl: python, python: "3.8", pytest_opts: "--anyio=asyncio"} + - {impl: python, python: "3.9", pytest_opts: ""} + - {impl: python, python: "3.10", pytest_opts: "--anyio=trio"} + - {impl: python, python: "3.11", pytest_opts: ""} + - {impl: c, python: "3.7", pytest_opts: "--anyio=trio"} + - {impl: c, python: "3.8", pytest_opts: ""} + - {impl: c, python: "3.9", pytest_opts: "--anyio=asyncio"} + - {impl: c, python: "3.10", pytest_opts: ""} + - {impl: c, python: "3.11", pytest_opts: ""} env: PSYCOPG_IMPL: ${{ matrix.impl }} DEPS: ./psycopg[test] ./psycopg_pool PSYCOPG_TEST_DSN: "host=127.0.0.1 user=runner dbname=postgres" + PYTEST_ADDOPTS: ${{ matrix.pytest_opts }} # MacOS on GitHub Actions seems particularly slow. # Don't run timing-based tests as they regularly fail. # pproxy-based tests fail too, with the proxy not coming up in 2s. @@ -165,21 +167,22 @@ jobs: fail-fast: false matrix: include: - - {impl: python, python: "3.7"} - - {impl: python, python: "3.8"} - - {impl: python, python: "3.9"} - - {impl: python, python: "3.10"} - - {impl: python, python: "3.11"} - - {impl: c, python: "3.7"} - - {impl: c, python: "3.8"} - - {impl: c, python: "3.9"} - - {impl: c, python: "3.10"} - - {impl: c, python: "3.11"} + - {impl: python, python: "3.7", pytest_opts: "--anyio=asyncio"} + - {impl: python, python: "3.8", pytest_opts: ""} + - {impl: python, python: "3.9", pytest_opts: "--anyio=trio"} + - {impl: python, python: "3.10", pytest_opts: ""} + - {impl: python, python: "3.11", pytest_opts: ""} + - {impl: c, python: "3.7", pytest_opts: ""} + - {impl: c, python: "3.8", pytest_opts: "--anyio=trio"} + - {impl: c, python: "3.9", pytest_opts: ""} + - {impl: c, python: "3.10", pytest_opts: "--anyio=asyncio"} + - {impl: c, python: "3.11", pytest_opts: ""} env: PSYCOPG_IMPL: ${{ matrix.impl }} DEPS: ./psycopg[test] ./psycopg_pool PSYCOPG_TEST_DSN: "host=127.0.0.1 dbname=postgres" + PYTEST_ADDOPTS: ${{ matrix.pytest_opts }} # On windows pproxy doesn't seem very happy. Also a few timing test fail. NOT_MARKERS: "timing proxy mypy" @@ -235,12 +238,13 @@ jobs: fail-fast: false matrix: include: - - {impl: c, crdb: "latest-v22.1", python: "3.10", libpq: newest} - - {impl: python, crdb: "latest-v22.2", python: "3.11"} + - {impl: c, crdb: "latest-v22.1", python: "3.10", libpq: newest, pytest_opts: "--anyio=trio"} + - {impl: python, crdb: "latest-v22.2", python: "3.11", pytest_opts: ""} env: PSYCOPG_IMPL: ${{ matrix.impl }} DEPS: ./psycopg[test] ./psycopg_pool PSYCOPG_TEST_DSN: "host=127.0.0.1 port=26257 user=root dbname=defaultdb" + PYTEST_ADDOPTS: ${{ matrix.pytest_opts }} steps: - uses: actions/checkout@v3 diff --git a/tests/conftest.py b/tests/conftest.py index 6acad9aa2..8c6f0728f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,6 +34,15 @@ def pytest_configure(config): def pytest_addoption(parser): + parser.addoption( + "--anyio", + choices=["asyncio", "trio"], + help=( + "Use AnyIO implementation of the async API, run tests with " + "specified backend. If unset, use plain asyncio implementation, " + "and run with asyncio backend." + ), + ) parser.addoption( "--loop", choices=["default", "uvloop"], @@ -46,8 +55,15 @@ def pytest_report_header(config): rv = [] rv.append(f"default selector: {selectors.DefaultSelector.__name__}") + backend = config.getoption("--anyio") + if backend: + rv.append(f"AnyIO backend: {backend}") loop = config.getoption("--loop") if loop != "default": + if backend not in (None, "asyncio"): + raise pytest.UsageError( + f"--loop={loop} is incompatible with --anyio={backend}" + ) rv.append(f"asyncio loop: {loop}") return rv @@ -67,23 +83,28 @@ def pytest_sessionstart(session): cache.set("segfault", True) +@pytest.fixture(scope="session") +def use_anyio(pytestconfig, anyio_backend): + """True if AnyIO-based implementations of Psycopg API should be used.""" + return pytestconfig.option.anyio is not None + + asyncio_options: Dict[str, Any] = {} if sys.platform == "win32" and sys.version_info >= (3, 8): asyncio_options["policy"] = asyncio.WindowsSelectorEventLoopPolicy() -@pytest.fixture( - params=[ - pytest.param(("asyncio", asyncio_options.copy()), id="asyncio"), - pytest.param(("trio", {}), id="trio"), - ], - scope="session", -) -def anyio_backend(request): - backend, options = request.param - if request.config.option.loop == "uvloop": - options["use_uvloop"] = True - return backend, options +@pytest.fixture(scope="session") +def anyio_backend(pytestconfig): + opt = pytestconfig.option.anyio + if opt in (None, "asyncio"): + options = asyncio_options.copy() + if pytestconfig.option.loop == "uvloop": + options["use_uvloop"] = True + return "asyncio", options + else: + assert opt == "trio" + return "trio", {} @pytest.fixture(scope="session") diff --git a/tests/fix_db.py b/tests/fix_db.py index 7542ae8ae..7d524ed5a 100644 --- a/tests/fix_db.py +++ b/tests/fix_db.py @@ -239,21 +239,15 @@ def conn_cls(session_dsn): @pytest.fixture(scope="session") -def aconn_cls(session_dsn, anyio_backend_name): - cls_by_backend = { - "asyncio": psycopg.AsyncConnection, - "trio": psycopg.AnyIOConnection, - } +def aconn_cls(session_dsn, use_anyio): + cls = psycopg.AnyIOConnection if use_anyio else psycopg.AsyncConnection if crdb_version: from psycopg.crdb import AsyncCrdbConnection, AnyIOCrdbConnection - cls_by_backend = { - "asyncio": AsyncCrdbConnection, - "trio": AnyIOCrdbConnection, - } + cls = AnyIOCrdbConnection if use_anyio else AsyncCrdbConnection - return cls_by_backend[anyio_backend_name] + return cls @pytest.fixture(scope="session") diff --git a/tests/pool/conftest.py b/tests/pool/conftest.py index a4d1f3564..052794f3c 100644 --- a/tests/pool/conftest.py +++ b/tests/pool/conftest.py @@ -3,12 +3,9 @@ from ..conftest import asyncio_options -@pytest.fixture( - params=[pytest.param(("asyncio", asyncio_options.copy()), id="asyncio")], - scope="session", -) +@pytest.fixture(scope="session") def anyio_backend(request): - backend, options = request.param + options = asyncio_options.copy() if request.config.option.loop == "uvloop": options["use_uvloop"] = True - return backend, options + return "asyncio", options diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index 34d2ec3b7..076899493 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -737,7 +737,7 @@ async def test_cancel_closed(aconn): @pytest.fixture -async def fake_resolve(monkeypatch, anyio_backend_name): +async def fake_resolve(monkeypatch, use_anyio): fake_hosts = {"foo.com": "1.1.1.1"} async def fake_getaddrinfo(host, port, **kwargs): @@ -748,7 +748,7 @@ async def fake_getaddrinfo(host, port, **kwargs): else: return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (addr, 432))] - if anyio_backend_name == "asyncio": + if not use_anyio: monkeypatch.setattr(asyncio.get_running_loop(), "getaddrinfo", fake_getaddrinfo) else: monkeypatch.setattr(anyio, "getaddrinfo", fake_getaddrinfo) diff --git a/tests/test_waiting.py b/tests/test_waiting.py index f020d4815..a071471d4 100644 --- a/tests/test_waiting.py +++ b/tests/test_waiting.py @@ -116,19 +116,13 @@ def test_wait_large_fd(dsn, waitfn): @pytest.fixture -def wait_async(anyio_backend_name): - return { - "asyncio": waiting.wait_asyncio, - "trio": waiting_anyio.wait, - }[anyio_backend_name] +def wait_async(use_anyio): + return waiting_anyio.wait if use_anyio else waiting.wait_asyncio @pytest.fixture -def wait_conn_async(anyio_backend_name): - return { - "asyncio": waiting.wait_conn_asyncio, - "trio": waiting_anyio.wait_conn, - }[anyio_backend_name] +def wait_conn_async(use_anyio): + return waiting_anyio.wait_conn if use_anyio else waiting.wait_conn_asyncio @pytest.mark.parametrize("timeout", timeouts)