1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-20 02:48:57 +00:00

Fix flaky camera stream teardown (#158507)

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
This commit is contained in:
Paul Tarjan
2025-12-17 02:47:22 -10:00
committed by GitHub
parent 48d671ad5f
commit 080ec3524b
2 changed files with 45 additions and 56 deletions

View File

@@ -148,6 +148,22 @@ def mock_stream_source_fixture() -> Generator[AsyncMock]:
yield mock_stream_source
@pytest.fixture(name="mock_create_stream")
def mock_create_stream_fixture() -> Generator[Mock]:
"""Fixture to mock create_stream and prevent real stream threads."""
mock_stream = Mock()
mock_stream.add_provider = Mock()
mock_stream.start = AsyncMock()
mock_stream.endpoint_url = Mock(return_value="http://home.assistant/playlist.m3u8")
mock_stream.set_update_callback = Mock()
mock_stream.available = True
with patch(
"homeassistant.components.camera.create_stream",
return_value=mock_stream,
):
yield mock_stream
@pytest.fixture
async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None:
"""Initialize test WebRTC cameras with native RTC support."""

View File

@@ -346,20 +346,14 @@ async def test_websocket_stream_no_source(
@pytest.mark.usefixtures("mock_camera", "mock_stream")
async def test_websocket_camera_stream(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_create_stream: Mock
) -> None:
"""Test camera/stream websocket command."""
await async_setup_component(hass, "camera", {})
with (
patch(
"homeassistant.components.camera.Stream.endpoint_url",
return_value="http://home.assistant/playlist.m3u8",
) as mock_stream_view_url,
patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
return_value="http://example.com",
),
with patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
return_value="http://example.com",
):
# Request playlist through WebSocket
client = await hass_ws_client(hass)
@@ -369,7 +363,7 @@ async def test_websocket_camera_stream(
msg = await client.receive_json()
# Assert WebSocket response
assert mock_stream_view_url.called
assert mock_create_stream.endpoint_url.called
assert msg["id"] == 6
assert msg["type"] == TYPE_RESULT
assert msg["success"]
@@ -505,21 +499,18 @@ async def test_play_stream_service_no_source(hass: HomeAssistant) -> None:
@pytest.mark.usefixtures("mock_camera", "mock_stream")
async def test_handle_play_stream_service(hass: HomeAssistant) -> None:
async def test_handle_play_stream_service(
hass: HomeAssistant, mock_create_stream: Mock
) -> None:
"""Test camera play_stream service."""
await async_process_ha_core_config(
hass,
{"external_url": "https://example.com"},
)
await async_setup_component(hass, "media_player", {})
with (
patch(
"homeassistant.components.camera.Stream.endpoint_url",
) as mock_request_stream,
patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
return_value="http://example.com",
),
with patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
return_value="http://example.com",
):
# Call service
await hass.services.async_call(
@@ -533,17 +524,14 @@ async def test_handle_play_stream_service(hass: HomeAssistant) -> None:
)
# So long as we request the stream, the rest should be covered
# by the play_media service tests.
assert mock_request_stream.called
assert mock_create_stream.endpoint_url.called
@pytest.mark.usefixtures("mock_stream")
async def test_no_preload_stream(hass: HomeAssistant) -> None:
async def test_no_preload_stream(hass: HomeAssistant, mock_create_stream: Mock) -> None:
"""Test camera preload preference."""
demo_settings = camera.DynamicStreamSettings()
with (
patch(
"homeassistant.components.camera.Stream.endpoint_url",
) as mock_request_stream,
patch(
"homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings",
return_value=demo_settings,
@@ -557,15 +545,14 @@ async def test_no_preload_stream(hass: HomeAssistant) -> None:
await async_setup_component(hass, "camera", {DOMAIN: {"platform": "demo"}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert not mock_request_stream.called
assert not mock_create_stream.endpoint_url.called
@pytest.mark.usefixtures("mock_stream")
async def test_preload_stream(hass: HomeAssistant) -> None:
async def test_preload_stream(hass: HomeAssistant, mock_create_stream: Mock) -> None:
"""Test camera preload preference."""
demo_settings = camera.DynamicStreamSettings(preload_stream=True)
with (
patch("homeassistant.components.camera.create_stream") as mock_create_stream,
patch(
"homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings",
return_value=demo_settings,
@@ -575,14 +562,13 @@ async def test_preload_stream(hass: HomeAssistant) -> None:
return_value="http://example.com",
),
):
mock_create_stream.return_value.start = AsyncMock()
assert await async_setup_component(
hass, "camera", {DOMAIN: {"platform": "demo"}}
)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert mock_create_stream.called
assert mock_create_stream.start.called
@pytest.mark.usefixtures("mock_camera")
@@ -694,25 +680,16 @@ async def test_state_streaming(hass: HomeAssistant) -> None:
assert demo_camera.state == camera.CameraState.STREAMING
@pytest.mark.usefixtures("mock_camera", "mock_stream")
@pytest.mark.usefixtures("mock_camera", "mock_stream", "mock_create_stream")
async def test_stream_unavailable(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_create_stream: Mock
) -> None:
"""Camera state."""
await async_setup_component(hass, "camera", {})
with (
patch(
"homeassistant.components.camera.Stream.endpoint_url",
return_value="http://home.assistant/playlist.m3u8",
),
patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
return_value="http://example.com",
),
patch(
"homeassistant.components.camera.Stream.set_update_callback",
) as mock_update_callback,
with patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source",
return_value="http://example.com",
):
# Request playlist through WebSocket. We just want to create the stream
# but don't care about the result.
@@ -721,26 +698,22 @@ async def test_stream_unavailable(
{"id": 10, "type": "camera/stream", "entity_id": "camera.demo_camera"}
)
await client.receive_json()
assert mock_update_callback.called
assert mock_create_stream.set_update_callback.called
# Simulate the stream going unavailable
callback = mock_update_callback.call_args.args[0]
with patch(
"homeassistant.components.camera.Stream.available", new_callable=lambda: False
):
callback()
await hass.async_block_till_done()
callback = mock_create_stream.set_update_callback.call_args.args[0]
mock_create_stream.available = False
callback()
await hass.async_block_till_done()
demo_camera = hass.states.get("camera.demo_camera")
assert demo_camera is not None
assert demo_camera.state == STATE_UNAVAILABLE
# Simulate stream becomes available
with patch(
"homeassistant.components.camera.Stream.available", new_callable=lambda: True
):
callback()
await hass.async_block_till_done()
mock_create_stream.available = True
callback()
await hass.async_block_till_done()
demo_camera = hass.states.get("camera.demo_camera")
assert demo_camera is not None