From 4747cdb2701e7df6128ed43022e3d70fb880343b Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Tue, 28 Nov 2023 15:50:30 +0000 Subject: [PATCH] test: add gc fixtures to skip tests on PyPy more easily --- tests/conftest.py | 70 ++++++++++++++++++++++++- tests/crdb/test_copy.py | 10 ++-- tests/crdb/test_copy_async.py | 10 ++-- tests/pool/test_pool.py | 4 +- tests/pool/test_pool_async.py | 4 +- tests/pool/test_pool_async_noasyncio.py | 4 +- tests/pool/test_pool_common.py | 6 +-- tests/pool/test_pool_common_async.py | 6 +-- tests/pq/test_pgconn.py | 4 +- tests/test_connection.py | 5 +- tests/test_connection_async.py | 5 +- tests/test_copy.py | 25 ++++----- tests/test_copy_async.py | 25 ++++----- tests/test_cursor.py | 14 ++--- tests/test_cursor_async.py | 14 ++--- tests/test_cursor_client.py | 13 ++--- tests/test_cursor_client_async.py | 13 ++--- tests/test_cursor_common.py | 4 +- tests/test_cursor_common_async.py | 4 +- tests/test_cursor_raw.py | 13 ++--- tests/test_cursor_raw_async.py | 13 ++--- tests/test_cursor_server.py | 3 +- tests/test_cursor_server_async.py | 4 +- tests/test_errors.py | 4 +- tests/types/test_array.py | 4 +- tests/utils.py | 41 --------------- 26 files changed, 147 insertions(+), 175 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8438fc6c6..5b02855d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,8 @@ +import gc import sys import asyncio import selectors -from typing import Any, Dict, List +from typing import Any, Dict, List, Tuple import pytest @@ -100,3 +101,70 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): terminalreporter.section("failed tests ignored") for msg in allow_fail_messages: terminalreporter.line(msg) + + +NO_COUNT_TYPES: Tuple[type, ...] = () + +if sys.version_info[:2] == (3, 10): + # On my laptop there are occasional creations of a single one of these objects + # with empty content, which might be some Decimal caching. + # Keeping the guard as strict as possible, to be extended if other types + # or versions are necessary. + try: + from _contextvars import Context # type: ignore + except ImportError: + pass + else: + NO_COUNT_TYPES += (Context,) + + +class GCFixture: + __slots__ = () + + @staticmethod + def collect() -> None: + """ + gc.collect(), but more insisting. + """ + for i in range(3): + gc.collect() + + @staticmethod + def count() -> int: + """ + len(gc.get_objects()), with subtleties. + """ + + if not NO_COUNT_TYPES: + return len(gc.get_objects()) + + # Note: not using a list comprehension because it pollutes the objects list. + rv = 0 + for obj in gc.get_objects(): + if isinstance(obj, NO_COUNT_TYPES): + continue + rv += 1 + + return rv + + +@pytest.fixture(name="gc") +def fixture_gc(): + """ + Provides a consistent way to run garbage collection and count references. + + **Note:** This will skip tests on PyPy. + """ + if sys.implementation.name == "pypy": + pytest.skip(reason="depends on refcount semantics") + return GCFixture() + + +@pytest.fixture +def gc_collect(): + """ + Provides a consistent way to run garbage collection. + + **Note:** This will *not* skip tests on PyPy. + """ + return GCFixture.collect diff --git a/tests/crdb/test_copy.py b/tests/crdb/test_copy.py index 2bf714f1c..4100c33f6 100644 --- a/tests/crdb/test_copy.py +++ b/tests/crdb/test_copy.py @@ -7,7 +7,7 @@ from psycopg.pq import Format from psycopg.adapt import PyFormat from psycopg.types.numeric import Int4 -from ..utils import eur, gc_collect, gc_count +from ..utils import eur from .._test_copy import sample_text, sample_binary # noqa from .._test_copy import ensure_table, sample_records from .._test_copy import sample_tabledef as sample_tabledef_pg @@ -191,7 +191,7 @@ from copy_in group by 1, 2, 3 [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)], ) @pytest.mark.crdb_skip("copy array") -def test_copy_from_leaks(conn_cls, dsn, faker, fmt, set_types): +def test_copy_from_leaks(conn_cls, dsn, faker, fmt, set_types, gc): faker.format = PyFormat.from_pq(fmt) faker.choose_schema(ncols=20) faker.make_records(20) @@ -219,12 +219,12 @@ def test_copy_from_leaks(conn_cls, dsn, faker, fmt, set_types): for got, want in zip(recs, faker.records): faker.assert_record(got, want) - gc_collect() + gc.collect() n = [] for i in range(3): work() - gc_collect() - n.append(gc_count()) + gc.collect() + n.append(gc.count()) assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}" diff --git a/tests/crdb/test_copy_async.py b/tests/crdb/test_copy_async.py index a994d9071..17a37a95f 100644 --- a/tests/crdb/test_copy_async.py +++ b/tests/crdb/test_copy_async.py @@ -7,7 +7,7 @@ from psycopg import sql, errors as e from psycopg.adapt import PyFormat from psycopg.types.numeric import Int4 -from ..utils import eur, gc_collect, gc_count +from ..utils import eur from .._test_copy import sample_text, sample_binary # noqa from .._test_copy import ensure_table_async, sample_records from .test_copy import sample_tabledef, copyopt @@ -196,7 +196,7 @@ from copy_in group by 1, 2, 3 [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)], ) @pytest.mark.crdb_skip("copy array") -async def test_copy_from_leaks(aconn_cls, dsn, faker, fmt, set_types): +async def test_copy_from_leaks(aconn_cls, dsn, faker, fmt, set_types, gc): faker.format = PyFormat.from_pq(fmt) faker.choose_schema(ncols=20) faker.make_records(20) @@ -224,11 +224,11 @@ async def test_copy_from_leaks(aconn_cls, dsn, faker, fmt, set_types): for got, want in zip(recs, faker.records): faker.assert_record(got, want) - gc_collect() + gc.collect() n = [] for i in range(3): await work() - gc_collect() - n.append(gc_count()) + gc.collect() + n.append(gc.count()) assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}" diff --git a/tests/pool/test_pool.py b/tests/pool/test_pool.py index 11a4cf30d..7886c288e 100644 --- a/tests/pool/test_pool.py +++ b/tests/pool/test_pool.py @@ -12,7 +12,7 @@ import psycopg from psycopg.pq import TransactionStatus from psycopg.rows import class_row, Row, TupleRow -from ..utils import assert_type, Counter, gc_collect, set_autocommit +from ..utils import assert_type, Counter, set_autocommit from ..acompat import Event, spawn, gather, sleep, skip_sync from .test_pool_common import delay_connection @@ -358,7 +358,7 @@ def test_fail_rollback_close(dsn, caplog, monkeypatch): assert "BAD" in caplog.records[2].message -def test_del_no_warning(dsn, recwarn): +def test_del_no_warning(dsn, recwarn, gc_collect): p = pool.ConnectionPool(dsn, min_size=2, open=False) p.open() with p.connection() as conn: diff --git a/tests/pool/test_pool_async.py b/tests/pool/test_pool_async.py index 0d6dbdfd0..71694c0cf 100644 --- a/tests/pool/test_pool_async.py +++ b/tests/pool/test_pool_async.py @@ -9,7 +9,7 @@ import psycopg from psycopg.pq import TransactionStatus from psycopg.rows import class_row, Row, TupleRow -from ..utils import assert_type, Counter, gc_collect, set_autocommit +from ..utils import assert_type, Counter, set_autocommit from ..acompat import AEvent, spawn, gather, asleep, skip_sync from .test_pool_common_async import delay_connection @@ -362,7 +362,7 @@ async def test_fail_rollback_close(dsn, caplog, monkeypatch): assert "BAD" in caplog.records[2].message -async def test_del_no_warning(dsn, recwarn): +async def test_del_no_warning(dsn, recwarn, gc_collect): p = pool.AsyncConnectionPool(dsn, min_size=2, open=False) await p.open() async with p.connection() as conn: diff --git a/tests/pool/test_pool_async_noasyncio.py b/tests/pool/test_pool_async_noasyncio.py index 3b96d9a34..b2ad2ffa2 100644 --- a/tests/pool/test_pool_async_noasyncio.py +++ b/tests/pool/test_pool_async_noasyncio.py @@ -6,8 +6,6 @@ import sys import pytest -from ..utils import gc_collect - try: import psycopg_pool as pool except ImportError: @@ -63,7 +61,7 @@ def test_cant_create_open_outside_loop(dsn): @pytest.fixture -def asyncio_run(recwarn): +def asyncio_run(recwarn, gc_collect): """Fixture reuturning asyncio.run, but managing resources at exit. In certain runs, fd objects are leaked and the error will only be caught diff --git a/tests/pool/test_pool_common.py b/tests/pool/test_pool_common.py index 9165eb785..3b83cbad1 100644 --- a/tests/pool/test_pool_common.py +++ b/tests/pool/test_pool_common.py @@ -9,7 +9,7 @@ import pytest import psycopg -from ..utils import gc_collect, set_autocommit +from ..utils import set_autocommit from ..acompat import Event, spawn, gather, sleep, is_alive, skip_async, skip_sync try: @@ -347,12 +347,12 @@ def test_putconn_wrong_pool(pool_cls, dsn): @skip_async @pytest.mark.slow -def test_del_stops_threads(pool_cls, dsn): +def test_del_stops_threads(pool_cls, dsn, gc): p = pool_cls(dsn) assert p._sched_runner is not None ts = [p._sched_runner] + p._workers del p - gc_collect() + gc.collect() sleep(0.1) for t in ts: assert not is_alive(t), t diff --git a/tests/pool/test_pool_common_async.py b/tests/pool/test_pool_common_async.py index e1b7a2584..6605fc531 100644 --- a/tests/pool/test_pool_common_async.py +++ b/tests/pool/test_pool_common_async.py @@ -6,7 +6,7 @@ import pytest import psycopg -from ..utils import gc_collect, set_autocommit +from ..utils import set_autocommit from ..acompat import AEvent, spawn, gather, asleep, is_alive, skip_async, skip_sync try: @@ -364,12 +364,12 @@ async def test_putconn_wrong_pool(pool_cls, dsn): @skip_async @pytest.mark.slow -async def test_del_stops_threads(pool_cls, dsn): +async def test_del_stops_threads(pool_cls, dsn, gc): p = pool_cls(dsn) assert p._sched_runner is not None ts = [p._sched_runner] + p._workers del p - gc_collect() + gc.collect() await asleep(0.1) for t in ts: assert not is_alive(t), t diff --git a/tests/pq/test_pgconn.py b/tests/pq/test_pgconn.py index 05661511a..ff18379a4 100644 --- a/tests/pq/test_pgconn.py +++ b/tests/pq/test_pgconn.py @@ -11,8 +11,6 @@ import psycopg from psycopg import pq import psycopg.generators -from ..utils import gc_collect - def test_connectdb(dsn): conn = pq.PGconn.connect(dsn.encode()) @@ -82,7 +80,7 @@ def test_finish(pgconn): @pytest.mark.slow -def test_weakref(dsn): +def test_weakref(dsn, gc_collect): conn = pq.PGconn.connect(dsn.encode()) w = weakref.ref(conn) conn.finish() diff --git a/tests/test_connection.py b/tests/test_connection.py index ef066653f..698ff8d64 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -13,7 +13,6 @@ from psycopg import Notify, pq, errors as e from psycopg.rows import tuple_row from psycopg.conninfo import conninfo_to_dict, make_conninfo -from .utils import gc_collect from .acompat import is_async, skip_sync, skip_async from ._test_cursor import my_row_factory from ._test_connection import tx_params, tx_params_isolation, tx_values_map @@ -137,7 +136,7 @@ def test_cursor_closed(conn): and (not is_async(__name__)), reason="Something with Exceptions, C, Python 3.12", ) -def test_connection_warn_close(conn_cls, dsn, recwarn): +def test_connection_warn_close(conn_cls, dsn, recwarn, gc_collect): conn = conn_cls.connect(dsn) conn.close() del conn @@ -244,7 +243,7 @@ def test_context_active_rollback_no_clobber(conn_cls, dsn, caplog): @pytest.mark.slow -def test_weakref(conn_cls, dsn): +def test_weakref(conn_cls, dsn, gc_collect): conn = conn_cls.connect(dsn) w = weakref.ref(conn) conn.close() diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index d0110a01a..32cc417b7 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -10,7 +10,6 @@ from psycopg import Notify, pq, errors as e from psycopg.rows import tuple_row from psycopg.conninfo import conninfo_to_dict, make_conninfo -from .utils import gc_collect from .acompat import is_async, skip_sync, skip_async from ._test_cursor import my_row_factory from ._test_connection import tx_params, tx_params_isolation, tx_values_map @@ -134,7 +133,7 @@ async def test_cursor_closed(aconn): and not is_async(__name__), reason="Something with Exceptions, C, Python 3.12", ) -async def test_connection_warn_close(aconn_cls, dsn, recwarn): +async def test_connection_warn_close(aconn_cls, dsn, recwarn, gc_collect): conn = await aconn_cls.connect(dsn) await conn.close() del conn @@ -242,7 +241,7 @@ async def test_context_active_rollback_no_clobber(aconn_cls, dsn, caplog): @pytest.mark.slow -async def test_weakref(aconn_cls, dsn): +async def test_weakref(aconn_cls, dsn, gc_collect): conn = await aconn_cls.connect(dsn) w = weakref.ref(conn) await conn.close() diff --git a/tests/test_copy.py b/tests/test_copy.py index 25d15f2f8..fda854e60 100644 --- a/tests/test_copy.py +++ b/tests/test_copy.py @@ -3,7 +3,6 @@ # DO NOT CHANGE! Change the original file instead. import string import hashlib -import sys from io import BytesIO, StringIO from random import choice, randrange from itertools import cycle @@ -21,7 +20,7 @@ from psycopg.types import TypeInfo from psycopg.types.hstore import register_hstore from psycopg.types.numeric import Int4 -from .utils import eur, gc_collect, gc_count +from .utils import eur from ._test_copy import sample_text, sample_binary, sample_binary_rows # noqa from ._test_copy import sample_values, sample_records, sample_tabledef from ._test_copy import ensure_table, py_to_raw, special_chars, FileWriter @@ -674,14 +673,11 @@ def test_connection_writer(conn, format, buffer): @pytest.mark.slow -@pytest.mark.skipif( - sys.implementation.name == "pypy", reason="depends on refcount semantics" -) @pytest.mark.parametrize( "fmt, set_types", [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)] ) @pytest.mark.parametrize("method", ["read", "iter", "row", "rows"]) -def test_copy_to_leaks(conn_cls, dsn, faker, fmt, set_types, method): +def test_copy_to_leaks(conn_cls, dsn, faker, fmt, set_types, method, gc): faker.format = PyFormat.from_pq(fmt) faker.choose_schema(ncols=20) faker.make_records(20) @@ -721,24 +717,21 @@ def test_copy_to_leaks(conn_cls, dsn, faker, fmt, set_types, method): elif method == "rows": list(copy.rows()) - gc_collect() + gc.collect() n = [] for i in range(3): work() - gc_collect() - n.append(gc_count()) + gc.collect() + n.append(gc.count()) assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}" @pytest.mark.slow -@pytest.mark.skipif( - sys.implementation.name == "pypy", reason="depends on refcount semantics" -) @pytest.mark.parametrize( "fmt, set_types", [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)] ) -def test_copy_from_leaks(conn_cls, dsn, faker, fmt, set_types): +def test_copy_from_leaks(conn_cls, dsn, faker, fmt, set_types, gc): faker.format = PyFormat.from_pq(fmt) faker.choose_schema(ncols=20) faker.make_records(20) @@ -766,12 +759,12 @@ def test_copy_from_leaks(conn_cls, dsn, faker, fmt, set_types): for got, want in zip(recs, faker.records): faker.assert_record(got, want) - gc_collect() + gc.collect() n = [] for i in range(3): work() - gc_collect() - n.append(gc_count()) + gc.collect() + n.append(gc.count()) assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}" diff --git a/tests/test_copy_async.py b/tests/test_copy_async.py index 807d94bd5..f52cc6144 100644 --- a/tests/test_copy_async.py +++ b/tests/test_copy_async.py @@ -1,6 +1,5 @@ import string import hashlib -import sys from io import BytesIO, StringIO from random import choice, randrange from itertools import cycle @@ -18,7 +17,7 @@ from psycopg.types import TypeInfo from psycopg.types.hstore import register_hstore from psycopg.types.numeric import Int4 -from .utils import eur, gc_collect, gc_count +from .utils import eur from .acompat import alist from ._test_copy import sample_text, sample_binary, sample_binary_rows # noqa from ._test_copy import sample_values, sample_records, sample_tabledef @@ -691,15 +690,12 @@ async def test_connection_writer(aconn, format, buffer): @pytest.mark.slow -@pytest.mark.skipif( - sys.implementation.name == "pypy", reason="depends on refcount semantics" -) @pytest.mark.parametrize( "fmt, set_types", [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)], ) @pytest.mark.parametrize("method", ["read", "iter", "row", "rows"]) -async def test_copy_to_leaks(aconn_cls, dsn, faker, fmt, set_types, method): +async def test_copy_to_leaks(aconn_cls, dsn, faker, fmt, set_types, method, gc): faker.format = PyFormat.from_pq(fmt) faker.choose_schema(ncols=20) faker.make_records(20) @@ -739,25 +735,22 @@ async def test_copy_to_leaks(aconn_cls, dsn, faker, fmt, set_types, method): elif method == "rows": await alist(copy.rows()) - gc_collect() + gc.collect() n = [] for i in range(3): await work() - gc_collect() - n.append(gc_count()) + gc.collect() + n.append(gc.count()) assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}" @pytest.mark.slow -@pytest.mark.skipif( - sys.implementation.name == "pypy", reason="depends on refcount semantics" -) @pytest.mark.parametrize( "fmt, set_types", [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)], ) -async def test_copy_from_leaks(aconn_cls, dsn, faker, fmt, set_types): +async def test_copy_from_leaks(aconn_cls, dsn, faker, fmt, set_types, gc): faker.format = PyFormat.from_pq(fmt) faker.choose_schema(ncols=20) faker.make_records(20) @@ -785,12 +778,12 @@ async def test_copy_from_leaks(aconn_cls, dsn, faker, fmt, set_types): for got, want in zip(recs, faker.records): faker.assert_record(got, want) - gc_collect() + gc.collect() n = [] for i in range(3): await work() - gc_collect() - n.append(gc_count()) + gc.collect() + n.append(gc.count()) assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}" diff --git a/tests/test_cursor.py b/tests/test_cursor.py index 845b0941c..28b96a7b4 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -7,12 +7,9 @@ Tests for psycopg.Cursor that are not supposed to pass for subclasses. import pytest import psycopg -import sys from psycopg import pq, rows, errors as e from psycopg.adapt import PyFormat -from .utils import gc_collect, gc_count - def test_default_cursor(conn): cur = conn.cursor() @@ -66,14 +63,11 @@ def test_query_params_executemany(conn): @pytest.mark.slow -@pytest.mark.skipif( - sys.implementation.name == "pypy", reason="depends on refcount semantics" -) @pytest.mark.parametrize("fmt", PyFormat) @pytest.mark.parametrize("fmt_out", pq.Format) @pytest.mark.parametrize("fetch", ["one", "many", "all", "iter"]) @pytest.mark.parametrize("row_factory", ["tuple_row", "dict_row", "namedtuple_row"]) -def test_leak(conn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory): +def test_leak(conn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory, gc): faker.format = fmt faker.choose_schema(ncols=5) faker.make_records(10) @@ -105,10 +99,10 @@ def test_leak(conn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory): pass n = [] - gc_collect() + gc.collect() for i in range(3): work() - gc_collect() - n.append(gc_count()) + gc.collect() + n.append(gc.count()) assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}" diff --git a/tests/test_cursor_async.py b/tests/test_cursor_async.py index 00e10883c..9c5c3ebdd 100644 --- a/tests/test_cursor_async.py +++ b/tests/test_cursor_async.py @@ -4,12 +4,9 @@ Tests for psycopg.Cursor that are not supposed to pass for subclasses. import pytest import psycopg -import sys from psycopg import pq, rows, errors as e from psycopg.adapt import PyFormat -from .utils import gc_collect, gc_count - async def test_default_cursor(aconn): cur = aconn.cursor() @@ -65,14 +62,11 @@ async def test_query_params_executemany(aconn): @pytest.mark.slow -@pytest.mark.skipif( - sys.implementation.name == "pypy", reason="depends on refcount semantics" -) @pytest.mark.parametrize("fmt", PyFormat) @pytest.mark.parametrize("fmt_out", pq.Format) @pytest.mark.parametrize("fetch", ["one", "many", "all", "iter"]) @pytest.mark.parametrize("row_factory", ["tuple_row", "dict_row", "namedtuple_row"]) -async def test_leak(aconn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory): +async def test_leak(aconn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory, gc): faker.format = fmt faker.choose_schema(ncols=5) faker.make_records(10) @@ -106,10 +100,10 @@ async def test_leak(aconn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory): pass n = [] - gc_collect() + gc.collect() for i in range(3): await work() - gc_collect() - n.append(gc_count()) + gc.collect() + n.append(gc.count()) assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}" diff --git a/tests/test_cursor_client.py b/tests/test_cursor_client.py index a0284d17a..f9d9d9744 100644 --- a/tests/test_cursor_client.py +++ b/tests/test_cursor_client.py @@ -5,10 +5,8 @@ import datetime as dt import pytest import psycopg -import sys from psycopg import rows -from .utils import gc_collect, gc_count from .fix_crdb import crdb_encoding @@ -79,12 +77,9 @@ def test_query_params_executemany(conn): @pytest.mark.slow -@pytest.mark.skipif( - sys.implementation.name == "pypy", reason="depends on refcount semantics" -) @pytest.mark.parametrize("fetch", ["one", "many", "all", "iter"]) @pytest.mark.parametrize("row_factory", ["tuple_row", "dict_row", "namedtuple_row"]) -def test_leak(conn_cls, dsn, faker, fetch, row_factory): +def test_leak(conn_cls, dsn, faker, fetch, row_factory, gc): faker.choose_schema(ncols=5) faker.make_records(10) row_factory = getattr(rows, row_factory) @@ -115,11 +110,11 @@ def test_leak(conn_cls, dsn, faker, fetch, row_factory): pass n = [] - gc_collect() + gc.collect() for i in range(3): work() - gc_collect() - n.append(gc_count()) + gc.collect() + n.append(gc.count()) assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}" diff --git a/tests/test_cursor_client_async.py b/tests/test_cursor_client_async.py index a3ba12ae6..63fb5d5af 100644 --- a/tests/test_cursor_client_async.py +++ b/tests/test_cursor_client_async.py @@ -2,10 +2,8 @@ import datetime as dt import pytest import psycopg -import sys from psycopg import rows -from .utils import gc_collect, gc_count from .fix_crdb import crdb_encoding @@ -78,12 +76,9 @@ async def test_query_params_executemany(aconn): @pytest.mark.slow -@pytest.mark.skipif( - sys.implementation.name == "pypy", reason="depends on refcount semantics" -) @pytest.mark.parametrize("fetch", ["one", "many", "all", "iter"]) @pytest.mark.parametrize("row_factory", ["tuple_row", "dict_row", "namedtuple_row"]) -async def test_leak(aconn_cls, dsn, faker, fetch, row_factory): +async def test_leak(aconn_cls, dsn, faker, fetch, row_factory, gc): faker.choose_schema(ncols=5) faker.make_records(10) row_factory = getattr(rows, row_factory) @@ -116,11 +111,11 @@ async def test_leak(aconn_cls, dsn, faker, fetch, row_factory): pass n = [] - gc_collect() + gc.collect() for i in range(3): await work() - gc_collect() - n.append(gc_count()) + gc.collect() + n.append(gc.count()) assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}" diff --git a/tests/test_cursor_common.py b/tests/test_cursor_common.py index 535aa0153..159e67cb1 100644 --- a/tests/test_cursor_common.py +++ b/tests/test_cursor_common.py @@ -17,7 +17,7 @@ from psycopg import sql, rows from psycopg.adapt import PyFormat from psycopg.types import TypeInfo -from .utils import gc_collect, raiseif +from .utils import raiseif from .acompat import closing from .fix_crdb import crdb_encoding from ._test_cursor import my_row_factory, ph @@ -123,7 +123,7 @@ def test_context(conn): @pytest.mark.slow -def test_weakref(conn): +def test_weakref(conn, gc_collect): cur = conn.cursor() w = weakref.ref(cur) cur.close() diff --git a/tests/test_cursor_common_async.py b/tests/test_cursor_common_async.py index 4268fdd70..840de65ca 100644 --- a/tests/test_cursor_common_async.py +++ b/tests/test_cursor_common_async.py @@ -14,7 +14,7 @@ from psycopg import sql, rows from psycopg.adapt import PyFormat from psycopg.types import TypeInfo -from .utils import gc_collect, raiseif +from .utils import raiseif from .acompat import aclosing, alist, anext from .fix_crdb import crdb_encoding from ._test_cursor import my_row_factory, ph @@ -121,7 +121,7 @@ async def test_context(aconn): @pytest.mark.slow -async def test_weakref(aconn): +async def test_weakref(aconn, gc_collect): cur = aconn.cursor() w = weakref.ref(cur) await cur.close() diff --git a/tests/test_cursor_raw.py b/tests/test_cursor_raw.py index 76726aa8d..059d22d1c 100644 --- a/tests/test_cursor_raw.py +++ b/tests/test_cursor_raw.py @@ -3,12 +3,10 @@ # DO NOT CHANGE! Change the original file instead. import pytest import psycopg -import sys from psycopg import pq, rows, errors as e from psycopg.adapt import PyFormat from ._test_cursor import ph -from .utils import gc_collect, gc_count @pytest.fixture @@ -72,14 +70,11 @@ def test_query_params_executemany(conn): @pytest.mark.slow -@pytest.mark.skipif( - sys.implementation.name == "pypy", reason="depends on refcount semantics" -) @pytest.mark.parametrize("fmt", PyFormat) @pytest.mark.parametrize("fmt_out", pq.Format) @pytest.mark.parametrize("fetch", ["one", "many", "all", "iter"]) @pytest.mark.parametrize("row_factory", ["tuple_row", "dict_row", "namedtuple_row"]) -def test_leak(conn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory): +def test_leak(conn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory, gc): faker.format = fmt faker.choose_schema(ncols=5) faker.make_records(10) @@ -112,9 +107,9 @@ def test_leak(conn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory): pass n = [] - gc_collect() + gc.collect() for i in range(3): work() - gc_collect() - n.append(gc_count()) + gc.collect() + n.append(gc.count()) assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}" diff --git a/tests/test_cursor_raw_async.py b/tests/test_cursor_raw_async.py index 24d69cebb..e6e85124a 100644 --- a/tests/test_cursor_raw_async.py +++ b/tests/test_cursor_raw_async.py @@ -1,11 +1,9 @@ import pytest import psycopg -import sys from psycopg import pq, rows, errors as e from psycopg.adapt import PyFormat from ._test_cursor import ph -from .utils import gc_collect, gc_count @pytest.fixture @@ -69,14 +67,11 @@ async def test_query_params_executemany(aconn): @pytest.mark.slow -@pytest.mark.skipif( - sys.implementation.name == "pypy", reason="depends on refcount semantics" -) @pytest.mark.parametrize("fmt", PyFormat) @pytest.mark.parametrize("fmt_out", pq.Format) @pytest.mark.parametrize("fetch", ["one", "many", "all", "iter"]) @pytest.mark.parametrize("row_factory", ["tuple_row", "dict_row", "namedtuple_row"]) -async def test_leak(aconn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory): +async def test_leak(aconn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory, gc): faker.format = fmt faker.choose_schema(ncols=5) faker.make_records(10) @@ -109,9 +104,9 @@ async def test_leak(aconn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory): pass n = [] - gc_collect() + gc.collect() for i in range(3): await work() - gc_collect() - n.append(gc_count()) + gc.collect() + n.append(gc.count()) assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}" diff --git a/tests/test_cursor_server.py b/tests/test_cursor_server.py index 9fdfdbda7..b4259abb2 100644 --- a/tests/test_cursor_server.py +++ b/tests/test_cursor_server.py @@ -7,7 +7,6 @@ import psycopg from psycopg import rows, errors as e from psycopg.pq import Format -from .utils import gc_collect pytestmark = pytest.mark.crdb_skip("server-side cursor") @@ -256,7 +255,7 @@ def test_close_no_clobber(conn): cur.fetchall() -def test_warn_close(conn, recwarn): +def test_warn_close(conn, recwarn, gc_collect): recwarn.clear() cur = conn.cursor("foo") cur.execute("select generate_series(1, 10) as bar") diff --git a/tests/test_cursor_server_async.py b/tests/test_cursor_server_async.py index 0417ab3c3..0f0efa2a9 100644 --- a/tests/test_cursor_server_async.py +++ b/tests/test_cursor_server_async.py @@ -5,7 +5,7 @@ from psycopg import rows, errors as e from psycopg.pq import Format from .acompat import alist -from .utils import gc_collect + pytestmark = pytest.mark.crdb_skip("server-side cursor") @@ -262,7 +262,7 @@ async def test_close_no_clobber(aconn): await cur.fetchall() -async def test_warn_close(aconn, recwarn): +async def test_warn_close(aconn, recwarn, gc_collect): recwarn.clear() cur = aconn.cursor("foo") await cur.execute("select generate_series(1, 10) as bar") diff --git a/tests/test_errors.py b/tests/test_errors.py index ddf57513c..a5016ae32 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -9,7 +9,7 @@ import psycopg from psycopg import pq from psycopg import errors as e -from .utils import eur, gc_collect +from .utils import eur from .fix_crdb import is_crdb @@ -187,7 +187,7 @@ def test_diag_pickle(conn): (pq.__impl__ in ("c", "binary") and sys.version_info[:2] == (3, 12)), reason="Something with Exceptions, C, Python 3.12", ) -def test_diag_survives_cursor(conn): +def test_diag_survives_cursor(conn, gc_collect): cur = conn.cursor() with pytest.raises(e.Error) as exc: cur.execute("select * from nosuchtable") diff --git a/tests/types/test_array.py b/tests/types/test_array.py index 5f84c5a19..4da6a2149 100644 --- a/tests/types/test_array.py +++ b/tests/types/test_array.py @@ -13,8 +13,6 @@ from psycopg.types import TypeInfo from psycopg.postgres import types as builtins from psycopg.types.array import register_array -from ..utils import gc_collect - tests_str = [ ([[[[[["a"]]]]]], "{{{{{{a}}}}}}"), @@ -342,7 +340,7 @@ def test_all_chars_with_bounds(conn, fmt_out): @pytest.mark.slow -def test_register_array_leak(conn): +def test_register_array_leak(conn, gc_collect): info = TypeInfo.fetch(conn, "date") ntypes = [] for i in range(2): diff --git a/tests/utils.py b/tests/utils.py index 88a6bb9a0..8eef880bd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,3 @@ -import gc import re import sys import operator @@ -156,46 +155,6 @@ class VersionCheck: return (ver_maj, ver_min, ver_fix) -def gc_collect(): - """ - gc.collect(), but more insisting. - """ - for i in range(3): - gc.collect() - - -NO_COUNT_TYPES: Tuple[type, ...] = () - -if sys.version_info[:2] == (3, 10): - # On my laptop there are occasional creations of a single one of these objects - # with empty content, which might be some Decimal caching. - # Keeping the guard as strict as possible, to be extended if other types - # or versions are necessary. - try: - from _contextvars import Context # type: ignore - except ImportError: - pass - else: - NO_COUNT_TYPES += (Context,) - - -def gc_count() -> int: - """ - len(gc.get_objects()), with subtleties. - """ - if not NO_COUNT_TYPES: - return len(gc.get_objects()) - - # Note: not using a list comprehension because it pollutes the objects list. - rv = 0 - for obj in gc.get_objects(): - if isinstance(obj, NO_COUNT_TYPES): - continue - rv += 1 - - return rv - - @contextmanager def raiseif(cond, *args, **kwargs): """ -- 2.39.5