personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / conftest.py
6.2 kB 196 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3"""Pytest fixture-leak detector. 4 5Captures `git status --porcelain -- tests/fixtures/` at session start and 6diffs at session end. Fails the session with a named-path error when tests 7leave the fixture tree dirty. 8""" 9 10from __future__ import annotations 11 12import os 13import subprocess 14import sys 15import tempfile 16from pathlib import Path 17 18import pytest 19 20_TMPDIR_FALLBACK_NOTICE: str | None = None 21 22 23def _apply_tmpdir_fallback() -> None: 24 """Route tmp dirs to /var/tmp when TMPDIR is not exported. 25 26 make test sets TMPDIR=/var/tmp at the shell level (see Makefile). Direct 27 pytest invocations (.venv/bin/pytest, python -m pytest, uv run pytest) 28 bypass that. This prelude closes the gap by setting TMPDIR and 29 tempfile.tempdir at module-import time, before pytest builds its 30 tmp_path_factory. 31 """ 32 global _TMPDIR_FALLBACK_NOTICE 33 34 if "TMPDIR" in os.environ: 35 return 36 37 # Test-only override so unwritable-target branch can be exercised 38 # without chmod 000 /var/tmp. 39 target = os.environ.get("_SOLSTONE_TMPDIR_FALLBACK_TARGET", "/var/tmp") 40 41 if not (os.path.isdir(target) and os.access(target, os.W_OK)): 42 if os.environ.get("_SOLSTONE_TMPDIR_FALLBACK_NOTIFIED") != "1": 43 _TMPDIR_FALLBACK_NOTICE = ( 44 f"solstone: pytest invoked without TMPDIR export and fallback " 45 f"target {target} is not writable; leaving TMPDIR unset.\n" 46 ) 47 os.environ["_SOLSTONE_TMPDIR_FALLBACK_NOTIFIED"] = "1" 48 return 49 50 os.environ["TMPDIR"] = target 51 tempfile.tempdir = target 52 53 if os.environ.get("_SOLSTONE_TMPDIR_FALLBACK_NOTIFIED") != "1": 54 _TMPDIR_FALLBACK_NOTICE = ( 55 f"solstone: pytest invoked without TMPDIR export; routing tmp dirs " 56 f"to {target}. Prefer 'make test' to set TMPDIR at the shell level.\n" 57 ) 58 os.environ["_SOLSTONE_TMPDIR_FALLBACK_NOTIFIED"] = "1" 59 60 61_apply_tmpdir_fallback() 62 63_FIXTURE_ROOT = "tests/fixtures" 64_BASELINE: set[tuple[str, str]] | None = None 65_GIT_AVAILABLE = True 66 67 68def _capture_status(repo_root: Path) -> set[tuple[str, str]] | None: 69 """Return the set of (status_XY, path) tuples from fixture-tree git status. 70 71 Returns None when git is unavailable or the command fails (e.g. not a git 72 repo). 73 """ 74 try: 75 result = subprocess.run( 76 ["git", "status", "--porcelain", "--", _FIXTURE_ROOT], 77 cwd=repo_root, 78 capture_output=True, 79 text=True, 80 check=False, 81 ) 82 except FileNotFoundError: 83 return None 84 if result.returncode != 0: 85 return None 86 87 entries: set[tuple[str, str]] = set() 88 for line in result.stdout.splitlines(): 89 if len(line) < 4: 90 continue 91 status = line[:2] 92 path = line[3:] 93 if " -> " in path: 94 path = path.split(" -> ", 1)[1] 95 entries.add((status, path)) 96 return entries 97 98 99def _format_leak_message(new_entries: set[tuple[str, str]]) -> str: 100 lines = [f" {status} {path}" for status, path in sorted(new_entries)] 101 return ( 102 "\n" 103 "solstone fixture-leak detector: tests left tests/fixtures/ dirty\n" 104 + "\n".join(lines) 105 + "\n\n" 106 "To fix, use one of these isolation mechanisms:\n" 107 " - journal_copy fixture (tests/conftest.py:188) — copies tracked fixtures to tmp_path\n" 108 " - point SOLSTONE_JOURNAL at a tmp_path directly\n" 109 " - mock the subprocess/write path so code never touches tests/fixtures/\n" 110 "\n" 111 "Prior incidents: f6f382a6, 2996e072\n" 112 ) 113 114 115def pytest_sessionstart(session): 116 global _BASELINE, _GIT_AVAILABLE, _TMPDIR_FALLBACK_NOTICE 117 118 if hasattr(session.config, "workerinput"): 119 return 120 121 if _TMPDIR_FALLBACK_NOTICE is not None: 122 sys.stderr.write(_TMPDIR_FALLBACK_NOTICE) 123 _TMPDIR_FALLBACK_NOTICE = None 124 125 repo_root = session.config.rootpath 126 _BASELINE = _capture_status(repo_root) 127 if _BASELINE is None: 128 _GIT_AVAILABLE = False 129 sys.stderr.write("solstone fixture-leak detector: git unavailable, skipping\n") 130 131 132def pytest_sessionfinish(session, exitstatus): 133 if hasattr(session.config, "workerinput"): 134 return 135 136 if not _GIT_AVAILABLE or _BASELINE is None: 137 return 138 139 repo_root = session.config.rootpath 140 current = _capture_status(repo_root) 141 if current is None: 142 return 143 144 new_entries = current - _BASELINE 145 if not new_entries: 146 return 147 148 sys.stderr.write(_format_leak_message(new_entries)) 149 session.exitstatus = 1 150 151 152@pytest.fixture(autouse=True) 153def _reset_local_slot_sizing_state(): 154 """Keep per-process local sizing and admission state from leaking across tests.""" 155 156 def _reset() -> None: 157 admission = sys.modules.get("solstone.think.admission") 158 if admission is not None: 159 admission.reset_admission_state() 160 local_server = sys.modules.get("solstone.think.providers.local_server") 161 if local_server is not None: 162 local_server.reset_parallel_slots_cache() 163 fanout_policy = sys.modules.get("solstone.think.providers.fanout_policy") 164 if fanout_policy is not None: 165 fanout_policy.reset_default_cap_log_state() 166 thinking = sys.modules.get("solstone.think.thinking") 167 if thinking is not None: 168 thinking.reset_dispatch_admission_state() 169 170 _reset() 171 yield 172 _reset() 173 174 175@pytest.fixture(autouse=True) 176def _default_local_vulkan_gpu(): 177 """Keep local-provider readiness deterministic on GPU-less test hosts.""" 178 from solstone.think.providers import local_vulkan 179 180 original_detect_gpus = local_vulkan.detect_gpus 181 local_vulkan.reset_detect_cache() 182 183 def fake_detect_gpus(): 184 return [ 185 local_vulkan.VulkanDevice( 186 1, 187 "NVIDIA GeForce GTX 1660 Ti", 188 local_vulkan.VK_TYPE_DISCRETE, 189 6390, 190 ) 191 ] 192 193 local_vulkan.detect_gpus = fake_detect_gpus 194 yield 195 local_vulkan.detect_gpus = original_detect_gpus 196 local_vulkan.reset_detect_cache()