From 948310ea2247ec6b02bc6e940502022f5d65aab6 Mon Sep 17 00:00:00 2001 From: Daniel Meppiel Date: Mon, 11 May 2026 13:44:26 +0200 Subject: [PATCH 1/2] fix(tests): override Path.home() in root conftest so Windows xdist gw2 cannot crash PR #1271 set HOME / USERPROFILE / HOMEDRIVE / HOMEPATH at conftest import time, but a single xdist worker (gw2) on windows-2025-vs2026 still hit 46 'RuntimeError: Could not determine home directory' failures (run 25667668848). Whatever the cause -- worker subprocess spawning order, conftest discovery quirks, or some other code path calling Path.home() before the unit-conftest mutation took effect on that worker -- the env-mutation approach is not robust enough. Override Path.home() directly in tests/conftest.py (root). The override: - is applied at import time of the root conftest, which every xdist worker loads before any test in any directory runs; - returns Path(env['HOME']) when set, then the Windows trio, falling back to a hermetic tmp dir only when nothing is set; - never raises, so any production or test code path calling Path.home() during the test run gets a usable path even on runners that would otherwise leave the worker subprocess with an empty HOME / USERPROFILE. Per-test 'monkeypatch.setenv("HOME", ...)' (e.g. the scope-resolution tests under tests/unit/integration/) keeps working because the override reads from os.environ on every call. The previous tests/unit/conftest.py import-time mutation is now redundant -- delete it. Refs run https://github.com/microsoft/apm/actions/runs/25667668848 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 + tests/conftest.py | 67 ++++++++++++++++++++++++++++++++++++++---- tests/unit/conftest.py | 60 ------------------------------------- 3 files changed, 62 insertions(+), 66 deletions(-) delete mode 100644 tests/unit/conftest.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b30046b00..54f040011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Pin `Path.home()` under unit tests via a session-scoped autouse conftest fixture, fixing 56 Windows runner failures on the new `windows-2025-vs2026` GitHub-hosted image where `USERPROFILE`/`HOMEDRIVE`+`HOMEPATH` are not seeded for pytest workers; also patch the `_check_and_notify_updates` import binding in the disabled-self-update test so it no longer races on the version-check cache. (#1270) - `apm install` now works on macOS git 2.53.0 (Homebrew): bare-cache commands switch to `--git-dir` to satisfy the `safe.bareRepository=explicit` default; fetched SHAs are pinned as synthetic refs so `git clone --local --shared` no longer silently omits them. (#1268) - Set the unit-test hermetic HOME at conftest import time so a single xdist worker on the `windows-2025-vs2026` runner can no longer race fixture setup and re-trigger the 53 `Path.home()` failures the session-scoped autouse fixture was supposed to prevent. (#1271) +- Override `Path.home()` itself in the root test conftest so the 46 remaining Windows `RuntimeError: Could not determine home directory` failures on xdist worker `gw2` cannot recur regardless of which conftest the worker imports first; per-test `monkeypatch.setenv("HOME", ...)` continues to work because the override consults env vars before falling back to the hermetic tmp dir. (#1272) ## [0.13.0] - 2026-05-11 diff --git a/tests/conftest.py b/tests/conftest.py index 954b2bf8b..1f498da66 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,19 +1,74 @@ -# Root conftest.py — shared pytest configuration +# Root conftest.py -- shared pytest configuration # # Test directory structure: -# tests/unit/ — Fast isolated unit tests (default CI scope) -# tests/integration/ — E2E tests requiring network / external services -# tests/acceptance/ — Acceptance criteria tests -# tests/benchmarks/ — Performance benchmarks (excluded by default) -# tests/test_*.py — Root-level tests (mixed unit/integration) +# tests/unit/ -- Fast isolated unit tests (default CI scope) +# tests/integration/ -- E2E tests requiring network / external services +# tests/acceptance/ -- Acceptance criteria tests +# tests/benchmarks/ -- Performance benchmarks (excluded by default) +# tests/test_*.py -- Root-level tests (mixed unit/integration) # # Quick reference: # uv run pytest tests/unit tests/test_console.py -x # CI-equivalent fast run # uv run pytest # Full suite # uv run pytest -m benchmark # Benchmarks only +# +# Path.home() override (top of file, runs at conftest import time on every +# pytest-xdist worker): the windows-2025-vs2026 GitHub-hosted runner does not +# seed USERPROFILE / HOMEDRIVE / HOMEPATH for pytest-xdist worker subprocesses, +# so Path.home() raises RuntimeError. Earlier attempts patched only the env +# vars in tests/unit/conftest.py, but at least one xdist worker (gw2) still +# evaluated Path.home() before that conftest's import-time mutation took +# effect. Override Path.home() directly here -- the root conftest is loaded +# by every worker before any test in any test directory runs, so this is +# the earliest hook we have without writing a pytest plugin. + +import os +import tempfile +from pathlib import Path import pytest +_TMP_HOME = Path(tempfile.mkdtemp(prefix="apm-test-home-")) + + +def _ensure_home_env(home: Path) -> None: + home_str = str(home) + os.environ["HOME"] = home_str + if os.name == "nt": + os.environ["USERPROFILE"] = home_str + drive, _, tail = home_str.partition(":") + if tail: + os.environ["HOMEDRIVE"] = f"{drive}:" + os.environ["HOMEPATH"] = tail + + +_ensure_home_env(_TMP_HOME) + + +def _hermetic_home(_cls=Path) -> Path: + """Resolve a home dir without ever raising. + + Honors HOME / USERPROFILE / HOMEDRIVE+HOMEPATH so per-test + `monkeypatch.setenv("HOME", ...)` (or its Windows-trio equivalent) + keeps working. Falls back to a hermetic tmp dir only when the env + is empty -- which is the windows-2025-vs2026 xdist worker case. + """ + home = os.environ.get("HOME") + if not home and os.name == "nt": + home = os.environ.get("USERPROFILE") + if not home: + drive = os.environ.get("HOMEDRIVE", "") + tail = os.environ.get("HOMEPATH", "") + if tail: + home = drive + tail + return Path(home) if home else _TMP_HOME + + +# Override Path.home() so any code path -- production or test -- that calls +# it during the test run gets the hermetic tmp dir, regardless of whether +# the runner subprocess inherited a usable HOME / USERPROFILE. +Path.home = classmethod(_hermetic_home) # type: ignore[method-assign] + @pytest.fixture(autouse=True, scope="session") def _validate_primitive_coverage(): diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py deleted file mode 100644 index 5566b2c59..000000000 --- a/tests/unit/conftest.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Unit-test conftest: hermetic HOME isolation. - -This module sets ``HOME`` (POSIX) / ``USERPROFILE`` + ``HOMEDRIVE`` + -``HOMEPATH`` (Windows) to a process-wide tmp directory **at import -time**, before any fixture or test resolution. Two reasons: - -1. Hermeticity. Unit tests must not read or write the contributor's real - ``~`` (config files, runtimes, caches). Tests that call - ``RuntimeManager()`` or ``Path.home()`` directly previously inherited - whatever HOME the runner had. -2. Windows runner robustness. On the ``windows-2025-vs2026`` GitHub - Actions image the ``USERPROFILE`` / ``HOMEDRIVE`` + ``HOMEPATH`` - triplet is not set under the pytest-xdist worker subprocess, so - ``Path.home()`` raises ``RuntimeError: Could not determine home - directory.`` - -Why import-time and not a session-scoped autouse fixture: a previous -attempt used ``@pytest.fixture(scope="session", autouse=True)`` and -still failed on a single xdist worker (``gw2`` on Windows). Whatever -the cause (worker scheduling order, a downstream fixture resolving -``Path.home()`` before the autouse fixture's setup completed), running -the env mutation at conftest import time guarantees the env vars are -in place before pytest collects, schedules, or runs anything in this -worker process. Pytest imports each test directory's conftest.py once -per worker, before any fixtures run. - -Per-test fixtures that need a different HOME (e.g. tests/unit/integration -that exercise scope resolution) keep using ``monkeypatch.setenv`` and -override this baseline; monkeypatch's snapshot/restore cycle preserves -this baseline across tests. -""" - -from __future__ import annotations - -import os -import tempfile -from pathlib import Path - - -def _set_home_env(home: Path) -> None: - """Set ``HOME`` and the Windows-equivalent vars to ``home``. - - ``Path.home()`` consults ``HOME`` on POSIX but ``USERPROFILE`` - (with ``HOMEDRIVE`` + ``HOMEPATH`` fallback) on Windows. We - overwrite unconditionally because the Windows runner sometimes - leaves these keys present but empty, which still trips - ``Path.home()``. - """ - home_str = str(home) - os.environ["HOME"] = home_str - if os.name == "nt": - os.environ["USERPROFILE"] = home_str - drive, _, tail = home_str.partition(":") - if tail: - os.environ["HOMEDRIVE"] = f"{drive}:" - os.environ["HOMEPATH"] = tail - - -_TMP_HOME = Path(tempfile.mkdtemp(prefix="apm-unit-home-")) -_set_home_env(_TMP_HOME) From eb0768b853303fe3835795b36381885dfc6b9bdd Mon Sep 17 00:00:00 2001 From: Daniel Meppiel Date: Mon, 11 May 2026 13:47:46 +0200 Subject: [PATCH 2/2] test: regression trap for Path.home() with cleared env (gw2 scenario) Locks in the contract: Path.home() must not raise even when every env var ntpath.expanduser / posixpath.expanduser consults is unset. This is exactly the windows-2025-vs2026 xdist worker case that produced the 56 / 53 / 46 failure cascades. Also asserts that per-test monkeypatch.setenv("HOME", ...) keeps working (so the scope-resolution tests under tests/unit/integration/ don't regress). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/unit/test_path_home_override.py | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/unit/test_path_home_override.py diff --git a/tests/unit/test_path_home_override.py b/tests/unit/test_path_home_override.py new file mode 100644 index 000000000..5e07bd897 --- /dev/null +++ b/tests/unit/test_path_home_override.py @@ -0,0 +1,33 @@ +"""Regression trap for the root-conftest Path.home() override. + +Locks in the contract that survives the windows-2025-vs2026 GitHub +runner: ``Path.home()`` MUST NOT raise even when every env var that +``ntpath.expanduser`` (Windows) or ``posixpath.expanduser`` (POSIX) +consults is unset. The earlier 56-failure / 53-failure / 46-failure +runs all tripped on a single xdist worker that hit +``RuntimeError: Could not determine home directory``. +""" + +from __future__ import annotations + +from pathlib import Path + + +def test_path_home_does_not_raise_with_cleared_env(monkeypatch): + """Path.home() must return a usable Path even with HOME/USERPROFILE/etc cleared.""" + for key in ("HOME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH"): + monkeypatch.delenv(key, raising=False) + + home = Path.home() + + assert isinstance(home, Path) + assert str(home) + + +def test_path_home_honors_per_test_home_setenv(monkeypatch, tmp_path): + """Per-test ``monkeypatch.setenv("HOME", ...)`` must still redirect Path.home().""" + target = tmp_path / "custom-home" + target.mkdir() + monkeypatch.setenv("HOME", str(target)) + + assert Path.home() == target