personal memory agent
0
fork

Configure Feed

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

conftest: fallback TMPDIR=/var/tmp for non-make pytest runs

make test exports TMPDIR=/var/tmp at the shell, but direct invocations
(.venv/bin/pytest, python -m pytest, uv run pytest) bypass the Makefile
and leak test dirs into /tmp. Add a module-level prelude in the root
conftest that sets TMPDIR and tempfile.tempdir to /var/tmp when TMPDIR
is unset, with a one-time stderr notice pointing at `make test` and a
visible degradation path when the target is not writable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+243 -1
+50 -1
conftest.py
··· 9 9 10 10 from __future__ import annotations 11 11 12 + import os 12 13 import subprocess 13 14 import sys 15 + import tempfile 14 16 from pathlib import Path 17 + 18 + _TMPDIR_FALLBACK_NOTICE: str | None = None 19 + 20 + 21 + def _apply_tmpdir_fallback() -> None: 22 + """Route tmp dirs to /var/tmp when TMPDIR is not exported. 23 + 24 + make test sets TMPDIR=/var/tmp at the shell level (see Makefile). Direct 25 + pytest invocations (.venv/bin/pytest, python -m pytest, uv run pytest) 26 + bypass that. This prelude closes the gap by setting TMPDIR and 27 + tempfile.tempdir at module-import time, before pytest builds its 28 + tmp_path_factory. 29 + """ 30 + global _TMPDIR_FALLBACK_NOTICE 31 + 32 + if "TMPDIR" in os.environ: 33 + return 34 + 35 + # Test-only override so unwritable-target branch can be exercised 36 + # without chmod 000 /var/tmp. 37 + target = os.environ.get("_SOLSTONE_TMPDIR_FALLBACK_TARGET", "/var/tmp") 38 + 39 + if not (os.path.isdir(target) and os.access(target, os.W_OK)): 40 + if os.environ.get("_SOLSTONE_TMPDIR_FALLBACK_NOTIFIED") != "1": 41 + _TMPDIR_FALLBACK_NOTICE = ( 42 + f"solstone: pytest invoked without TMPDIR export and fallback " 43 + f"target {target} is not writable; leaving TMPDIR unset.\n" 44 + ) 45 + os.environ["_SOLSTONE_TMPDIR_FALLBACK_NOTIFIED"] = "1" 46 + return 47 + 48 + os.environ["TMPDIR"] = target 49 + tempfile.tempdir = target 50 + 51 + if os.environ.get("_SOLSTONE_TMPDIR_FALLBACK_NOTIFIED") != "1": 52 + _TMPDIR_FALLBACK_NOTICE = ( 53 + f"solstone: pytest invoked without TMPDIR export; routing tmp dirs " 54 + f"to {target}. Prefer 'make test' to set TMPDIR at the shell level.\n" 55 + ) 56 + os.environ["_SOLSTONE_TMPDIR_FALLBACK_NOTIFIED"] = "1" 57 + 58 + 59 + _apply_tmpdir_fallback() 15 60 16 61 _FIXTURE_ROOT = "tests/fixtures" 17 62 _BASELINE: set[tuple[str, str]] | None = None ··· 66 111 67 112 68 113 def pytest_sessionstart(session): 69 - global _BASELINE, _GIT_AVAILABLE 114 + global _BASELINE, _GIT_AVAILABLE, _TMPDIR_FALLBACK_NOTICE 115 + 116 + if _TMPDIR_FALLBACK_NOTICE is not None: 117 + sys.stderr.write(_TMPDIR_FALLBACK_NOTICE) 118 + _TMPDIR_FALLBACK_NOTICE = None 70 119 71 120 repo_root = session.config.rootpath 72 121 _BASELINE = _capture_status(repo_root)
+193
tests/test_tmpdir_fallback.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + """Canary tests for the root conftest TMPDIR fallback prelude.""" 4 + 5 + from __future__ import annotations 6 + 7 + import os 8 + import subprocess 9 + import sys 10 + from pathlib import Path 11 + 12 + import pytest 13 + 14 + pytest_plugins = ["pytester"] 15 + 16 + _ROOT_CONFTEST = Path(__file__).resolve().parent.parent / "conftest.py" 17 + _NOTICE = ( 18 + "solstone: pytest invoked without TMPDIR export; routing tmp dirs to " 19 + "/var/tmp. Prefer 'make test' to set TMPDIR at the shell level.\n" 20 + ) 21 + 22 + 23 + def _install_root_conftest(pytester: pytest.Pytester) -> None: 24 + pytester.makepyfile(conftest=_ROOT_CONFTEST.read_text(encoding="utf-8")) 25 + 26 + 27 + def _run_nested( 28 + pytester: pytest.Pytester, 29 + env_overrides: dict[str, str | None] | None = None, 30 + ) -> subprocess.CompletedProcess[str]: 31 + env = os.environ.copy() 32 + env.pop("TMPDIR", None) 33 + env.pop("_SOLSTONE_TMPDIR_FALLBACK_NOTIFIED", None) 34 + env.pop("_SOLSTONE_TMPDIR_FALLBACK_TARGET", None) 35 + if env_overrides: 36 + for key, value in env_overrides.items(): 37 + if value is None: 38 + env.pop(key, None) 39 + else: 40 + env[key] = value 41 + return subprocess.run( 42 + [sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider"], 43 + cwd=pytester.path, 44 + env=env, 45 + capture_output=True, 46 + text=True, 47 + check=False, 48 + ) 49 + 50 + 51 + def test_prelude_redirects_to_var_tmp(pytester: pytest.Pytester) -> None: 52 + _install_root_conftest(pytester) 53 + pytester.makepyfile( 54 + test_tmpdir=""" 55 + import os 56 + import tempfile 57 + 58 + def test_redirects_to_var_tmp(): 59 + assert tempfile.gettempdir() == "/var/tmp" 60 + assert os.environ["TMPDIR"] == "/var/tmp" 61 + """ 62 + ) 63 + 64 + result = _run_nested(pytester) 65 + 66 + assert result.returncode == 0, result.stderr + result.stdout 67 + assert "1 passed" in result.stdout 68 + 69 + 70 + def test_notice_present_when_tmpdir_unset(pytester: pytest.Pytester) -> None: 71 + _install_root_conftest(pytester) 72 + pytester.makepyfile( 73 + test_notice=""" 74 + def test_noop(): 75 + assert True 76 + """ 77 + ) 78 + 79 + result = _run_nested(pytester) 80 + combined = result.stderr + result.stdout 81 + 82 + assert result.returncode == 0, combined 83 + assert _NOTICE in combined 84 + 85 + 86 + def test_notice_absent_when_tmpdir_already_set(pytester: pytest.Pytester) -> None: 87 + _install_root_conftest(pytester) 88 + pytester.makepyfile( 89 + test_notice=""" 90 + import os 91 + import tempfile 92 + 93 + def test_uses_existing_tmpdir(): 94 + assert tempfile.gettempdir() == "/tmp" 95 + assert os.environ["TMPDIR"] == "/tmp" 96 + """ 97 + ) 98 + 99 + result = _run_nested(pytester, {"TMPDIR": "/tmp"}) 100 + combined = result.stderr + result.stdout 101 + 102 + assert result.returncode == 0, combined 103 + assert "solstone: pytest invoked without TMPDIR" not in combined 104 + 105 + 106 + def test_notice_single_fire_across_workers(pytester: pytest.Pytester) -> None: 107 + _install_root_conftest(pytester) 108 + pytester.makepyfile( 109 + test_notice=""" 110 + def test_noop(): 111 + assert True 112 + """ 113 + ) 114 + 115 + first = _run_nested(pytester) 116 + second = _run_nested(pytester, {"_SOLSTONE_TMPDIR_FALLBACK_NOTIFIED": "1"}) 117 + combined = first.stderr + first.stdout + second.stderr + second.stdout 118 + 119 + assert first.returncode == 0, combined 120 + assert second.returncode == 0, combined 121 + assert combined.count(_NOTICE) == 1 122 + 123 + 124 + def test_unwritable_target_degrades_visibly( 125 + pytester: pytest.Pytester, tmp_path: Path 126 + ) -> None: 127 + _install_root_conftest(pytester) 128 + pytester.makepyfile( 129 + test_notice=""" 130 + import os 131 + 132 + def test_tmpdir_stays_unset(): 133 + assert os.environ.get("TMPDIR") is None 134 + """ 135 + ) 136 + blocked = tmp_path / "blocked" 137 + blocked.mkdir() 138 + os.chmod(blocked, 0) 139 + 140 + try: 141 + result = _run_nested( 142 + pytester, {"_SOLSTONE_TMPDIR_FALLBACK_TARGET": str(blocked)} 143 + ) 144 + finally: 145 + os.chmod(blocked, 0o700) 146 + 147 + combined = result.stderr + result.stdout 148 + notice = ( 149 + "solstone: pytest invoked without TMPDIR export and fallback target " 150 + f"{blocked} is not writable; leaving TMPDIR unset.\n" 151 + ) 152 + 153 + assert result.returncode == 0, combined 154 + assert notice in combined 155 + assert "routing tmp dirs to" not in combined 156 + 157 + 158 + def test_subprocess_pytest_lands_in_var_tmp(pytester: pytest.Pytester) -> None: 159 + _install_root_conftest(pytester) 160 + test_path = pytester.makepyfile( 161 + test_subprocess=""" 162 + import os 163 + import tempfile 164 + 165 + def test_redirects_in_fresh_subprocess(): 166 + assert tempfile.gettempdir() == "/var/tmp" 167 + assert os.environ["TMPDIR"] == "/var/tmp" 168 + """ 169 + ) 170 + env = os.environ.copy() 171 + env.pop("TMPDIR", None) 172 + env.pop("_SOLSTONE_TMPDIR_FALLBACK_NOTIFIED", None) 173 + env.pop("_SOLSTONE_TMPDIR_FALLBACK_TARGET", None) 174 + 175 + result = subprocess.run( 176 + [ 177 + sys.executable, 178 + "-m", 179 + "pytest", 180 + "-q", 181 + "-p", 182 + "no:cacheprovider", 183 + str(test_path), 184 + ], 185 + cwd=pytester.path, 186 + env=env, 187 + capture_output=True, 188 + text=True, 189 + check=False, 190 + ) 191 + 192 + assert result.returncode == 0, result.stderr + result.stdout 193 + assert "1 passed" in result.stdout