Skip to content

Commit a3937c1

Browse files
committed
feat: Add cleanup_database option to postgres fixture.
Note, this could **potentially** reveal preexisting issues in code under test, that could be perceived as a "breaking" change. By deleting the database under test at the end of the test's execution, any database connections left connected to the database might cause the `DELETE DATABASE` command to fail. PMR will **try** to use the `WITH FORCE` option on database versions >= 13.0, but that option does not exist on prior versions of postgres. In any case, if this happens, it **is** ultimately revealing a "bug" in the code it is testing. Additionally, you can simply turn off database cleanup in one of various ways.
1 parent 4a92dc2 commit a3937c1

File tree

7 files changed

+165
-13
lines changed

7 files changed

+165
-13
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pytest-mock-resources"
3-
version = "2.12.1"
3+
version = "2.13.0"
44
description = "A pytest plugin for easily instantiating reproducible mock resources."
55
authors = [
66
"Omar Khan <oakhan3@gmail.com>",

src/pytest_mock_resources/fixture/postgresql.py

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
PostgresConfig,
1818
)
1919
from pytest_mock_resources.fixture.base import asyncio_fixture, generate_fixture_id, Scope
20+
from pytest_mock_resources.hooks import should_cleanup
2021
from pytest_mock_resources.sqlalchemy import (
2122
bifurcate_actions,
2223
EngineManager,
@@ -114,6 +115,7 @@ def create_postgres_fixture(
114115
engine_kwargs=None,
115116
template_database=True,
116117
actions_share_transaction=None,
118+
cleanup_database: Optional[bool] = None,
117119
):
118120
"""Produce a Postgres fixture.
119121
@@ -141,6 +143,10 @@ def create_postgres_fixture(
141143
fixtures for backwards compatibility; and disabled by default for
142144
asynchronous fixtures (the way v2-style/async features work in SQLAlchemy can lead
143145
to bad default behavior).
146+
cleanup_database: Whether to clean up the database after the test completes. Defaults to `None`,
147+
which defers the decision to the pmr_cleanup/--pmr-cleanup pytest options (which default to True).
148+
Note this does not currently clean up any "template" databases produced in service
149+
of the fixture.
144150
"""
145151
fixture_id = generate_fixture_id(enabled=template_database, name="pg")
146152

@@ -155,13 +161,23 @@ def create_postgres_fixture(
155161
}
156162

157163
@pytest.fixture(scope=scope)
158-
def _sync(*_, pmr_postgres_container, pmr_postgres_config):
159-
fixture = _sync_fixture(pmr_postgres_config, engine_manager_kwargs, engine_kwargs_)
164+
def _sync(*_, pmr_postgres_container, pmr_postgres_config, pytestconfig):
165+
fixture = _sync_fixture(
166+
pmr_postgres_config,
167+
engine_manager_kwargs,
168+
engine_kwargs_,
169+
cleanup_database=should_cleanup(pytestconfig, cleanup_database),
170+
)
160171
for _, conn in fixture:
161172
yield conn
162173

163-
async def _async(*_, pmr_postgres_container, pmr_postgres_config):
164-
fixture = _async_fixture(pmr_postgres_config, engine_manager_kwargs, engine_kwargs_)
174+
async def _async(*_, pmr_postgres_container, pmr_postgres_config, pytestconfig):
175+
fixture = _async_fixture(
176+
pmr_postgres_config,
177+
engine_manager_kwargs,
178+
engine_kwargs_,
179+
cleanup_database=should_cleanup(pytestconfig, cleanup_database),
180+
)
165181
async for _, conn in fixture:
166182
yield conn
167183

@@ -170,7 +186,14 @@ async def _async(*_, pmr_postgres_container, pmr_postgres_config):
170186
return _sync
171187

172188

173-
def _sync_fixture(pmr_config, engine_manager_kwargs, engine_kwargs, *, fixture="postgres"):
189+
def _sync_fixture(
190+
pmr_config,
191+
engine_manager_kwargs,
192+
engine_kwargs,
193+
*,
194+
fixture="postgres",
195+
cleanup_database: bool = True,
196+
):
174197
root_engine = cast(Engine, get_sqlalchemy_engine(pmr_config, pmr_config.root_database))
175198
conn = retry(root_engine.connect, retries=DEFAULT_RETRIES)
176199
conn.close()
@@ -216,10 +239,27 @@ def _sync_fixture(pmr_config, engine_manager_kwargs, engine_kwargs, *, fixture="
216239
engine = get_sqlalchemy_engine(pmr_config, database_name, **engine_kwargs)
217240
yield from engine_manager.manage_sync(engine)
218241

242+
if cleanup_database:
243+
with root_engine.connect() as root_conn:
244+
with root_conn.begin() as trans:
245+
_drop_database(root_conn, database_name)
246+
trans.commit()
247+
root_engine.dispose()
248+
249+
250+
async def _async_fixture(
251+
pmr_config,
252+
engine_manager_kwargs,
253+
engine_kwargs,
254+
*,
255+
fixture="postgres",
256+
cleanup_database: bool = True,
257+
):
258+
from sqlalchemy.ext.asyncio import AsyncEngine
219259

220-
async def _async_fixture(pmr_config, engine_manager_kwargs, engine_kwargs, *, fixture="postgres"):
221-
root_engine = get_sqlalchemy_engine(
222-
pmr_config, pmr_config.root_database, async_=True, autocommit=True
260+
root_engine = cast(
261+
AsyncEngine,
262+
get_sqlalchemy_engine(pmr_config, pmr_config.root_database, async_=True, autocommit=True),
223263
)
224264

225265
root_conn = await async_retry(root_engine.connect, retries=DEFAULT_RETRIES)
@@ -239,7 +279,10 @@ async def _async_fixture(pmr_config, engine_manager_kwargs, engine_kwargs, *, fi
239279
if template_manager:
240280
assert template_database
241281

242-
engine = get_sqlalchemy_engine(pmr_config, template_database, **engine_kwargs, async_=True)
282+
engine = cast(
283+
AsyncEngine,
284+
get_sqlalchemy_engine(pmr_config, template_database, **engine_kwargs, async_=True),
285+
)
243286
async with engine.begin() as conn:
244287
await conn.run_sync(template_manager.run_static_actions)
245288
await conn.commit()
@@ -260,6 +303,13 @@ async def _async_fixture(pmr_config, engine_manager_kwargs, engine_kwargs, *, fi
260303
async for engine, conn in engine_manager.manage_async(engine):
261304
yield engine, conn
262305

306+
if cleanup_database:
307+
async with root_engine.connect() as root_conn:
308+
async with root_conn.begin() as trans:
309+
await root_conn.run_sync(_drop_database, database_name)
310+
await trans.commit()
311+
await root_engine.dispose()
312+
263313

264314
def create_engine_manager(
265315
root_connection,
@@ -356,5 +406,15 @@ def _generate_database_name(conn):
356406
return f"pytest_mock_resource_db_{id_}"
357407

358408

409+
def _drop_database(root_conn: Connection, database_name: str):
410+
with_force = ""
411+
412+
assert root_conn.dialect.server_version_info
413+
if root_conn.dialect.server_version_info >= (13, 0):
414+
with_force = " WITH FORCE"
415+
416+
root_conn.execute(text(f"DROP DATABASE {database_name}{with_force}"))
417+
418+
359419
pmr_postgres_config = create_postgres_config_fixture()
360420
pmr_postgres_container = create_postgres_container_fixture()

src/pytest_mock_resources/hooks.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import warnings
3+
from typing import Optional
34

45
_resource_kinds = ["postgres", "redshift", "mongo", "redis", "mysql", "moto"]
56

@@ -17,6 +18,12 @@ def pytest_addoption(parser):
1718
type="bool",
1819
default=True,
1920
)
21+
parser.addini(
22+
"pmr_cleanup",
23+
"Optionally cleanup created fixture resources.",
24+
type="bool",
25+
default=True,
26+
)
2027
parser.addini(
2128
"pmr_docker_client",
2229
"Optional docker client name to use: docker, podman, nerdctl",
@@ -28,17 +35,38 @@ def pytest_addoption(parser):
2835
group.addoption(
2936
"--pmr-multiprocess-safe",
3037
action="store_true",
31-
default=False,
38+
default=None,
3239
help="Enable multiprocess-safe mode",
3340
dest="pmr_multiprocess_safe",
3441
)
3542
group.addoption(
3643
"--pmr-cleanup-container",
3744
action="store_true",
38-
default=True,
45+
default=None,
46+
help="Optionally disable attempts to cleanup created containers",
47+
dest="pmr_cleanup_container",
48+
)
49+
group.addoption(
50+
"--no-pmr-cleanup-container",
51+
action="store_false",
52+
default=None,
3953
help="Optionally disable attempts to cleanup created containers",
4054
dest="pmr_cleanup_container",
4155
)
56+
group.addoption(
57+
"--pmr-cleanup",
58+
action="store_true",
59+
default=None,
60+
help="Optionally cleanup created fixture resources.",
61+
dest="pmr_cleanup",
62+
)
63+
group.addoption(
64+
"--no-pmr-cleanup",
65+
action="store_false",
66+
default=None,
67+
help="Optionally cleanup created fixture resources.",
68+
dest="pmr_cleanup",
69+
)
4270
group.addoption(
4371
"--pmr-docker-client",
4472
default=None,
@@ -49,7 +77,7 @@ def pytest_addoption(parser):
4977

5078
def get_pytest_flag(config, name, *, default=None):
5179
value = getattr(config.option, name, default)
52-
if value:
80+
if value is not None:
5381
return value
5482

5583
return config.getini(name)
@@ -59,6 +87,12 @@ def use_multiprocess_safe_mode(config):
5987
return bool(get_pytest_flag(config, "pmr_multiprocess_safe"))
6088

6189

90+
def should_cleanup(config, value: Optional[bool] = None) -> bool:
91+
if value is None:
92+
return bool(get_pytest_flag(config, "pmr_cleanup"))
93+
return value
94+
95+
6296
def get_docker_client_name(config) -> str:
6397
pmr_docker_client = os.getenv("PMR_DOCKER_CLIENT")
6498
if pmr_docker_client:
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from sqlalchemy import Column, String, text
2+
3+
from pytest_mock_resources import create_postgres_fixture, Rows
4+
from pytest_mock_resources.compat.sqlalchemy import declarative_base
5+
6+
Base = declarative_base()
7+
8+
9+
class User(Base):
10+
__tablename__ = "user"
11+
12+
name = Column(String, primary_key=True)
13+
14+
15+
rows = Rows(User(name="Harold"), User(name="Gump"))
16+
17+
18+
pg_no_clean = create_postgres_fixture(Base, rows, session=True, cleanup_database=False)
19+
20+
21+
non_cleaned_database_name = None
22+
23+
24+
def test_not_to_be_cleaned_up(pg_no_clean):
25+
global non_cleaned_database_name
26+
non_cleaned_database_name = pg_no_clean.pmr_credentials.database
27+
28+
names = [u.name for u in pg_no_clean.query(User).all()]
29+
assert names == ["Harold", "Gump"]
30+
31+
names = pg_no_clean.execute(text("SELECT datname FROM pg_database")).all()
32+
unique_names = {name for (name,) in names}
33+
assert non_cleaned_database_name in unique_names
34+
35+
36+
def test_database_is_not_cleaned_up(pg_no_clean):
37+
global non_cleaned_database_name
38+
39+
assert non_cleaned_database_name is not None
40+
41+
assert non_cleaned_database_name != pg_no_clean.pmr_credentials.database
42+
43+
names = pg_no_clean.execute(text("SELECT datname FROM pg_database")).all()
44+
unique_names = {name for (name,) in names}
45+
assert non_cleaned_database_name in unique_names

tests/fixture/test_database.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def test_create_custom_connection(postgres_3):
9191

9292
with engine.connect() as conn:
9393
conn.execute(text("select 1"))
94+
engine.dispose()
9495

9596

9697
def test_create_custom_connection_from_dict(postgres_3):
@@ -103,13 +104,15 @@ def test_create_custom_connection_from_dict(postgres_3):
103104

104105
with engine.connect() as conn:
105106
conn.execute(text("select 1"))
107+
engine.dispose()
106108

107109

108110
def test_create_custom_connection_url(postgres_3):
109111
url = compat.sqlalchemy.URL(**postgres_3.pmr_credentials.as_sqlalchemy_url_kwargs())
110112
engine = create_engine(url, isolation_level="AUTOCOMMIT")
111113
with engine.connect() as conn:
112114
conn.execute(text("select 1"))
115+
engine.dispose()
113116

114117

115118
def test_bad_actions(postgres):

tests/fixture/test_pmr_credentials.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,4 @@ def verify_relational(connection, credentials, session=False):
119119
manual_engine = create_engine(credentials.as_url())
120120
with manual_engine.connect() as conn:
121121
conn.execute(text("select * from foo"))
122+
manual_engine.dispose()

tests/test_examples.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,12 @@ def test_multiprocess_container_cleanup_race_condition(pytester):
1919
args = ["-vv", "-n", "2", "--pmr-multiprocess-safe", "test_split.py"]
2020
result = pytester.inline_run(*args)
2121
result.assertoutcome(passed=2, skipped=0, failed=0)
22+
23+
24+
@pytest.mark.postgres
25+
def test_postgres_no_cleanup(pytester):
26+
pytester.copy_example()
27+
28+
args = ["-vv", "--pmr-multiprocess-safe", "test_cleanup.py"]
29+
result = pytester.inline_run(*args)
30+
result.assertoutcome(passed=2, skipped=0, failed=0)

0 commit comments

Comments
 (0)