Skip to content

Commit 821ea77

Browse files
authored
Merge pull request #8 from tempus2016/claude/analyze-test-coverage-LkM4P
Claude/analyze test coverage lk m4 p
2 parents 20c1c6c + c6ac79d commit 821ea77

8 files changed

Lines changed: 1568 additions & 0 deletions

File tree

.github/workflows/tests.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
pull_request:
7+
branches: ["**"]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: "3.11"
19+
20+
- name: Install dependencies
21+
run: pip install pytest
22+
23+
- name: Run tests
24+
run: python -m pytest tests/ -v

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
*.swp
2+
__pycache__/
3+
*.pyc
4+
.pytest_cache/
25

36
# Dev environment - generated files
47
dev/config/.storage/auth

pytest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[pytest]
2+
testpaths = tests
3+
pythonpath = .

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
"""Shared test configuration and Home Assistant stubs for TaskMate tests.
2+
3+
All homeassistant stubs are installed into sys.modules here, at module-load
4+
time, so that any subsequent `from custom_components.taskmate.xxx import …`
5+
statements resolve without needing a real HA installation.
6+
"""
7+
from __future__ import annotations
8+
9+
import asyncio
10+
import sys
11+
from unittest.mock import AsyncMock, MagicMock
12+
import datetime as _dt
13+
14+
import pytest
15+
16+
# ---------------------------------------------------------------------------
17+
# Home Assistant stubs
18+
# These must be in place BEFORE any integration module is imported.
19+
# ---------------------------------------------------------------------------
20+
21+
_UTC = _dt.timezone.utc
22+
23+
24+
# ── homeassistant.core ──────────────────────────────────────────────────────
25+
26+
class FakeHass:
27+
"""Minimal mock of HomeAssistant."""
28+
29+
def __init__(self):
30+
self.services = MagicMock()
31+
self.services.async_call = AsyncMock()
32+
self.bus = MagicMock()
33+
34+
def async_create_task(self, coro):
35+
# Don't schedule; just close to avoid 'coroutine never awaited' warnings
36+
if asyncio.iscoroutine(coro):
37+
coro.close()
38+
return None
39+
40+
41+
_ha_core = MagicMock()
42+
_ha_core.HomeAssistant = FakeHass
43+
_ha_core.callback = lambda f: f # pass-through decorator
44+
_ha_core.ServiceCall = MagicMock
45+
46+
47+
# ── homeassistant.helpers.update_coordinator ────────────────────────────────
48+
49+
class FakeDataUpdateCoordinator:
50+
"""Minimal base class that TaskMateCoordinator inherits from."""
51+
52+
def __init__(self, hass, logger, *, name, update_interval=None):
53+
self.hass = hass
54+
self.data: dict = {}
55+
56+
async def async_refresh(self):
57+
"""No-op in tests unless overridden."""
58+
59+
60+
_ha_coordinator = MagicMock()
61+
_ha_coordinator.DataUpdateCoordinator = FakeDataUpdateCoordinator
62+
63+
64+
# ── homeassistant.helpers.storage ───────────────────────────────────────────
65+
66+
class FakeStore:
67+
"""In-memory Store substitute that avoids the filesystem."""
68+
69+
def __init__(self, hass, version, key):
70+
self._data = None
71+
72+
async def async_load(self):
73+
return self._data
74+
75+
async def async_save(self, data):
76+
self._data = data
77+
78+
79+
_ha_storage_mod = MagicMock()
80+
_ha_storage_mod.Store = FakeStore
81+
82+
83+
# ── homeassistant.helpers.event ─────────────────────────────────────────────
84+
85+
_ha_event = MagicMock()
86+
_ha_event.async_track_time_change = MagicMock(return_value=lambda: None)
87+
88+
89+
# ── homeassistant.util.dt ────────────────────────────────────────────────────
90+
# coordinator.py imports this as: from homeassistant.util import dt as dt_util
91+
92+
_DEFAULT_NOW = _dt.datetime(2024, 3, 20, 12, 0, 0, tzinfo=_UTC) # Wednesday
93+
94+
95+
class _DtUtilMock:
96+
"""Controllable drop-in for homeassistant.util.dt."""
97+
98+
_now: _dt.datetime = _DEFAULT_NOW
99+
100+
def now(self) -> _dt.datetime:
101+
return self._now
102+
103+
@staticmethod
104+
def as_local(dt: _dt.datetime) -> _dt.datetime:
105+
return dt # treat everything as UTC in tests
106+
107+
108+
dt_util_mock = _DtUtilMock()
109+
110+
_ha_util = MagicMock()
111+
_ha_util.dt = dt_util_mock # `from homeassistant.util import dt` resolves here
112+
113+
114+
# ── Register all stubs ───────────────────────────────────────────────────────
115+
116+
sys.modules.update(
117+
{
118+
"homeassistant": MagicMock(),
119+
"homeassistant.core": _ha_core,
120+
"homeassistant.config_entries": MagicMock(),
121+
"homeassistant.const": MagicMock(),
122+
"homeassistant.helpers": MagicMock(),
123+
"homeassistant.helpers.storage": _ha_storage_mod,
124+
"homeassistant.helpers.event": _ha_event,
125+
"homeassistant.helpers.update_coordinator": _ha_coordinator,
126+
"homeassistant.helpers.config_validation": MagicMock(),
127+
"homeassistant.util": _ha_util,
128+
"homeassistant.util.dt": dt_util_mock,
129+
# Stub the frontend sub-module so __init__.py's relative import succeeds
130+
# without executing frontend.py (which has its own heavy HA dependencies).
131+
"custom_components.taskmate.frontend": MagicMock(),
132+
# voluptuous is used by __init__.py for service schemas
133+
"voluptuous": MagicMock(),
134+
}
135+
)
136+
137+
138+
# ---------------------------------------------------------------------------
139+
# Pytest fixtures
140+
# ---------------------------------------------------------------------------
141+
142+
@pytest.fixture
143+
def hass():
144+
"""Return a fresh FakeHass instance."""
145+
return FakeHass()
146+
147+
148+
@pytest.fixture
149+
def event_loop():
150+
"""Provide a fresh asyncio event loop per test."""
151+
loop = asyncio.new_event_loop()
152+
yield loop
153+
loop.close()
154+
155+
156+
def run_async(coro, loop=None):
157+
"""Run a coroutine synchronously in tests."""
158+
if loop is None:
159+
loop = asyncio.new_event_loop()
160+
try:
161+
return loop.run_until_complete(coro)
162+
finally:
163+
loop.close()
164+
return loop.run_until_complete(coro)

0 commit comments

Comments
 (0)