Skip to content

Commit 145f2d8

Browse files
authored
Merge pull request #159 from kraken-tech/meshy/part_of_a_transaction-fail-outside-tests
Ensure `part_of_a_transaction` only runs in tests
2 parents 74dcf4c + 13e8350 commit 145f2d8

3 files changed

Lines changed: 36 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1515

1616
### Added
1717
- Added MariaDB and SQLite to the test matrix.
18+
- `part_of_a_transaction` now raises an error if it is called outside of tests.
19+
This prevents code which misleadingly runs after-commit callbacks.
20+
For the same reason, it will also fail when called within transaction test cases.
1821
- `part_of_a_transaction` now raises an error if unhandled callbacks are detected when it starts.
1922
This makes it more similar to `transaction`.
2023
The error can be silenced by setting the `SUBATOMIC_CATCH_UNHANDLED_AFTER_COMMIT_CALLBACKS_IN_TESTS` setting to `False`

src/django_subatomic/test.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ class _UnhandledCallbacks(Exception):
3030
callbacks: tuple[Callable[[], object], ...]
3131

3232

33+
class _OnlyForUseInDjangoTestTransaction(Exception):
34+
"""
35+
Raised when `part_of_a_transaction` is used without a transaction created by Django tests.
36+
37+
This can also be raised in tests which handle their own transaction
38+
instead of allowing the testsuite to wrap the test in a transaction.
39+
(These kinds of tests are called "transaction testcases".)
40+
This prevents `part_of_a_transaction` from running after-commit callbacks.
41+
"""
42+
43+
3344
@contextlib.contextmanager
3445
def part_of_a_transaction(using: str | None = None) -> Generator[None]:
3546
"""
@@ -41,18 +52,25 @@ def part_of_a_transaction(using: str | None = None) -> Generator[None]:
4152
This works by entering a new "atomic" block, so that the inner-most "atomic"
4253
isn't the one created by the test-suite.
4354
44-
In "transaction testcases" this will create a transaction, but if you're writing
45-
a transaction testcase, you probably want to manage transactions more explicitly
46-
than by calling this.
47-
4855
Note that this does not handle after-commit callback simulation. If you need that,
4956
use [`transaction`][django_subatomic.db.transaction] instead.
57+
58+
In production code and "transaction testcases" this will raise an error
59+
to ensure we don't misleadingly run after-commit callbacks.
5060
"""
5161
connection = transaction.get_connection(using)
62+
5263
raise_unhandled_callbacks = getattr(
5364
settings, "SUBATOMIC_CATCH_UNHANDLED_AFTER_COMMIT_CALLBACKS_IN_TESTS", True
5465
)
5566

67+
# We must be called from inside an atomic block created by the test suite
68+
# to avoid running after-commit callbacks on exit.
69+
# We don't check that the atomic block is from the test suite though,
70+
# because if it's created elsewhere we'll see an error from `durable=True` below.
71+
if len(connection.atomic_blocks) == 0:
72+
raise _OnlyForUseInDjangoTestTransaction
73+
5674
if raise_unhandled_callbacks:
5775
callbacks = connection.run_on_commit
5876
if callbacks:

tests/test_test.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,17 @@ def test_fails_when_nested_inside_an_atomic_block(
131131
with test.part_of_a_transaction():
132132
...
133133

134+
@pytest.mark.django_db(transaction=True)
135+
def test_fails_when_test_suite_not_managing_transactions(self) -> None:
136+
"""
137+
`part_of_a_transaction` cannot be used if the test suite isn't managing transactions.
138+
"""
139+
with (
140+
pytest.raises(test._OnlyForUseInDjangoTestTransaction), # noqa: SLF001
141+
test.part_of_a_transaction(),
142+
):
143+
...
144+
134145

135146
def _callback_which_should_not_be_called() -> None:
136147
pytest.fail("Callback should not have been called.") # pragma: no cover

0 commit comments

Comments
 (0)