Skip to content

Commit 99cec2f

Browse files
authored
ci: add unit tests to PR pipeline and pre-commit hook (homeassistant-ai#620)
* ci: add unit tests to PR pipeline and pre-commit hook Unit tests were not run in CI or locally, so failures went undetected. Add a unit-tests job to the PR workflow and a pre-commit hook that runs them on every commit. Also rewrite test_performance_parallelization to assert on observable outcomes (correct results, resilience, parallelism) instead of internal implementation details that broke when the fetch strategy changed. * ci: authenticate to GHCR for higher rate limits Add docker/login-action to authenticate with GITHUB_TOKEN before pulling the Home Assistant image. This avoids anonymous rate limits on GHCR that caused CI failures on shared runners. * ci: increase retry attempts and wait for GHCR rate limits on ARM ARM runners share a congested GHCR rate limit pool. Increase from 3 attempts / 15s wait to 5 attempts / 30s wait to give more time for the rate limit bucket to refill. * ci: remove ineffective GHCR login step Fork GITHUB_TOKEN has no elevated access to the home-assistant package, so the login didn't help with rate limits. Rely on retry logic instead. * ci: only run unit test hook on code changes Replace always_run with file pattern filter so the pre-commit unit test hook skips non-code commits (docs, CI config, etc). * ci: cache HA Docker image and fallback to Docker Hub Replace retry-based pull with actions/cache for the HA image. On cache miss, try GHCR first then fall back to Docker Hub. The image is cached per-architecture so ARM and x64 each get their own cached tar. This avoids repeated GHCR rate limit failures on shared ARM runners. * fix: keep HA image versions in sync via Renovate Add workflow YAML files to Renovate's managerFilePatterns so HA_IMAGE_GHCR env vars are bumped alongside test file versions. - Expand Renovate regex manager to scan .github/workflows/*.yml - Add # renovate: comments to HA_IMAGE_GHCR in pr.yml and e2e-tests.yml - Derive HA_IMAGE_DOCKERHUB at runtime from HA_IMAGE_GHCR version - Extract HA_IMAGE variable in conftest.py and test_env_manager.py for consistency with run_uat.py
1 parent 59787a2 commit 99cec2f

9 files changed

Lines changed: 399 additions & 286 deletions

File tree

.github/workflows/e2e-tests.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ permissions:
2020
env:
2121
PYTHON_VERSION: "3.13"
2222
UV_CACHE_DIR: /tmp/.uv-cache
23+
# renovate: datasource=docker depName=ghcr.io/home-assistant/home-assistant
24+
HA_IMAGE_GHCR: "ghcr.io/home-assistant/home-assistant:2026.1.3"
2325

2426
jobs:
2527
# Comprehensive E2E validation for main branch
@@ -33,6 +35,8 @@ jobs:
3335

3436
- name: Set up Docker Buildx
3537
uses: docker/setup-buildx-action@v3
38+
with:
39+
cache-binary: true
3640

3741
- name: Install uv
3842
uses: astral-sh/setup-uv@v7
@@ -45,6 +49,37 @@ jobs:
4549
- name: Install dependencies
4650
run: uv sync --all-extras --dev
4751

52+
- name: Cache HA Docker image
53+
id: cache-ha-image
54+
uses: actions/cache@v4
55+
with:
56+
path: /tmp/ha-image.tar
57+
key: ha-image-${{ env.HA_IMAGE_GHCR }}-${{ runner.arch }}
58+
59+
- name: Load cached HA image
60+
if: steps.cache-ha-image.outputs.cache-hit == 'true'
61+
run: docker load -i /tmp/ha-image.tar
62+
63+
- name: Pull HA image (GHCR → Docker Hub fallback)
64+
if: steps.cache-ha-image.outputs.cache-hit != 'true'
65+
run: |
66+
HA_VERSION="${HA_IMAGE_GHCR##*:}"
67+
HA_IMAGE_DOCKERHUB="homeassistant/home-assistant:${HA_VERSION}"
68+
for registry in "$HA_IMAGE_GHCR" "$HA_IMAGE_DOCKERHUB"; do
69+
echo "Trying $registry..."
70+
if docker pull "$registry"; then
71+
if [ "$registry" != "$HA_IMAGE_GHCR" ]; then
72+
docker tag "$registry" "$HA_IMAGE_GHCR"
73+
fi
74+
docker save "$HA_IMAGE_GHCR" -o /tmp/ha-image.tar
75+
echo "Pulled and cached from $registry"
76+
exit 0
77+
fi
78+
echo "Failed to pull from $registry, trying next..."
79+
sleep 15
80+
done
81+
echo "All registries failed" && exit 1
82+
4883
- name: Run full E2E test suite
4984
run: |
5085
echo "🚀 Running full E2E test suite with 3 workers..."

.github/workflows/pr.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ on:
1010
env:
1111
PYTHON_VERSION: "3.13"
1212
UV_CACHE_DIR: /tmp/.uv-cache
13+
# renovate: datasource=docker depName=ghcr.io/home-assistant/home-assistant
14+
HA_IMAGE_GHCR: "ghcr.io/home-assistant/home-assistant:2026.1.3"
1315

1416
jobs:
1517
lint:
@@ -28,6 +30,23 @@ jobs:
2830
- name: Run ruff check
2931
run: uv run ruff check src/ tests/
3032

33+
# Fast unit tests (no Docker, no HA instance needed)
34+
unit-tests:
35+
name: Unit Tests
36+
runs-on: ubuntu-latest
37+
container:
38+
image: ghcr.io/astral-sh/uv:0.9.30-python3.13-bookworm-slim
39+
timeout-minutes: 5
40+
41+
steps:
42+
- uses: actions/checkout@v6
43+
44+
- name: Install dependencies
45+
run: uv sync --all-extras --dev
46+
47+
- name: Run unit tests
48+
run: uv run pytest tests/src/unit/ -n auto --tb=short -v
49+
3150
# Comprehensive E2E validation for all PRs
3251
e2e-validation:
3352
name: E2E Validation (${{ matrix.os }})
@@ -50,6 +69,8 @@ jobs:
5069

5170
- name: Set up Docker Buildx
5271
uses: docker/setup-buildx-action@v3
72+
with:
73+
cache-binary: true
5374

5475
- name: Install uv
5576
uses: astral-sh/setup-uv@v7
@@ -62,6 +83,37 @@ jobs:
6283
- name: Install dependencies
6384
run: uv sync --all-extras --dev
6485

86+
- name: Cache HA Docker image
87+
id: cache-ha-image
88+
uses: actions/cache@v4
89+
with:
90+
path: /tmp/ha-image.tar
91+
key: ha-image-${{ env.HA_IMAGE_GHCR }}-${{ runner.arch }}
92+
93+
- name: Load cached HA image
94+
if: steps.cache-ha-image.outputs.cache-hit == 'true'
95+
run: docker load -i /tmp/ha-image.tar
96+
97+
- name: Pull HA image (GHCR → Docker Hub fallback)
98+
if: steps.cache-ha-image.outputs.cache-hit != 'true'
99+
run: |
100+
HA_VERSION="${HA_IMAGE_GHCR##*:}"
101+
HA_IMAGE_DOCKERHUB="homeassistant/home-assistant:${HA_VERSION}"
102+
for registry in "$HA_IMAGE_GHCR" "$HA_IMAGE_DOCKERHUB"; do
103+
echo "Trying $registry..."
104+
if docker pull "$registry"; then
105+
if [ "$registry" != "$HA_IMAGE_GHCR" ]; then
106+
docker tag "$registry" "$HA_IMAGE_GHCR"
107+
fi
108+
docker save "$HA_IMAGE_GHCR" -o /tmp/ha-image.tar
109+
echo "Pulled and cached from $registry"
110+
exit 0
111+
fi
112+
echo "Failed to pull from $registry, trying next..."
113+
sleep 15
114+
done
115+
echo "All registries failed" && exit 1
116+
65117
- name: Run full E2E test suite
66118
run: |
67119
echo "🚀 Running full E2E test suite with ${{ matrix.pytest_workers }} workers..."
@@ -84,6 +136,8 @@ jobs:
84136

85137
- name: Set up Docker Buildx
86138
uses: docker/setup-buildx-action@v3
139+
with:
140+
cache-binary: true
87141

88142
- name: Install uv
89143
uses: astral-sh/setup-uv@v7

.pre-commit-config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ repos:
44
hooks:
55
- id: ruff
66
args: [--fix]
7+
- repo: local
8+
hooks:
9+
- id: unit-tests
10+
name: unit tests
11+
entry: uv run pytest tests/src/unit/ -n auto -m "not slow" --tb=short -q
12+
language: system
13+
pass_filenames: false
14+
files: ^(src/|tests/|pyproject\.toml)

renovate.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
{
1717
"customType": "regex",
1818
"managerFilePatterns": [
19-
"/tests/.*\\.py$/"
19+
"/tests/.*\\.py$/",
20+
"/.github/workflows/.*\\.yml$/"
2021
],
2122
"matchStrings": [
2223
"# renovate: datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)\\s+.*?\\S+:(?<currentValue>[\\d.]+)"

tests/src/e2e/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ def ha_container_with_fresh_config():
165165

166166
# Create testcontainer with port configuration
167167
# renovate: datasource=docker depName=ghcr.io/home-assistant/home-assistant
168-
container = DockerContainer("ghcr.io/home-assistant/home-assistant:2026.1.3")
168+
HA_IMAGE = "ghcr.io/home-assistant/home-assistant:2026.1.3"
169+
container = DockerContainer(HA_IMAGE)
169170

170171
# Check for custom port via environment variable
171172
custom_port = os.environ.get("HA_TEST_PORT")

tests/src/unit/test_bulk_device_control.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ async def test_non_dict_operation_reports_error(self, device_control_tools):
7777
for detail in result["skipped_details"]:
7878
assert "not a dict" in detail["error"]
7979

80+
@pytest.mark.slow
8081
@pytest.mark.asyncio
8182
async def test_mixed_valid_and_invalid_operations(self, device_control_tools):
8283
"""Mix of valid and invalid operations reports skipped ones.

tests/src/unit/test_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import subprocess
55
import sys
66

7+
import pytest
78

89

10+
@pytest.mark.slow
911
class TestConfigErrorHandling:
1012
"""Test configuration error handling and user-friendly messages."""
1113

0 commit comments

Comments
 (0)