1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-11 07:42:26 +01:00
Files
esphome/tests/integration/test_loop_disable_enable.py
J. Nick Koston 80a8f1437e tests
2025-06-15 19:38:13 -05:00

172 lines
6.4 KiB
Python

"""Integration test for loop disable/enable functionality."""
from __future__ import annotations
import asyncio
import logging
from pathlib import Path
from typing import Any
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
_LOGGER = logging.getLogger(__name__)
@pytest.mark.asyncio
async def test_loop_disable_enable(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that components can disable and enable their loop() method."""
# Get the absolute path to the external components directory
external_components_path = str(
Path(__file__).parent / "fixtures" / "external_components"
)
# Replace the placeholder in the YAML config with the actual path
yaml_config = yaml_config.replace(
"EXTERNAL_COMPONENT_PATH", external_components_path
)
log_messages: list[tuple[int, str]] = []
def on_log(msg: Any) -> None:
"""Capture log messages."""
if hasattr(msg, "level") and hasattr(msg, "message"):
log_messages.append((msg.level, msg.message.decode("utf-8")))
_LOGGER.info(f"ESPHome log: [{msg.level}] {msg.message.decode('utf-8')}")
# Write, compile and run the ESPHome device, then connect to API
async with run_compiled(yaml_config), api_client_connected() as client:
# Subscribe to logs (not awaitable)
client.subscribe_logs(on_log)
# Wait for the component to run through its test sequence
# The component should:
# 1. Try to disable/enable in setup (before calculate_looping_components_)
# 2. Run loop 50 times then disable itself
# 3. Not run loop again after disabling
await asyncio.sleep(5.0) # Give it time to run
# Debug: Print all captured logs
_LOGGER.info(f"Total logs captured: {len(log_messages)}")
for level, msg in log_messages[:20]: # First 20 logs
_LOGGER.info(f"Log: {msg}")
# Analyze captured logs
setup_logs = [msg for level, msg in log_messages if "setup()" in msg]
loop_logs = [msg for level, msg in log_messages if "Loop count:" in msg]
disable_logs = [msg for level, msg in log_messages if "Disabling loop" in msg]
error_logs = [msg for level, msg in log_messages if "ERROR" in msg]
# Verify setup was called
assert len(setup_logs) > 0, "Component setup() was not called"
# Verify loop was called multiple times
assert len(loop_logs) > 0, "Component loop() was never called"
# Extract loop counts from logs
loop_counts = []
for _, msg in loop_logs:
# Parse "Loop count: X" messages
if "Loop count:" in msg:
try:
count = int(msg.split("Loop count:")[1].strip())
loop_counts.append(count)
except (ValueError, IndexError):
pass
# Verify loop ran exactly 50 times before disabling
assert max(loop_counts) == 50, (
f"Expected max loop count 50, got {max(loop_counts)}"
)
# Verify disable message was logged
assert any(
"Disabling loop after 50 iterations" in msg for _, msg in disable_logs
), "Component did not log disable message"
# Verify no errors (loop should not be called after disable)
assert len(error_logs) == 0, f"Found error logs: {error_logs}"
# Wait a bit more to ensure loop doesn't continue
await asyncio.sleep(2.0)
# Re-check - should still be no errors
error_logs_2 = [msg for level, msg in log_messages if "ERROR" in msg]
assert len(error_logs_2) == 0, f"Found error logs after wait: {error_logs_2}"
# The final loop count should still be 50
final_loop_logs = [msg for _, msg in log_messages if "Loop count:" in msg]
final_counts = []
for msg in final_loop_logs:
if "Loop count:" in msg:
try:
count = int(msg.split("Loop count:")[1].strip())
final_counts.append(count)
except (ValueError, IndexError):
pass
assert max(final_counts) == 50, (
f"Loop continued after disable! Max count: {max(final_counts)}"
)
@pytest.mark.asyncio
async def test_loop_disable_enable_reentrant(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that disable_loop is reentrant (component can disable itself during its own loop)."""
# Get the absolute path to the external components directory
external_components_path = str(
Path(__file__).parent / "fixtures" / "external_components"
)
# Replace the placeholder in the YAML config with the actual path
yaml_config = yaml_config.replace(
"EXTERNAL_COMPONENT_PATH", external_components_path
)
# The basic test above already tests this - the component disables itself
# during its own loop() call at iteration 50
# This test just verifies that specific behavior more explicitly
log_messages: list[tuple[int, str]] = []
def on_log(msg: Any) -> None:
"""Capture log messages."""
if hasattr(msg, "level") and hasattr(msg, "message"):
log_messages.append((msg.level, msg.message.decode("utf-8")))
async with run_compiled(yaml_config), api_client_connected() as client:
client.subscribe_logs(on_log)
await asyncio.sleep(5.0)
# Look for the sequence: Loop count 50 -> Disable message -> No more loops
found_50 = False
found_disable = False
found_51_error = False
for i, (_, msg) in enumerate(log_messages):
if "Loop count: 50" in msg:
found_50 = True
# Check next few messages for disable
for j in range(i, min(i + 5, len(log_messages))):
if "Disabling loop after 50 iterations" in log_messages[j][1]:
found_disable = True
break
elif "Loop count: 51" in msg or "ERROR" in msg:
found_51_error = True
assert found_50, "Component did not reach loop count 50"
assert found_disable, "Component did not disable itself at count 50"
assert not found_51_error, (
"Component continued looping after disable or had errors"
)