Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions tests/components/airq/test_integration_stalling_regression.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be in test_init.py, to be consistent with most of the codebase.
Also, we should not interact directly with the coordinator or other integration internals: https://developers.home-assistant.io/docs/development_testing/#writing-tests-for-integrations
Can we refactor this to avoid interacting with the internals?

Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Test that the coordinator does not hang with unresponding server.

Users reported that the integration was stalling silently at random intervals.
This was identified to be due to an incorrect timeout in aioairq and was fixed
in aioairq==0.4.7 and homeassistant==2025.10.3.
This is the regression test.
"""

import asyncio
import time

import pytest
import pytest_asyncio

from homeassistant.components.airq import AirQCoordinator
from homeassistant.components.airq.const import DOMAIN
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
from homeassistant.core import HomeAssistant

from tests.common import MockConfigEntry

TIMEOUT_DEFAULT = 15
TIMEOUT_HANGING = TIMEOUT_DEFAULT * 3
TIMEOUT_SAFETY = TIMEOUT_DEFAULT * 2
IP = "127.0.0.1"


@pytest_asyncio.fixture
async def hanging_server():
"""TCP server that accepts connections but never sends data.

This makes HTTP clients wait forever for the status line / headers.
"""

async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
try:
# Keep the connection open; don't read or write HTTP data.
await asyncio.sleep(TIMEOUT_HANGING)
finally:
writer.close()

# pick any free ephemeral port on localhost and bind the server to it
server = await asyncio.start_server(handler, host=IP, port=0)
# get the actual selected port
port = server.sockets[0].getsockname()[1]
try:
yield (IP, port)
finally:
server.close()


@pytest.mark.usefixtures("socket_enabled")
async def test_coordinator_timeout_with_hanging_device(
hass: HomeAssistant,
hanging_server,
) -> None:
"""Test that AirQCoordinator times out by itself instead of getting stuck."""
host, port = hanging_server

# Create config entry with the actual hanging server address
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_IP_ADDRESS: f"{host}:{port}", # Use actual port!
CONF_PASSWORD: "test_password",
},
unique_id="test_hanging_device",
)

# Create coordinator
coordinator = AirQCoordinator(hass, config_entry)

started = time.perf_counter()

with pytest.raises(asyncio.TimeoutError):
async with asyncio.timeout(TIMEOUT_SAFETY):
await coordinator._async_update_data()

elapsed = time.perf_counter() - started

assert elapsed < TIMEOUT_DEFAULT + 1, (
f"Expected ~{TIMEOUT_DEFAULT}s, got {elapsed:.1f}s (aioairq==0.4.6 behavior)"
)
Loading