"""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 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: they sit below today's walk (13 modules, 60 imports, # 35 checked arguments, 7 awaited HA calls) so a broken walker shows up as a # failure instead of a silently green sweep. assert report.modules >= 10, report assert report.imports >= 40, report assert report.call_args >= 20, report assert report.awaited_calls >= 5, report 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)