Files
James Mackay 2e1d162ef4 test(harness): make the stub scoping independent of file order (t44)
A subset of test files in an order the suite was not written for failed 3
test_init.py::TestImportStep tests with ModuleNotFoundError: voluptuous, while the
full suite stayed green. Two order-only mechanisms, both now fixed at the source:

1. entity_stublib.install() guarded on ONE sentinel
   (`homeassistant.components.media_player`). A file whose teardown popped
   `voluptuous` but left the sentinel turned the next file's install() into a no-op,
   so importing config_flow failed. The guard now checks every face a file scopes on
   (`_STUB_FACES`, derived from ENTITIES_STUB_NAMES so the two cannot drift) and
   rebuilds the set when anything is missing. install() also registers the
   `homeassistant.helpers.selector` face itself, which the per-file installers had
   been adding separately — without it the derived list would never be complete.

2. Teardowns restored only the names their file had listed, so a module the file
   loaded (`nsh.diagnostics`, `nsh.media_player`) survived into the next file still
   holding stub objects — a stub exception class, the redact helper — that had since
   been replaced; comparisons against the new objects then failed (5 tests in
   test_command_routing on an unlucky order). Every file now ends through
   entity_stublib.snapshot_modules()/restore_modules(), which drops EVERYTHING the
   file created under the harness namespaces (homeassistant/voluptuous/nsh) and puts
   back everything it replaced. Hand-written lists were the bug, so there is none to
   keep in sync.

The t7 no-leak discipline is unchanged: a file's stubs still never reach another
file, and a file's loaded modules still do not survive it. A session-scoped baseline
install was tried first and rejected: files whose own installer guards on "the stubs
are already there" skipped installing their richer faces, which produced exactly the
inconsistent state a baseline is meant to prevent (10 test_standalone_setup failures).

tests/test_harness_scoping.py: the sentinel poisoning, the wholesale-rebuild
invariant, a teardown that removes what the file loaded, the stale-object case
(diagnostics vs the redact helper), and — because order is the thing under test —
two subprocess runs of the reported orderings.

Verified: the reported order (test_command_routing, test_protocol_transport,
test_standalone_setup, test_init) 152 passed / 1 skipped; the minimal pair both ways;
the previously failing 6-test order now 167 passed; every test file in REVERSE
alphabetical order 369 passed / 2 skipped; two scrambled thirds green; full suite
369 / 2 and 370 / 1 with androidtvremote2; ruff clean.
2026-10-11 06:41:56 +01:00

54 lines
1.8 KiB
Python

"""Test bootstrap.
Puts the vendored ha_adb_common package on sys.path so helpers can be imported
directly (their code is stdlib-only) without installing Home Assistant or
executing the integration's __init__ module.
The integration-package modules that stay Home-Assistant-free at import time
(currently connection.py) are also made importable — via a synthetic package
("nsh") that wraps custom_components/nvidia_shield WITHOUT executing its
__init__.py (that one imports Home Assistant). Only the stdlib-only modules
are loaded into it explicitly; their relative imports resolve normally.
"""
import importlib.util
import sys
import types
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "custom_components" / "nvidia_shield"))
_SHIELD_DIR = REPO_ROOT / "custom_components" / "nvidia_shield"
_PKG = "nsh"
def _load_module(name: str, path: Path, search_paths: list[str] | None = None):
"""Import a source file under ``name`` without touching real packages."""
spec = importlib.util.spec_from_file_location(
name, path, submodule_search_locations=search_paths
)
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module
# Synthetic integration package — no __init__.py execution (HA-free).
_root = types.ModuleType(_PKG)
_root.__path__ = [str(_SHIELD_DIR)]
sys.modules[_PKG] = _root
# Real subpackage __init__ (stdlib-only) so `from .ha_adb_common import ...`
# works through the synthetic package.
_load_module(
f"{_PKG}.ha_adb_common",
_SHIELD_DIR / "ha_adb_common" / "__init__.py",
[str(_SHIELD_DIR / "ha_adb_common")],
)
# Stdlib-only integration modules.
for _mod in ("const", "adb_session", "connection"):
_load_module(f"{_PKG}.{_mod}", _SHIELD_DIR / f"{_mod}.py")