Skip to content

Commit a971f55

Browse files
committed
[#37] Explicitly handle transactions in the runner
1 parent 388dc94 commit a971f55

File tree

5 files changed

+82
-5
lines changed

5 files changed

+82
-5
lines changed

Diff for: django_setup_configuration/management/commands/setup_configuration.py

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from pathlib import Path
22

33
from django.core.management import BaseCommand, CommandError
4-
from django.db import transaction
54

65
from django_setup_configuration.exceptions import ValidateRequirementsFailure
76
from django_setup_configuration.runner import SetupConfigurationRunner
@@ -33,7 +32,6 @@ def add_arguments(self, parser):
3332
help="Path to YAML file containing the configurations",
3433
)
3534

36-
@transaction.atomic
3735
def handle(self, **options):
3836
yaml_file = Path(options["yaml_file"]).resolve()
3937
if not yaml_file.exists():

Diff for: django_setup_configuration/runner.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Any, Generator
77

88
from django.conf import settings
9+
from django.db import transaction
910
from django.utils.module_loading import import_string
1011

1112
from pydantic import ValidationError
@@ -157,7 +158,8 @@ def _execute_step(
157158
step_exc = None
158159

159160
try:
160-
step.execute(config_model)
161+
with transaction.atomic():
162+
step.execute(config_model)
161163
except BaseException as exc:
162164
step_exc = exc
163165
finally:
@@ -207,8 +209,25 @@ def execute_all_iter(self) -> Generator[StepExecutionResult, Any, None]:
207209
Generator[StepExecutionResult, Any, None]: The results of each step's
208210
execution.
209211
"""
210-
for step in self.enabled_steps:
211-
yield self._execute_step(step)
212+
213+
# Not the most elegant approach to rollbacks, but it's preferable to the
214+
# pitfalls of manual transaction management. We want all steps to run and only
215+
# rollback at the end, hence intra-step exceptions are caught and persisted.
216+
class Rollback(BaseException):
217+
pass
218+
219+
try:
220+
with transaction.atomic():
221+
results = []
222+
for step in self.enabled_steps:
223+
result = self._execute_step(step)
224+
results.append(result)
225+
yield result
226+
227+
if any(result.run_exception for result in results):
228+
raise Rollback # Trigger the rollback
229+
except Rollback:
230+
pass
212231

213232
def execute_all(self) -> list[StepExecutionResult]:
214233
"""

Diff for: tests/test_runner.py

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from django_setup_configuration.test_utils import execute_single_step
1313
from tests.conftest import TestStep, TestStepConfig
1414

15+
pytestmark = pytest.mark.django_db
16+
1517

1618
def test_runner_raises_on_non_existent_step_module_path(test_step_yaml_path):
1719
with pytest.raises(ConfigurationException):

Diff for: tests/test_test_utils.py

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from django_setup_configuration.test_utils import execute_single_step
55
from tests.conftest import TestStep
66

7+
pytestmark = pytest.mark.django_db
8+
79

810
def test_exception_during_execute_step_is_immediately_raised(
911
step_execute_mock,

Diff for: tests/test_transactions.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from django.contrib.auth.models import User
2+
3+
import pytest
4+
5+
from testapp.configuration import UserConfigurationStep
6+
from tests.conftest import TestStep
7+
8+
pytestmark = pytest.mark.django_db
9+
10+
11+
@pytest.fixture()
12+
def yaml_file_with_valid_configuration(yaml_file_factory, test_step_valid_config):
13+
yaml_path = yaml_file_factory(
14+
{
15+
"user_configuration_enabled": True,
16+
"user_configuration": {
17+
"username": "demo",
18+
"password": "secret",
19+
},
20+
"some_extra_attrs": "should be allowed",
21+
}
22+
| test_step_valid_config
23+
)
24+
25+
return yaml_path
26+
27+
28+
def test_runner_rolls_back_all_on_failing_step(
29+
runner_factory, yaml_file_with_valid_configuration, step_execute_mock
30+
):
31+
runner = runner_factory(
32+
steps=[UserConfigurationStep, TestStep],
33+
yaml_source=yaml_file_with_valid_configuration,
34+
)
35+
exc = Exception()
36+
step_execute_mock.side_effect = exc
37+
38+
user_configuration_step_result, test_step_result = runner.execute_all()
39+
40+
assert test_step_result.has_run
41+
assert test_step_result.run_exception is exc
42+
43+
assert user_configuration_step_result.has_run
44+
assert user_configuration_step_result.run_exception is None
45+
assert User.objects.count() == 0
46+
47+
step_execute_mock.side_effect = None
48+
49+
user_configuration_step_result, test_step_result = runner.execute_all()
50+
51+
assert test_step_result.has_run
52+
assert test_step_result.run_exception is None
53+
54+
assert user_configuration_step_result.has_run
55+
assert user_configuration_step_result.run_exception is None
56+
assert User.objects.count() == 1

0 commit comments

Comments
 (0)