"""Run the Home-Assistant API-drift sweep (t37). Skips cleanly when homeassistant is not installed — every other test file runs against hand-written stubs, and this is the one check that must see the real package. CI's `ha-api-drift` job installs it: uvx --with homeassistant --with pytest-asyncio pytest tests/test_ha_api_drift.py -q The sweep itself lives in ``tests/ha_api_drift.py`` (documented there). """ from __future__ import annotations import ast import subprocess import sys from pathlib import Path import ha_api_drift import pytest pytest.importorskip( "homeassistant", reason="the API-drift sweep resolves our call sites against a real install", ) #: The three t36 drifts plus the `options_updates` kwarg t35 removed — the #: sweep must flag every one of them, or it cannot protect us from the next. PRE_FIX_SOURCES = { "d1_browse_media_source.py": ( "from homeassistant.components.media_player import BrowseMediaSource\n" ), "d2_util_diagnostic.py": ( "from homeassistant.util.diagnostic import async_redact_data\n" ), "d3_unawaited_discovery.py": ( "from homeassistant import config_entries\n" "\n" "\n" "class PreFixFlow(config_entries.ConfigFlow, domain='nvidia_shield'):\n" " async def async_step_zeroconf(self, discovery_info):\n" " self._async_handle_discovery_without_unique_id()\n" ), "t35_options_updates.py": ( "from homeassistant import config_entries\n" "\n" "\n" "class PreFixReauthFlow(config_entries.ConfigFlow, domain='nvidia_shield'):\n" " async def async_step_reauth_confirm(self, user_input=None):\n" " self.async_update_reload_and_abort(\n" " self.entry, options_updates={'protocol_enabled': True}\n" " )\n" " return self.async_abort(reason='reauth_successful')\n" ), } def _require_installed_homeassistant(): """Skip when sys.modules holds a test stub rather than an install.""" import sys module = sys.modules["homeassistant"] path = getattr(module, "__file__", None) if path is None or not Path(path).exists(): pytest.skip("homeassistant in sys.modules is a test stub, not an install") def test_no_api_drift_in_the_production_tree(): """The gate: every HA import, kwarg and await matches the real package.""" _require_installed_homeassistant() report = ha_api_drift.scan() assert report.findings == [], "\n" + str(report) def test_the_sweep_is_not_vacuous(): """The counters prove the walk really covered the production tree.""" _require_installed_homeassistant() report = ha_api_drift.scan() # Floors, not targets: the numbers live in ha_api_drift.FLOORS — one source # of truth for this wrapper and the standalone entry point (t39 F1) — so a # broken or mis-targeted walk fails here instead of looking green. violations = report.floor_violations() assert violations == [], "\n".join(violations) def test_the_sweep_flags_the_pre_fix_defects(tmp_path): """Red proof: the t36 drifts and the options_updates kwarg are all caught.""" _require_installed_homeassistant() for name, source in PRE_FIX_SOURCES.items(): (tmp_path / name).write_text(source, encoding="utf-8") report = ha_api_drift.scan(tmp_path) found = {(finding.path, finding.kind) for finding in report.findings} assert found == { ("d1_browse_media_source.py", "broken-import"), ("d2_util_diagnostic.py", "broken-import"), ("d3_unawaited_discovery.py", "missing-await"), ("t35_options_updates.py", "unknown-kwarg"), }, "\n" + str(report) def test_the_sweep_accepts_the_fixed_forms(tmp_path): """The same call sites written the way production writes them are clean.""" _require_installed_homeassistant() (tmp_path / "fixed.py").write_text( "from homeassistant.components.media_player import BrowseMedia\n" "from homeassistant.helpers.redact import async_redact_data\n" "from homeassistant import config_entries\n" "\n" "\n" "class FixedFlow(config_entries.ConfigFlow, domain='nvidia_shield'):\n" " async def async_step_zeroconf(self, discovery_info):\n" " await self._async_handle_discovery_without_unique_id()\n" "\n" " def finish(self):\n" " return self.async_update_reload_and_abort(\n" " self.entry, options={'protocol_enabled': True}\n" " )\n", encoding="utf-8", ) report = ha_api_drift.scan(tmp_path) assert report.findings == [], "\n" + str(report) def _run_drift_module(*paths): """Run the standalone entry point in this (HA-bearing) interpreter.""" return subprocess.run( [ sys.executable, str(Path(ha_api_drift.__file__).resolve()), *[str(path) for path in paths], ], capture_output=True, text=True, check=False, ) def test_the_standalone_entry_point_passes_on_the_production_tree(): """F1: the CLI is the sweep, with the same verdict on the real tree.""" result = _run_drift_module() assert result.returncode == 0, result.stderr assert "no Home-Assistant API drift" in result.stdout def test_the_standalone_entry_point_fails_on_an_empty_directory(tmp_path): """t39 F1: zeros must not exit 0 — the floors apply to the CLI too.""" result = _run_drift_module(tmp_path) assert result.returncode != 0 assert "0 modules" in result.stdout # the counter that used to look green assert "FLOOR:" in result.stderr assert "drift sweep FAILED" in result.stderr def test_the_standalone_entry_point_fails_on_a_missing_path(tmp_path): """A mis-specified path is a failure, not a silent zero-module sweep.""" result = _run_drift_module(tmp_path / "not-a-directory") assert result.returncode != 0 assert "bad-path" in result.stdout def test_no_floor_constants_are_duplicated_in_this_wrapper(): """F1: the floors come from ha_api_drift.FLOORS — nowhere else.""" source = Path(__file__).read_text(encoding="utf-8") tree = ast.parse(source) local_ints = [ target.id for node in tree.body if isinstance(node, ast.Assign) for target in node.targets if isinstance(target, ast.Name) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, int) ] assert local_ints == [], f"floor-like constants belong in ha_api_drift: {local_ints}" assert "floor_violations" in source def test_the_declared_python_floor_never_lags_homeassistant(): """F2: the CI guard's floor is HA's own Requires-Python, not a guess.""" _require_installed_homeassistant() import importlib.metadata as metadata declared = metadata.metadata("homeassistant")["Requires-Python"] parsed = ha_api_drift.parse_requires_python(declared) assert parsed is not None, f"unparsable Requires-Python: {declared!r}" assert tuple(parsed) <= ha_api_drift.HOMEASSISTANT_PYTHON_FLOOR, ( f"tests/ha_api_drift.py declares {ha_api_drift.HOMEASSISTANT_PYTHON_FLOOR} " f"but the installed homeassistant requires {declared} — raise the constant " f"and the setup-python version in .gitea/workflows/test.yml" )