diff --git a/tests/components/hdc2080/common.yaml b/tests/components/hdc2080/common.yaml index cb14cb183b..48c6de2cd5 100644 --- a/tests/components/hdc2080/common.yaml +++ b/tests/components/hdc2080/common.yaml @@ -1,5 +1,6 @@ sensor: - platform: hdc2080 + i2c_id: i2c_bus temperature: name: Temperature humidity: diff --git a/tests/integration/fixtures/scheduler_interval_reschedule.yaml b/tests/integration/fixtures/scheduler_interval_reschedule.yaml index 91a8f38849..a0f882deee 100644 --- a/tests/integration/fixtures/scheduler_interval_reschedule.yaml +++ b/tests/integration/fixtures/scheduler_interval_reschedule.yaml @@ -25,6 +25,13 @@ globals: - id: callback_timeout_fired type: bool initial_value: "false" + # Track set_interval replacing itself from within callback + - id: replace_count + type: int + initial_value: "0" + - id: replaced_count + type: int + initial_value: "0" interval: # Fast interval: 50ms @@ -81,3 +88,26 @@ interval: // Stop this interval after scheduling the timeout id(timeout_creator)->stop_poller(); } + + # Interval that calls set_interval with the same name from within its callback, + # replacing itself. Tests that the old item (marked removed) is not rescheduled + # via push_heap, and the new item goes through to_add_ correctly. + - interval: 60ms + id: replace_test + then: + - lambda: |- + id(replace_count) += 1; + if (id(replace_count) == 1) { + ESP_LOGI("test", "REPLACE_ORIGINAL_FIRE"); + // Replace the polling interval with a named interval on the same component + id(replace_test)->set_interval("replaced_interval", 80, []() { + id(replaced_count) += 1; + ESP_LOGI("test", "REPLACED_FIRE count=%d", id(replaced_count)); + if (id(replaced_count) >= 3) { + id(replace_test)->cancel_interval("replaced_interval"); + ESP_LOGI("test", "REPLACED_STOPPED"); + } + }); + // Stop the original polling interval + id(replace_test)->stop_poller(); + } diff --git a/tests/integration/test_scheduler_interval_reschedule.py b/tests/integration/test_scheduler_interval_reschedule.py index 701cbfa65c..d141bd9f15 100644 --- a/tests/integration/test_scheduler_interval_reschedule.py +++ b/tests/integration/test_scheduler_interval_reschedule.py @@ -9,6 +9,7 @@ Key scenarios tested: 2. Heap ordering is preserved — faster intervals fire proportionally more often 3. An interval that cancels itself mid-callback is not rescheduled 4. A timeout scheduled from within an interval callback (to_add_ path) still works +5. An interval that replaces itself via set_interval from within its callback """ from __future__ import annotations @@ -36,11 +37,14 @@ async def test_scheduler_interval_reschedule( slow_3_future: asyncio.Future[tuple[int, int]] = loop.create_future() self_cancel_stopped_future: asyncio.Future[None] = loop.create_future() callback_timeout_future: asyncio.Future[None] = loop.create_future() + replace_original_future: asyncio.Future[None] = loop.create_future() + replaced_stopped_future: asyncio.Future[None] = loop.create_future() self_cancel_fire_count = 0 + replaced_fire_count = 0 def on_log_line(line: str) -> None: - nonlocal self_cancel_fire_count + nonlocal self_cancel_fire_count, replaced_fire_count if "FAST_10_REACHED" in line and not fast_10_future.done(): fast_10_future.set_result(None) @@ -63,6 +67,16 @@ async def test_scheduler_interval_reschedule( if "CALLBACK_TIMEOUT_FIRED" in line and not callback_timeout_future.done(): callback_timeout_future.set_result(None) + if "REPLACE_ORIGINAL_FIRE" in line and not replace_original_future.done(): + replace_original_future.set_result(None) + + match = re.search(r"REPLACED_FIRE count=(\d+)", line) + if match: + replaced_fire_count = int(match.group(1)) + + if "REPLACED_STOPPED" in line and not replaced_stopped_future.done(): + replaced_stopped_future.set_result(None) + async with ( run_compiled(yaml_config, line_callback=on_log_line), api_client_connected() as client, @@ -128,3 +142,24 @@ async def test_scheduler_interval_reschedule( await asyncio.wait_for(callback_timeout_future, timeout=5.0) except TimeoutError: pytest.fail("Timeout scheduled from interval callback did not fire") + + # 6. Interval that replaces itself via set_interval from within callback + # The original fires once, sets up a new named interval, then stops itself. + # The replacement interval should fire 3 times then cancel itself. + try: + await asyncio.wait_for(replace_original_future, timeout=5.0) + except TimeoutError: + pytest.fail("Replace-test original interval did not fire") + + try: + await asyncio.wait_for(replaced_stopped_future, timeout=5.0) + except TimeoutError: + pytest.fail( + f"Replaced interval did not stop. Fired {replaced_fire_count} times" + ) + + # Wait to ensure replacement doesn't fire again after cancellation + await asyncio.sleep(0.3) + assert replaced_fire_count == 3, ( + f"Replaced interval fired {replaced_fire_count} times, expected exactly 3" + )