mirror of
https://github.com/esphome/esphome.git
synced 2025-09-11 07:42:26 +01:00
tests
This commit is contained in:
171
tests/integration/test_loop_disable_enable.py
Normal file
171
tests/integration/test_loop_disable_enable.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""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"
|
||||
)
|
Reference in New Issue
Block a user