dirigent-testing 0.18.0__tar.gz → 0.18.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dirigent_testing-0.18.0 → dirigent_testing-0.18.1}/PKG-INFO +3 -3
- {dirigent_testing-0.18.0 → dirigent_testing-0.18.1}/pyproject.toml +3 -3
- {dirigent_testing-0.18.0 → dirigent_testing-0.18.1}/pyproject.toml.orig +3 -3
- {dirigent_testing-0.18.0 → dirigent_testing-0.18.1}/src/dirigent_testing/__init__.py +2 -0
- dirigent_testing-0.18.1/src/dirigent_testing/fixtures.py +119 -0
- dirigent_testing-0.18.0/src/dirigent_testing/fixtures.py +0 -58
- {dirigent_testing-0.18.0 → dirigent_testing-0.18.1}/LICENSE +0 -0
- {dirigent_testing-0.18.0 → dirigent_testing-0.18.1}/README.md +0 -0
- {dirigent_testing-0.18.0 → dirigent_testing-0.18.1}/src/dirigent_testing/conformance.py +0 -0
- {dirigent_testing-0.18.0 → dirigent_testing-0.18.1}/src/dirigent_testing/doubles.py +0 -0
- {dirigent_testing-0.18.0 → dirigent_testing-0.18.1}/src/dirigent_testing/environment.py +0 -0
- {dirigent_testing-0.18.0 → dirigent_testing-0.18.1}/src/dirigent_testing/messages.py +0 -0
- {dirigent_testing-0.18.0 → dirigent_testing-0.18.1}/src/dirigent_testing/py.typed +0 -0
- {dirigent_testing-0.18.0 → dirigent_testing-0.18.1}/src/dirigent_testing/running.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dirigent-testing
|
|
3
|
-
Version: 0.18.
|
|
3
|
+
Version: 0.18.1
|
|
4
4
|
Summary: Test doubles and pytest fixtures for writing and testing dirigent blocks.
|
|
5
5
|
License-Expression: LicenseRef-Proprietary
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
-
Requires-Dist: dirigent-common==0.18.
|
|
10
|
-
Requires-Dist: dirigent-plugin==0.18.
|
|
9
|
+
Requires-Dist: dirigent-common==0.18.1
|
|
10
|
+
Requires-Dist: dirigent-plugin==0.18.1
|
|
11
11
|
Requires-Dist: httpx2>=2.12.0
|
|
12
12
|
Requires-Dist: pydantic>=2.13.5
|
|
13
13
|
Requires-Dist: pytest>=9.1.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-testing"
|
|
3
|
-
version = "0.18.
|
|
3
|
+
version = "0.18.1"
|
|
4
4
|
description = "Test doubles and pytest fixtures for writing and testing dirigent blocks."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -11,8 +11,8 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.13",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"dirigent-common==0.18.
|
|
15
|
-
"dirigent-plugin==0.18.
|
|
14
|
+
"dirigent-common==0.18.1",
|
|
15
|
+
"dirigent-plugin==0.18.1",
|
|
16
16
|
"httpx2>=2.12.0",
|
|
17
17
|
"pydantic>=2.13.5",
|
|
18
18
|
"pytest>=9.1.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-testing"
|
|
3
|
-
version = "0.18.
|
|
3
|
+
version = "0.18.1"
|
|
4
4
|
description = "Test doubles and pytest fixtures for writing and testing dirigent blocks."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -11,8 +11,8 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.13",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"dirigent-common==0.18.
|
|
15
|
-
"dirigent-plugin==0.18.
|
|
14
|
+
"dirigent-common==0.18.1",
|
|
15
|
+
"dirigent-plugin==0.18.1",
|
|
16
16
|
"httpx2>=2.12.0",
|
|
17
17
|
"pydantic>=2.13.5",
|
|
18
18
|
"pytest>=9.1.1",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from dirigent_testing.conformance import assert_contribution_conforms, check_pack_examples
|
|
4
4
|
from dirigent_testing.doubles import FakeCapture, FakeContext, FakeRuns, FakeSink, FakeStorage, RecordingLogger
|
|
5
5
|
from dirigent_testing.environment import CONFIGURING_PREFIXES, TEST_WIDTH, pin_terminal, scrub_configuration
|
|
6
|
+
from dirigent_testing.fixtures import no_connection_outlives_its_loop
|
|
6
7
|
from dirigent_testing.running import call_block
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
@@ -17,6 +18,7 @@ __all__ = [
|
|
|
17
18
|
"assert_contribution_conforms",
|
|
18
19
|
"call_block",
|
|
19
20
|
"check_pack_examples",
|
|
21
|
+
"no_connection_outlives_its_loop",
|
|
20
22
|
"pin_terminal",
|
|
21
23
|
"scrub_configuration",
|
|
22
24
|
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""The pytest plugin: the fixtures a block author gets from installing this package.
|
|
2
|
+
|
|
3
|
+
Registered by entry point, so the names live in every consumer's suite. They are prefixed
|
|
4
|
+
to say what they are handed by, because a consumer's own fixtures got there first.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Final
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from dirigent_testing.doubles import FakeContext, FakeStorage
|
|
14
|
+
from dirigent_testing.environment import scrub_configuration
|
|
15
|
+
|
|
16
|
+
#: The server the lanes that need a real PostgreSQL start for themselves.
|
|
17
|
+
POSTGRES_IMAGE: Final = "postgres:17-alpine"
|
|
18
|
+
|
|
19
|
+
#: Every database connection a pool has opened and not closed again, by identity.
|
|
20
|
+
_open_connections: set[int] = set()
|
|
21
|
+
|
|
22
|
+
#: Whether the listeners that fill the set above have been registered yet.
|
|
23
|
+
_watching_pools = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _watch_pools() -> None:
|
|
27
|
+
"""Register the pool listeners the connection guard counts with, once per process.
|
|
28
|
+
|
|
29
|
+
SQLAlchemy is not a dependency of this package, so it is imported on first use: a pack
|
|
30
|
+
without it installs the plugin, and has no pool to watch.
|
|
31
|
+
"""
|
|
32
|
+
global _watching_pools
|
|
33
|
+
if _watching_pools:
|
|
34
|
+
return
|
|
35
|
+
_watching_pools = True
|
|
36
|
+
try:
|
|
37
|
+
from sqlalchemy import event
|
|
38
|
+
from sqlalchemy.pool import Pool
|
|
39
|
+
except ModuleNotFoundError: # pragma: no cover - a suite without SQLAlchemy opens no pooled connection
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
@event.listens_for(Pool, "connect")
|
|
43
|
+
def _connection_opened(dbapi_connection: Any, _record: Any) -> None:
|
|
44
|
+
"""Record a connection a pool has just opened."""
|
|
45
|
+
_open_connections.add(id(dbapi_connection))
|
|
46
|
+
|
|
47
|
+
@event.listens_for(Pool, "close")
|
|
48
|
+
def _connection_closed(dbapi_connection: Any, _record: Any) -> None:
|
|
49
|
+
"""Forget a connection a pool has closed."""
|
|
50
|
+
_open_connections.discard(id(dbapi_connection))
|
|
51
|
+
|
|
52
|
+
@event.listens_for(Pool, "close_detached")
|
|
53
|
+
def _detached_connection_closed(dbapi_connection: Any) -> None:
|
|
54
|
+
"""Forget a connection closed after it was detached from its pool."""
|
|
55
|
+
_open_connections.discard(id(dbapi_connection))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.fixture(scope="session")
|
|
59
|
+
def postgres_url() -> Iterator[str]:
|
|
60
|
+
"""Start a real PostgreSQL for the session, and hand back an asyncpg URL.
|
|
61
|
+
|
|
62
|
+
Session-scoped, so every lane that asks for it shares one container.
|
|
63
|
+
"""
|
|
64
|
+
postgres = pytest.importorskip("testcontainers.community.postgres")
|
|
65
|
+
with postgres.PostgresContainer(POSTGRES_IMAGE, driver="asyncpg") as container:
|
|
66
|
+
yield container.get_connection_url()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.fixture
|
|
70
|
+
def block_storage(tmp_path: Path) -> FakeStorage:
|
|
71
|
+
"""A storage facade over a throwaway directory."""
|
|
72
|
+
return FakeStorage(tmp_path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.fixture
|
|
76
|
+
def block_ctx(block_storage: FakeStorage, tmp_path: Path) -> FakeContext:
|
|
77
|
+
"""A context wired to that storage, with a scratch prefix inside it."""
|
|
78
|
+
return FakeContext(block_storage, "file://scratch", work=tmp_path / "work" / "runs" / "one")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.fixture
|
|
82
|
+
def local_block_ctx(tmp_path: Path) -> FakeContext:
|
|
83
|
+
"""A context whose scratch is a real file:// prefix, for a block that reads it as files."""
|
|
84
|
+
root = tmp_path / "artifacts"
|
|
85
|
+
root.mkdir()
|
|
86
|
+
return FakeContext(FakeStorage(root), f"file://{root}/runs/one", work=tmp_path / "work" / "runs" / "one")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.fixture
|
|
90
|
+
def defaults_only_environment(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
91
|
+
"""Run a test against defaults, whatever the shell exports.
|
|
92
|
+
|
|
93
|
+
Requested rather than autouse: a distributed plugin must not strip the environment of a
|
|
94
|
+
suite that never asked it to, so a consumer opts in -- once, from its own conftest.
|
|
95
|
+
"""
|
|
96
|
+
scrub_configuration(monkeypatch)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.fixture
|
|
100
|
+
def no_connection_outlives_its_loop() -> Iterator[None]:
|
|
101
|
+
"""Fail a test that ends with a database connection it opened still open.
|
|
102
|
+
|
|
103
|
+
aiosqlite drives every connection from a worker thread that answers the event loop the
|
|
104
|
+
connection was opened in. One still open when that loop closes is torn down later, by the
|
|
105
|
+
garbage collector, and the thread then calls into a loop that is gone: a RuntimeError
|
|
106
|
+
raised in a thread, which pytest reports against whichever test happens to be running at
|
|
107
|
+
that moment rather than against the one that left the connection behind.
|
|
108
|
+
|
|
109
|
+
Requested rather than autouse: a distributed plugin must not fail a run that never asked
|
|
110
|
+
it to, so a consumer opts in -- once, from its own conftest.
|
|
111
|
+
"""
|
|
112
|
+
_watch_pools()
|
|
113
|
+
held = set(_open_connections)
|
|
114
|
+
yield
|
|
115
|
+
leaked = _open_connections - held
|
|
116
|
+
assert not leaked, (
|
|
117
|
+
f"{len(leaked)} database connection(s) this test opened were never closed: "
|
|
118
|
+
"dispose every engine and close every session before the test ends"
|
|
119
|
+
)
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"""The pytest plugin: the fixtures a block author gets from installing this package.
|
|
2
|
-
|
|
3
|
-
Registered by entry point, so the names live in every consumer's suite. They are prefixed
|
|
4
|
-
to say what they are handed by, because a consumer's own fixtures got there first.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from collections.abc import Iterator
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Final
|
|
10
|
-
|
|
11
|
-
import pytest
|
|
12
|
-
|
|
13
|
-
from dirigent_testing.doubles import FakeContext, FakeStorage
|
|
14
|
-
from dirigent_testing.environment import scrub_configuration
|
|
15
|
-
|
|
16
|
-
#: The server the lanes that need a real PostgreSQL start for themselves.
|
|
17
|
-
POSTGRES_IMAGE: Final = "postgres:17-alpine"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@pytest.fixture(scope="session")
|
|
21
|
-
def postgres_url() -> Iterator[str]:
|
|
22
|
-
"""Start a real PostgreSQL for the session, and hand back an asyncpg URL.
|
|
23
|
-
|
|
24
|
-
Session-scoped, so every lane that asks for it shares one container.
|
|
25
|
-
"""
|
|
26
|
-
postgres = pytest.importorskip("testcontainers.community.postgres")
|
|
27
|
-
with postgres.PostgresContainer(POSTGRES_IMAGE, driver="asyncpg") as container:
|
|
28
|
-
yield container.get_connection_url()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@pytest.fixture
|
|
32
|
-
def block_storage(tmp_path: Path) -> FakeStorage:
|
|
33
|
-
"""A storage facade over a throwaway directory."""
|
|
34
|
-
return FakeStorage(tmp_path)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@pytest.fixture
|
|
38
|
-
def block_ctx(block_storage: FakeStorage, tmp_path: Path) -> FakeContext:
|
|
39
|
-
"""A context wired to that storage, with a scratch prefix inside it."""
|
|
40
|
-
return FakeContext(block_storage, "file://scratch", work=tmp_path / "work" / "runs" / "one")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@pytest.fixture
|
|
44
|
-
def local_block_ctx(tmp_path: Path) -> FakeContext:
|
|
45
|
-
"""A context whose scratch is a real file:// prefix, for a block that reads it as files."""
|
|
46
|
-
root = tmp_path / "artifacts"
|
|
47
|
-
root.mkdir()
|
|
48
|
-
return FakeContext(FakeStorage(root), f"file://{root}/runs/one", work=tmp_path / "work" / "runs" / "one")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@pytest.fixture
|
|
52
|
-
def defaults_only_environment(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
53
|
-
"""Run a test against defaults, whatever the shell exports.
|
|
54
|
-
|
|
55
|
-
Requested rather than autouse: a distributed plugin must not strip the environment of a
|
|
56
|
-
suite that never asked it to, so a consumer opts in -- once, from its own conftest.
|
|
57
|
-
"""
|
|
58
|
-
scrub_configuration(monkeypatch)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|