Files
esphome/tests/integration/test_loop_interval_decoupling.py
T
J. Nick Koston 7d12b984a8 [core] address review polish on main-loop decoupling
Doc and test updates from a code review of this PR:

- Correct the `tail_us == 0 on Phase A-only ticks` claim in the
  Application::loop() comment and the RuntimeStatsCollector::record_loop_active
  docstring. `loop_tail_start_us` is set to `loop_before_end_us`, and
  `loop_now_us` is sampled later, so `tail_us` on Phase A-only ticks is
  the small gate-check + record prefix — tiny but non-zero.
  (Also flagged by Copilot on application.h:623 and runtime_stats.h:45.)

- Call out ESP8266 as the floor case in the WDT_FEED_INTERVAL_MS margin
  table. Its soft WDT (~1.6 s) is the tightest margin at ~5x, so future
  changes to the constant need to preserve comfortable headroom there.

- Tighten the test lower bound at tests/integration/test_loop_interval_decoupling.py
  from `2 <= loop_delta <= 6` to `3 <= loop_delta <= 6`. Allowing 2 would
  let a >50% slowdown from the 4-in-2s nominal pass as CI jitter, which
  undermines the regression signal. 3 keeps the test honest while still
  absorbing realistic CI jitter.

- Add a second integration test
  (test_loop_interval_default_not_pulled_forward) that covers the inverse
  direction: at the default loop_interval_ with a fast scheduler item
  (5 ms — well under the old delay_time/2 = 8 ms floor), the component
  phase must still run at ~62 Hz, not the pre-fix ~128 Hz. This locks
  down the original 128 Hz → 62 Hz regression that motivated the PR.
2026-04-18 17:32:29 -05:00

76 lines
3.0 KiB
Python

"""Test that loop_interval_ no longer clamps scheduler cadence.
Regression test for the decoupling of Application::loop() component-phase
cadence from scheduler wake timing.
Setup:
- App.set_loop_interval(500) — raised for power-savings style cadence
- Scheduler interval at 50ms — should fire at 50ms regardless of loop_interval_
- Component loop (LoopTestComponent) — should run at 500ms cadence
Before the decoupling fix the old `std::max(next_schedule, delay_time / 2)`
floor clamped the sleep to ~250ms, so the 50ms scheduler only fired ~8 times
per 2s (vs the ~40 expected). After the fix the scheduler fires close to its
requested cadence while the component phase stays gated at loop_interval_.
"""
from __future__ import annotations
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_loop_interval_decoupling(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Raised loop_interval_ must not clamp scheduler item cadence."""
loop = asyncio.get_running_loop()
measurement_done: asyncio.Future[tuple[int, int]] = loop.create_future()
def on_log_line(line: str) -> None:
match = re.search(r"MEASUREMENT_DONE loop_delta=(\d+) sched_delta=(\d+)", line)
if match and not measurement_done.done():
measurement_done.set_result((int(match.group(1)), int(match.group(2))))
async with (
run_compiled(yaml_config, line_callback=on_log_line),
api_client_connected() as client,
):
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "loop-interval-decouple"
try:
loop_delta, sched_delta = await asyncio.wait_for(
measurement_done, timeout=10.0
)
except TimeoutError:
pytest.fail("MEASUREMENT_DONE marker never appeared")
# Observation window = 2s, loop_interval_ = 500ms.
# Component phase should fire ~4 times in 2s. The upper bound must be
# less than 8: the pre-decoupling behavior clamped to ~250ms cadence
# giving ~8 loops/2s, so allowing 8 would let the old behavior pass.
# Lower bound 3 (not 2) keeps the test honest: a >30% slowdown from
# the ~4 nominal is not normal CI jitter and should fail.
assert 3 <= loop_delta <= 6, (
f"Component loop should fire ~4 times in 2s at loop_interval=500ms, "
f"got {loop_delta}"
)
# Scheduler interval = 50ms → ~40 fires in 2s. Before the decoupling
# fix this clamped to ~8 fires. Assert >= 20 to catch the old clamped
# behavior with comfortable jitter headroom for slow CI hosts.
assert sched_delta >= 20, (
f"50ms scheduler interval should fire ~40 times in 2s but only "
f"fired {sched_delta}. This indicates loop_interval_ is still "
f"clamping scheduler cadence."
)