|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import importlib |
3 | 4 | import os |
4 | 5 | import sys |
5 | 6 | from pathlib import Path |
|
12 | 13 | if str(REPO_ROOT) not in sys.path: |
13 | 14 | sys.path.insert(0, str(REPO_ROOT)) |
14 | 15 |
|
| 16 | +import pytest # noqa: E402 |
| 17 | + |
| 18 | +from loopx import paths # noqa: E402 |
15 | 19 | from loopx.canary.runner import SMOKE_SUITE_CHOICES # noqa: E402 |
16 | 20 | from loopx.semantics.production import NPM_DEV_DEPENDENCIES_MISSING # noqa: E402 |
17 | 21 |
|
18 | 22 |
|
| 23 | +# Default runtime routes resolve from HOME at import time. A test that writes |
| 24 | +# either one leaves state behind, and once both hold state every later implicit |
| 25 | +# default route fails as a conflict, so each test gets its own disposable pair. |
| 26 | +_DEFAULT_ROUTE_REFERENCES = ( |
| 27 | + ("loopx.paths", "DEFAULT_RUNTIME_ROOT", (".loopx",)), |
| 28 | + ("loopx.paths", "LEGACY_RUNTIME_ROOT", (".codex", "loopx")), |
| 29 | + ("loopx.contract", "DEFAULT_RUNTIME_ROOT", (".loopx",)), |
| 30 | + ("loopx.contract", "LEGACY_RUNTIME_ROOT", (".codex", "loopx")), |
| 31 | + ("loopx.cli_commands.registry_admin_lifecycle", "DEFAULT_RUNTIME_ROOT", (".loopx",)), |
| 32 | + ("loopx.cli_commands.registry_admin_lifecycle", "LEGACY_LOCAL_RUNTIME_ROOT", (".codex", "loopx")), |
| 33 | + ("loopx.control_plane.runtime.local_state_migration", "DEFAULT_RUNTIME_ROOT", (".loopx",)), |
| 34 | + ("loopx.control_plane.runtime.local_state_migration", "LEGACY_RUNTIME_ROOT", (".codex", "loopx")), |
| 35 | +) |
| 36 | +_REAL_DEFAULT_ROUTES = (paths.DEFAULT_RUNTIME_ROOT, paths.LEGACY_RUNTIME_ROOT) |
| 37 | + |
| 38 | + |
| 39 | +@pytest.fixture(autouse=True) |
| 40 | +def _isolated_default_runtime_routes(tmp_path_factory, monkeypatch): |
| 41 | + home = tmp_path_factory.mktemp("home") |
| 42 | + monkeypatch.setenv("HOME", str(home)) |
| 43 | + monkeypatch.setenv("USERPROFILE", str(home)) |
| 44 | + monkeypatch.delenv("CODEX_HOME", raising=False) |
| 45 | + for module_name, attribute, parts in _DEFAULT_ROUTE_REFERENCES: |
| 46 | + monkeypatch.setattr(importlib.import_module(module_name), attribute, home.joinpath(*parts)) |
| 47 | + yield |
| 48 | + |
| 49 | + |
| 50 | +def pytest_sessionstart(session) -> None: |
| 51 | + # Only routes absent at start are guarded; existing developer state may be |
| 52 | + # changed by other local processes during the run. |
| 53 | + session.config._loopx_absent_routes = [ |
| 54 | + root for root in _REAL_DEFAULT_ROUTES if not os.path.lexists(root) |
| 55 | + ] |
| 56 | + |
| 57 | + |
| 58 | +def pytest_sessionfinish(session, exitstatus) -> None: |
| 59 | + created = [ |
| 60 | + root for root in getattr(session.config, "_loopx_absent_routes", []) |
| 61 | + if os.path.lexists(root) |
| 62 | + ] |
| 63 | + if not created: |
| 64 | + return |
| 65 | + reporter = session.config.pluginmanager.get_plugin("terminalreporter") |
| 66 | + if reporter is not None: |
| 67 | + reporter.write_sep("=", "loopx default runtime route leak", red=True) |
| 68 | + reporter.write_line( |
| 69 | + f"Tests created real default runtime routes {', '.join(map(str, created))}; " |
| 70 | + "isolate the writer." |
| 71 | + ) |
| 72 | + session.exitstatus = pytest.ExitCode.TESTS_FAILED |
| 73 | + |
| 74 | + |
19 | 75 | def pytest_addoption(parser) -> None: |
20 | 76 | group = parser.getgroup("loopx-smoke-suite") |
21 | 77 | group.addoption( |
|
0 commit comments