Fix flaky fire_callbacks tests in casper_glow (#167418)

This commit is contained in:
Mike O'Driscoll
2026-04-06 06:06:03 -04:00
committed by GitHub
parent 310af5a31a
commit 4ad2f752a3
5 changed files with 36 additions and 34 deletions

View File

@@ -1,6 +1,6 @@
"""Casper Glow session fixtures."""
from collections.abc import Callable, Generator
from collections.abc import Awaitable, Callable, Generator
from unittest.mock import MagicMock, patch
from pycasperglow import GlowState
@@ -53,15 +53,17 @@ def mock_casper_glow() -> Generator[MagicMock]:
@pytest.fixture
def fire_callbacks(
hass: HomeAssistant,
mock_casper_glow: MagicMock,
) -> Callable[[GlowState], None]:
) -> Callable[[GlowState], Awaitable[None]]:
"""Return a helper that fires all registered device callbacks with a given state."""
def _fire(state: GlowState) -> None:
async def _fire(state: GlowState) -> None:
for cb in (
call[0][0] for call in mock_casper_glow.register_callback.call_args_list
):
cb(state)
await hass.async_block_till_done()
return _fire

View File

@@ -1,6 +1,6 @@
"""Test the Casper Glow binary sensor platform."""
from collections.abc import Callable
from collections.abc import Awaitable, Callable
from unittest.mock import MagicMock, patch
from pycasperglow import GlowState
@@ -43,7 +43,7 @@ async def test_paused_state_update(
hass: HomeAssistant,
mock_casper_glow: MagicMock,
mock_config_entry: MockConfigEntry,
fire_callbacks: Callable[[GlowState], None],
fire_callbacks: Callable[[GlowState], Awaitable[None]],
is_paused: bool,
expected_state: str,
) -> None:
@@ -53,7 +53,7 @@ async def test_paused_state_update(
):
await setup_integration(hass, mock_config_entry)
fire_callbacks(GlowState(is_paused=is_paused))
await fire_callbacks(GlowState(is_paused=is_paused))
state = hass.states.get(PAUSED_ENTITY_ID)
assert state is not None
assert state.state == expected_state
@@ -63,7 +63,7 @@ async def test_paused_ignores_none_state(
hass: HomeAssistant,
mock_casper_glow: MagicMock,
mock_config_entry: MockConfigEntry,
fire_callbacks: Callable[[GlowState], None],
fire_callbacks: Callable[[GlowState], Awaitable[None]],
) -> None:
"""Test that a callback with is_paused=None does not overwrite the state."""
with patch(
@@ -72,13 +72,13 @@ async def test_paused_ignores_none_state(
await setup_integration(hass, mock_config_entry)
# Set a known value first
fire_callbacks(GlowState(is_paused=True))
await fire_callbacks(GlowState(is_paused=True))
state = hass.states.get(PAUSED_ENTITY_ID)
assert state is not None
assert state.state == STATE_ON
# Callback with no is_paused data — state should remain unchanged
fire_callbacks(GlowState(is_on=True))
await fire_callbacks(GlowState(is_on=True))
state = hass.states.get(PAUSED_ENTITY_ID)
assert state is not None
assert state.state == STATE_ON
@@ -93,7 +93,7 @@ async def test_charging_state_update(
hass: HomeAssistant,
mock_casper_glow: MagicMock,
mock_config_entry: MockConfigEntry,
fire_callbacks: Callable[[GlowState], None],
fire_callbacks: Callable[[GlowState], Awaitable[None]],
is_charging: bool,
expected_state: str,
) -> None:
@@ -103,7 +103,7 @@ async def test_charging_state_update(
):
await setup_integration(hass, mock_config_entry)
fire_callbacks(GlowState(is_charging=is_charging))
await fire_callbacks(GlowState(is_charging=is_charging))
state = hass.states.get(CHARGING_ENTITY_ID)
assert state is not None
assert state.state == expected_state
@@ -113,7 +113,7 @@ async def test_charging_ignores_none_state(
hass: HomeAssistant,
mock_casper_glow: MagicMock,
mock_config_entry: MockConfigEntry,
fire_callbacks: Callable[[GlowState], None],
fire_callbacks: Callable[[GlowState], Awaitable[None]],
) -> None:
"""Test that a callback with is_charging=None does not overwrite the state."""
with patch(
@@ -122,13 +122,13 @@ async def test_charging_ignores_none_state(
await setup_integration(hass, mock_config_entry)
# Set a known value first
fire_callbacks(GlowState(is_charging=True))
await fire_callbacks(GlowState(is_charging=True))
state = hass.states.get(CHARGING_ENTITY_ID)
assert state is not None
assert state.state == STATE_ON
# Callback with no is_charging data — state should remain unchanged
fire_callbacks(GlowState(is_on=True))
await fire_callbacks(GlowState(is_on=True))
state = hass.states.get(CHARGING_ENTITY_ID)
assert state is not None
assert state.state == STATE_ON

View File

@@ -1,6 +1,6 @@
"""Test the Casper Glow light platform."""
from collections.abc import Callable
from collections.abc import Awaitable, Callable
from unittest.mock import MagicMock, patch
from pycasperglow import CasperGlowError, GlowState
@@ -83,19 +83,19 @@ async def test_turn_off(
async def test_state_update_via_callback(
hass: HomeAssistant,
config_entry: MockConfigEntry,
fire_callbacks: Callable[[GlowState], None],
fire_callbacks: Callable[[GlowState], Awaitable[None]],
) -> None:
"""Test that the entity updates state when the device fires a callback."""
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == STATE_UNKNOWN
fire_callbacks(GlowState(is_on=True))
await fire_callbacks(GlowState(is_on=True))
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == STATE_ON
fire_callbacks(GlowState(is_on=False))
await fire_callbacks(GlowState(is_on=False))
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == STATE_OFF
@@ -149,10 +149,10 @@ async def test_brightness_snap_to_nearest(
async def test_brightness_update_via_callback(
hass: HomeAssistant,
config_entry: MockConfigEntry,
fire_callbacks: Callable[[GlowState], None],
fire_callbacks: Callable[[GlowState], Awaitable[None]],
) -> None:
"""Test that brightness updates via device callback."""
fire_callbacks(GlowState(is_on=True, brightness_level=80))
await fire_callbacks(GlowState(is_on=True, brightness_level=80))
state = hass.states.get(ENTITY_ID)
assert state is not None
@@ -196,7 +196,7 @@ async def test_state_update_via_callback_after_command_failure(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_casper_glow: MagicMock,
fire_callbacks: Callable[[GlowState], None],
fire_callbacks: Callable[[GlowState], Awaitable[None]],
) -> None:
"""Test that device callbacks correctly update state even after a command failure."""
mock_casper_glow.turn_on.side_effect = CasperGlowError("Connection failed")
@@ -215,7 +215,7 @@ async def test_state_update_via_callback_after_command_failure(
assert state.state == STATE_UNKNOWN
# Device sends a push state update — entity reflects true device state
fire_callbacks(GlowState(is_on=True))
await fire_callbacks(GlowState(is_on=True))
state = hass.states.get(ENTITY_ID)
assert state is not None

View File

@@ -1,6 +1,6 @@
"""Test the Casper Glow select platform."""
from collections.abc import Callable
from collections.abc import Awaitable, Callable
from unittest.mock import MagicMock, patch
from pycasperglow import CasperGlowError, GlowState
@@ -44,14 +44,14 @@ async def test_entities(
async def test_select_state_from_callback(
hass: HomeAssistant,
config_entry: MockConfigEntry,
fire_callbacks: Callable[[GlowState], None],
fire_callbacks: Callable[[GlowState], Awaitable[None]],
) -> None:
"""Test that the select entity shows dimming time reported by device callback."""
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == STATE_UNKNOWN
fire_callbacks(
await fire_callbacks(
GlowState(configured_dimming_time_minutes=int(DIMMING_TIME_OPTIONS[2]))
)
@@ -64,7 +64,7 @@ async def test_select_option(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_casper_glow: MagicMock,
fire_callbacks: Callable[[GlowState], None],
fire_callbacks: Callable[[GlowState], Awaitable[None]],
) -> None:
"""Test selecting a dimming time option."""
await hass.services.async_call(
@@ -82,7 +82,7 @@ async def test_select_option(
assert state.state == DIMMING_TIME_OPTIONS[1]
# A subsequent device callback must not overwrite the user's selection.
fire_callbacks(
await fire_callbacks(
GlowState(configured_dimming_time_minutes=int(DIMMING_TIME_OPTIONS[0]))
)
@@ -118,7 +118,7 @@ async def test_select_state_update_via_callback_after_command_failure(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_casper_glow: MagicMock,
fire_callbacks: Callable[[GlowState], None],
fire_callbacks: Callable[[GlowState], Awaitable[None]],
) -> None:
"""Test that device callbacks correctly update state even after a command failure."""
mock_casper_glow.set_brightness_and_dimming_time.side_effect = CasperGlowError(
@@ -138,7 +138,7 @@ async def test_select_state_update_via_callback_after_command_failure(
assert state.state == STATE_UNKNOWN
# Device sends a push state update — entity reflects true state
fire_callbacks(
await fire_callbacks(
GlowState(configured_dimming_time_minutes=int(DIMMING_TIME_OPTIONS[1]))
)
@@ -150,10 +150,10 @@ async def test_select_state_update_via_callback_after_command_failure(
async def test_select_ignores_remaining_time_updates(
hass: HomeAssistant,
config_entry: MockConfigEntry,
fire_callbacks: Callable[[GlowState], None],
fire_callbacks: Callable[[GlowState], Awaitable[None]],
) -> None:
"""Test that callbacks with only remaining time do not change the select state."""
fire_callbacks(GlowState(dimming_time_remaining_ms=2_640_000))
await fire_callbacks(GlowState(dimming_time_remaining_ms=2_640_000))
state = hass.states.get(ENTITY_ID)
assert state is not None

View File

@@ -1,6 +1,6 @@
"""Test the Casper Glow sensor platform."""
from collections.abc import Callable
from collections.abc import Awaitable, Callable
from unittest.mock import MagicMock, patch
from pycasperglow import BatteryLevel, GlowState
@@ -59,13 +59,13 @@ async def test_battery_state_updated_via_callback(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_casper_glow: MagicMock,
fire_callbacks: Callable[[GlowState], None],
fire_callbacks: Callable[[GlowState], Awaitable[None]],
) -> None:
"""Test battery sensor updates when a device callback fires."""
with patch("homeassistant.components.casper_glow.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry)
fire_callbacks(GlowState(battery_level=BatteryLevel.PCT_50))
await fire_callbacks(GlowState(battery_level=BatteryLevel.PCT_50))
state = hass.states.get(BATTERY_ENTITY_ID)
assert state is not None