Skip to content

Commit ef3d063

Browse files
mpasternakclaude
andcommitted
Fix two self-review bugs in the baseline + template-default flow
B1: _resolve_baseline_path mutated the caller's init_scripts list via .insert(0, ...), so re-invocation would keep prepending the baseline path. Now builds a new PostgresService via dataclasses.replace(). B2: apply_template_default ran BEFORE _resolve_baseline_path, so when a user enabled use_django_pg_baseline=true with no other init_scripts, the template default (SPEC §10.6) saw an empty list and skipped — and DJANGO_DB_TEST_TEMPLATE was never injected, defeating the point of loading a baseline. Reorder: baseline-resolve first, then template default. Adds three regression tests via pytester: * baseline-only triggers template default (B2) * register()'d shared PostgresService isn't mutated by the hook (B1) * missing django-pg-baseline raises a clear error (N4) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e7abd5a commit ef3d063

2 files changed

Lines changed: 115 additions & 4 deletions

File tree

src/pytest_testcontainers_django/plugin.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,12 @@ def _is_xdist_worker() -> bool:
6868

6969

7070
def _resolve_baseline_path(config: DjangoContainerConfig) -> DjangoContainerConfig:
71-
"""SPEC §10.3 Path B — auto-prepend django-pg-baseline's path when flagged."""
71+
"""SPEC §10.3 Path B — auto-prepend django-pg-baseline's path when flagged.
72+
73+
Returns a new config; never mutates the caller's lists, so re-invocation
74+
in the same process (pytester scenarios) doesn't keep prepending the
75+
baseline path.
76+
"""
7277
if not config.use_django_pg_baseline:
7378
return config
7479
try:
@@ -79,9 +84,14 @@ def _resolve_baseline_path(config: DjangoContainerConfig) -> DjangoContainerConf
7984
"installed. Install it (e.g. `pip install pytest-testcontainers-django"
8085
"[baseline]`) or set the flag to false."
8186
) from exc
87+
from dataclasses import replace as _replace
88+
8289
baseline = Path(get_baseline_path())
83-
config.postgres.init_scripts.insert(0, baseline)
84-
return config
90+
new_pg = _replace(
91+
config.postgres,
92+
init_scripts=[baseline, *config.postgres.init_scripts],
93+
)
94+
return _replace(config, postgres=new_pg)
8595

8696

8797
def _preload_rootdir_conftest(early_config: pytest.Config) -> None:
@@ -130,7 +140,6 @@ def pytest_load_initial_conftests(
130140

131141
rootdir = Path(getattr(early_config, "rootpath", Path.cwd()))
132142
config = _config.load_config(rootdir)
133-
config = _config.apply_template_default(config)
134143

135144
if _config.is_disabled(config, args):
136145
return
@@ -142,7 +151,13 @@ def pytest_load_initial_conftests(
142151
_env_snapshot = inject_worker(config)
143152
return
144153

154+
# Order matters: baseline resolution may add init scripts; the template
155+
# default (SPEC §10.6) reads init_scripts to decide whether to default
156+
# ``postgres_template = postgres_database``. Resolving baseline first
157+
# ensures use_django_pg_baseline=true triggers the template default
158+
# even when the user didn't list any other postgres_init_scripts.
145159
config = _resolve_baseline_path(config)
160+
config = _config.apply_template_default(config)
146161
_config.validate(config)
147162

148163
_reuse_active = _config.is_reuse_enabled(config)

tests/test_plugin.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,102 @@ def test_template_env_set():
232232
result.assert_outcomes(passed=1)
233233

234234

235+
def test_use_django_pg_baseline_triggers_template_default(
236+
pytester: pytest.Pytester,
237+
) -> None:
238+
"""B2 regression: when only the auto-prepended baseline is in init_scripts,
239+
apply_template_default still has to see it to default the template.
240+
"""
241+
sql = pytester.path / "baseline.sql"
242+
sql.write_text("-- baseline", encoding="utf-8")
243+
_bootstrap(
244+
pytester,
245+
pyproject_extra="""
246+
postgres_database = "myapp"
247+
use_django_pg_baseline = true
248+
""",
249+
conftest_extra=f"""
250+
import pathlib, sys, types
251+
fake = types.ModuleType("django_pg_baseline")
252+
fake.get_baseline_path = lambda: pathlib.Path({str(sql)!r})
253+
sys.modules["django_pg_baseline"] = fake
254+
""",
255+
)
256+
pytester.makepyfile(
257+
"""
258+
import os
259+
260+
def test_template_defaulted_from_baseline_only():
261+
assert os.environ["DJANGO_DB_TEST_TEMPLATE"] == "myapp"
262+
"""
263+
)
264+
result = pytester.runpytest("-p", "no:cacheprovider")
265+
result.assert_outcomes(passed=1)
266+
267+
268+
def test_use_django_pg_baseline_missing_dep_raises(pytester: pytest.Pytester) -> None:
269+
"""N4 regression: clear error when flag is set but dep isn't installed."""
270+
_bootstrap(
271+
pytester,
272+
pyproject_extra="""
273+
use_django_pg_baseline = true
274+
""",
275+
conftest_extra="""
276+
import sys
277+
# Ensure the import fails inside the hook even if the package is installed
278+
# at the outer level.
279+
sys.modules["django_pg_baseline"] = None
280+
""",
281+
)
282+
pytester.makepyfile("def test_dummy(): pass")
283+
result = pytester.runpytest("-p", "no:cacheprovider")
284+
result.stderr.fnmatch_lines(["*django-pg-baseline*"])
285+
assert result.ret != 0
286+
287+
288+
def test_baseline_path_resolution_does_not_mutate_register_config(
289+
pytester: pytest.Pytester,
290+
) -> None:
291+
"""B1 regression: re-invoking the hook (or sharing one PostgresService
292+
instance across processes via register()) must not keep prepending
293+
the baseline path to init_scripts.
294+
"""
295+
sql = pytester.path / "baseline.sql"
296+
sql.write_text("-- baseline", encoding="utf-8")
297+
_bootstrap(
298+
pytester,
299+
conftest_extra=f"""
300+
import pathlib, sys, types
301+
fake = types.ModuleType("django_pg_baseline")
302+
fake.get_baseline_path = lambda: pathlib.Path({str(sql)!r})
303+
sys.modules["django_pg_baseline"] = fake
304+
305+
from pytest_testcontainers_django import (
306+
DjangoContainerConfig, PostgresService, register,
307+
)
308+
309+
# A user might construct one shared config and register it; the hook must
310+
# not mutate it.
311+
SHARED_PG = PostgresService(database="myapp")
312+
register(DjangoContainerConfig(postgres=SHARED_PG, use_django_pg_baseline=True))
313+
""",
314+
)
315+
pytester.makepyfile(
316+
"""
317+
def test_init_scripts_not_doubled():
318+
from pytest_testcontainers_django import plugin as p
319+
from pytest_testcontainers_django import config as c
320+
# The user's PostgresService.init_scripts must still be empty —
321+
# the hook should have built a NEW config, not pushed onto theirs.
322+
registered = c._REGISTERED
323+
assert registered is not None
324+
assert registered.postgres.init_scripts == [], registered.postgres.init_scripts
325+
"""
326+
)
327+
result = pytester.runpytest("-p", "no:cacheprovider")
328+
result.assert_outcomes(passed=1)
329+
330+
235331
def test_invalid_init_script_path_fails_loudly(pytester: pytest.Pytester) -> None:
236332
_bootstrap(
237333
pytester,

0 commit comments

Comments
 (0)