|
8 | 8 |
|
9 | 9 | import importlib |
10 | 10 | import importlib.util |
| 11 | +import subprocess |
| 12 | +import sys |
| 13 | +import textwrap |
| 14 | +from collections.abc import Callable |
11 | 15 | from pathlib import Path |
12 | 16 |
|
13 | 17 | import pytest |
14 | 18 |
|
15 | 19 | from boost_weblate.formats import registry |
16 | 20 |
|
| 21 | +_ENDPOINT_APP_CONFIG = "boost_weblate.endpoint.apps.BoostEndpointConfig" |
| 22 | +_REPO_ROOT = Path(__file__).resolve().parents[1] |
| 23 | +_SETTINGS_OVERRIDE_PATH = _REPO_ROOT / "src/boost_weblate/settings_override.py" |
| 24 | + |
| 25 | + |
| 26 | +def _exec_settings_override(namespace: dict) -> None: |
| 27 | + exec( |
| 28 | + compile( |
| 29 | + _SETTINGS_OVERRIDE_PATH.read_text(encoding="utf-8"), |
| 30 | + str(_SETTINGS_OVERRIDE_PATH), |
| 31 | + "exec", |
| 32 | + ), |
| 33 | + namespace, |
| 34 | + ) |
| 35 | + |
17 | 36 |
|
18 | 37 | def _plugin_weblate_paths() -> tuple[str, ...]: |
19 | 38 | return registry.weblate_class_paths() |
@@ -143,3 +162,120 @@ def test_boost_task_timeout_settings_rejects_invalid_limits( |
143 | 162 | monkeypatch.setenv("BOOST_TASK_TIME_LIMIT", "900") |
144 | 163 | with pytest.raises(ValueError, match="BOOST_TASK_TIME_LIMIT"): |
145 | 164 | boost_task_timeout_settings() |
| 165 | + |
| 166 | + |
| 167 | +@pytest.mark.parametrize( |
| 168 | + "factory", |
| 169 | + [ |
| 170 | + lambda: ["django.contrib.auth"], |
| 171 | + lambda: ("django.contrib.auth",), |
| 172 | + ], |
| 173 | + ids=["list", "tuple"], |
| 174 | +) |
| 175 | +def test_double_exec_does_not_duplicate_installed_apps( |
| 176 | + factory: Callable[[], list[str] | tuple[str, ...]], |
| 177 | +) -> None: |
| 178 | + base_apps = factory() |
| 179 | + ns: dict[str, object] = {"INSTALLED_APPS": base_apps} |
| 180 | + _exec_settings_override(ns) |
| 181 | + _exec_settings_override(ns) |
| 182 | + apps = ns["INSTALLED_APPS"] |
| 183 | + assert apps.count(_ENDPOINT_APP_CONFIG) == 1 |
| 184 | + assert apps[0] == "django.contrib.auth" |
| 185 | + if isinstance(base_apps, tuple): |
| 186 | + assert isinstance(apps, tuple) |
| 187 | + |
| 188 | + |
| 189 | +def test_double_exec_does_not_double_ready_hooks() -> None: |
| 190 | + script = textwrap.dedent( |
| 191 | + f""" |
| 192 | + import os |
| 193 | + import sys |
| 194 | + import tempfile |
| 195 | + import types |
| 196 | + from pathlib import Path |
| 197 | +
|
| 198 | + repo = Path({str(_REPO_ROOT)!r}) |
| 199 | + sys.path.insert(0, str(repo)) |
| 200 | + sys.path.insert(0, str(repo / "src")) |
| 201 | +
|
| 202 | + import weblate.settings_example as _wl_example |
| 203 | +
|
| 204 | + ns: dict[str, object] = {{}} |
| 205 | + for _key, _value in _wl_example.__dict__.items(): |
| 206 | + if _key.isupper(): |
| 207 | + ns[_key] = _value |
| 208 | +
|
| 209 | + ns["INSTALLED_APPS"] = tuple( |
| 210 | + app |
| 211 | + for app in _wl_example.INSTALLED_APPS |
| 212 | + if app != "django.contrib.postgres" |
| 213 | + ) |
| 214 | +
|
| 215 | + _data = tempfile.mkdtemp(prefix="double_exec_settings_") |
| 216 | + ns["DATA_DIR"] = _data |
| 217 | + ns["CACHE_DIR"] = os.path.join(_data, "cache") |
| 218 | + ns["MEDIA_ROOT"] = os.path.join(_data, "media") |
| 219 | + ns["STATIC_ROOT"] = os.path.join(_data, "static") |
| 220 | + for _p in (ns["CACHE_DIR"], ns["MEDIA_ROOT"], ns["STATIC_ROOT"]): |
| 221 | + os.makedirs(_p, exist_ok=True) |
| 222 | +
|
| 223 | + ns["DATABASES"] = {{ |
| 224 | + "default": {{ |
| 225 | + "ENGINE": "django.db.backends.sqlite3", |
| 226 | + "NAME": os.path.join(_data, "test.sqlite3"), |
| 227 | + }} |
| 228 | + }} |
| 229 | + ns["SITE_DOMAIN"] = "test.invalid" |
| 230 | + ns["DEBUG"] = False |
| 231 | + ns["CELERY_TASK_ALWAYS_EAGER"] = True |
| 232 | + ns["CELERY_BROKER_URL"] = "memory://" |
| 233 | + ns["CELERY_TASK_EAGER_PROPAGATES"] = True |
| 234 | + ns["CELERY_RESULT_BACKEND"] = None |
| 235 | + ns["CACHES"] = {{ |
| 236 | + "default": {{"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} |
| 237 | + }} |
| 238 | + ns["PASSWORD_HASHERS"] = ["django.contrib.auth.hashers.MD5PasswordHasher"] |
| 239 | +
|
| 240 | + override_path = Path({str(_SETTINGS_OVERRIDE_PATH)!r}) |
| 241 | + override_code = compile( |
| 242 | + override_path.read_text(encoding="utf-8"), |
| 243 | + str(override_path), |
| 244 | + "exec", |
| 245 | + ) |
| 246 | + exec(override_code, ns) |
| 247 | + exec(override_code, ns) |
| 248 | +
|
| 249 | + settings_mod = types.ModuleType("tests._double_exec_settings") |
| 250 | + for _key, _value in ns.items(): |
| 251 | + if _key.isupper(): |
| 252 | + setattr(settings_mod, _key, _value) |
| 253 | + sys.modules["tests._double_exec_settings"] = settings_mod |
| 254 | + os.environ["DJANGO_SETTINGS_MODULE"] = "tests._double_exec_settings" |
| 255 | +
|
| 256 | + from boost_weblate.endpoint.apps import BoostEndpointConfig |
| 257 | +
|
| 258 | + ready_calls: list[int] = [] |
| 259 | + _original_ready = BoostEndpointConfig.ready |
| 260 | +
|
| 261 | + def _counting_ready(self: BoostEndpointConfig) -> None: |
| 262 | + ready_calls.append(1) |
| 263 | + return _original_ready(self) |
| 264 | +
|
| 265 | + BoostEndpointConfig.ready = _counting_ready # type: ignore[method-assign] |
| 266 | +
|
| 267 | + import django |
| 268 | +
|
| 269 | + django.setup() |
| 270 | + print(f"ready_calls={{len(ready_calls)}}") |
| 271 | + """ |
| 272 | + ) |
| 273 | + result = subprocess.run( |
| 274 | + [sys.executable, "-c", script], |
| 275 | + capture_output=True, |
| 276 | + text=True, |
| 277 | + cwd=_REPO_ROOT, |
| 278 | + check=False, |
| 279 | + ) |
| 280 | + assert result.returncode == 0, result.stderr |
| 281 | + assert "ready_calls=1" in result.stdout |
0 commit comments