The unit suite validates production modules against hand-written stubs, so a
call site that no longer matches real Home Assistant stays green — that is how
D1, D2 and D3 reached master. This adds the check that would have caught them.
`tests/ha_api_drift.py` (stdlib-only, documented inline) parses every module of
custom_components/nvidia_shield and resolves each Home-Assistant dependency
against an INSTALLED homeassistant:
1. every `homeassistant.*` import — module present, attribute present (or a
submodule of that name exists, which is what `from package import submodule`
means) — catches D1/D2 and any future move;
2. every keyword argument passed to a known HA API (HA_CALL_OWNERS: the
ConfigFlow/ConfigEntries APIs we call) must exist in the real signature —
catches the `options_updates` class of bug;
3. every `self.<method>()` call in a class that inherits from an HA class is
compared with the real definition: a coroutine must be awaited and an
awaited call must be a coroutine — catches D3.
`tests/test_ha_api_drift.py` runs it against the production tree, asserts the
sweep is not vacuous (module/import/argument/await counters), and pins the red
proof with the four pre-fix forms, plus the green proof for the fixed forms. It
skips cleanly when homeassistant is absent, so the stdlib-only jobs are
unaffected.
`.gitea/workflows/test.yml` gains the `ha-api-drift` job — the one place Home
Assistant is installed — guarded by an explicit Python >= 3.13 check (Home
Assistant 2026.x requires it) and the same PEP 668 retry the test job uses.
Red proof against the real pre-fix revisions (svn-style checkout of
`git show HEAD:…` into a temp dir, and the pre-t35 config_flow.py for the kwarg):
media_player.py:965: broken-import: from homeassistant.components.media_player
import BrowseMediaSource: …BrowseMediaSource does not exist
diagnostics.py:93: broken-import: from homeassistant.util.diagnostic import
async_redact_data: module 'homeassistant.util.diagnostic' is not importable
config_flow.py:425: missing-await: self._async_handle_discovery_without_unique_id()
is a coroutine on homeassistant.config_entries.ConfigFlow
config_flow.py:700: unknown-kwarg: async_update_reload_and_abort() has no
'options_updates' parameter (exit 1 on that tree, exit 0 on this one)
Verified: variant 1 289 passed / 2 skipped; variant 2 (androidtvremote2) 290
passed / 1 skipped; ruff clean; `UV_PYTHON=3.13 uvx --with homeassistant --with
pytest-asyncio pytest tests/test_ha_api_drift.py -q` → 4 passed (the bare form of
that command needs the pin here only because this sandbox's default Python is
3.15 and pycares has no wheel for it / no CMake to build it; CI pins the
interpreter explicitly).
92 lines
4.1 KiB
YAML
92 lines
4.1 KiB
YAML
# Quality gate: lint + unit tests + the HA API-drift sweep.
|
|
#
|
|
# The test suite is stdlib-only (parsers, intents, running-apps) and runs the
|
|
# vendored ha_adb_common helpers without installing Home Assistant — see
|
|
# tests/conftest.py. androidtvremote2 is installed so the real-library
|
|
# volume/TYPED-dict tests execute (tests/test_protocol_transport.py). Real
|
|
# firmware captures live in tests/fixtures
|
|
# (how to capture more: docs/RESEARCH_SHIELD.md §8).
|
|
#
|
|
# The `ha-api-drift` job is the ONE place Home Assistant is installed: every
|
|
# other job validates our code against hand-written stubs, which stay green
|
|
# even when a call site no longer matches the real API. That is how three
|
|
# drifts reached master (t36 D1/D2/D3) — see tests/ha_api_drift.py.
|
|
|
|
name: Test
|
|
|
|
on:
|
|
push:
|
|
branches: [master]
|
|
pull_request:
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Install test tooling
|
|
run: |
|
|
set -euo pipefail
|
|
# Try the plain install first: it succeeds on images whose Python is
|
|
# not externally managed (and on pip < 23, which has no PEP 668
|
|
# enforcement at all). When the runner image DOES mark its Python as
|
|
# externally managed — Debian/Ubuntu based Gitea act images do — pip
|
|
# refuses with externally-managed-environment and we retry with the
|
|
# sanctioned override. That override is safe here specifically
|
|
# because the job runs in a disposable container, and because the
|
|
# flag only exists on pip >= 23.0.1, the same version that raises
|
|
# the refusal in the first place.
|
|
#
|
|
# Tools are pinned to the versions the suite was validated against so
|
|
# a newer ruff rule set cannot redden the gate on unrelated commits;
|
|
# androidtvremote2 tracks the manifest floor so CI exercises the same
|
|
# dependency users install.
|
|
PKGS="pytest~=9.1 pytest-asyncio~=1.4 ruff~=0.17.0 'androidtvremote2>=0.2.0'"
|
|
if ! eval python3 -m pip install --quiet $PKGS; then
|
|
echo "system Python is externally managed; retrying with --break-system-packages"
|
|
eval python3 -m pip install --quiet --break-system-packages $PKGS
|
|
fi
|
|
|
|
- name: Lint
|
|
run: ruff check custom_components tests
|
|
|
|
- name: Unit tests
|
|
run: python3 -m pytest tests -q
|
|
|
|
# Resolve every Home-Assistant API use in our production modules against a
|
|
# real install: imports, keyword arguments and await/sync mismatches. The
|
|
# sweep is repo-owned (tests/ha_api_drift.py) and skips cleanly when
|
|
# Home Assistant is absent, so it is inert in the job above and live here.
|
|
ha-api-drift:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Check the runner Python can host Home Assistant
|
|
run: |
|
|
set -euo pipefail
|
|
# Home Assistant 2026.x requires Python >= 3.13; a newer runner just
|
|
# works. Fail loudly with the fix instead of a confusing resolver
|
|
# error when the image is older.
|
|
if ! python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 13) else 1)'; then
|
|
echo "Home Assistant needs Python >= 3.13 but this runner has $(python3 -V)."
|
|
echo "Add 'actions/setup-python@v5' with python-version '3.13' to this job."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Install Home Assistant + test tooling
|
|
run: |
|
|
set -euo pipefail
|
|
# Same PEP 668 dance as the test job (see the note there). The
|
|
# version floor tracks what we validate against; a resolver failure
|
|
# here is a real signal (Python too old), not something to swallow.
|
|
PKGS="pytest~=9.1 pytest-asyncio~=1.4 'homeassistant>=2026.10'"
|
|
if ! eval python3 -m pip install --quiet $PKGS; then
|
|
echo "system Python is externally managed; retrying with --break-system-packages"
|
|
eval python3 -m pip install --quiet --break-system-packages $PKGS
|
|
fi
|
|
|
|
- name: HA API-drift sweep
|
|
run: python3 -m pytest tests/test_ha_api_drift.py -q
|