This commit is contained in:
J. Nick Koston
2026-02-21 21:12:31 -06:00
parent 38058d0308
commit d21d897766
4 changed files with 47 additions and 43 deletions
+3 -3
View File
@@ -601,7 +601,7 @@ async def build_condition_list(
return conditions
def has_deferred_actions(actions: ConfigType) -> bool:
def has_non_synchronous_actions(actions: ConfigType) -> bool:
"""Check if a validated action list contains any non-synchronous actions.
Non-synchronous actions (delay, wait_until, script.wait, etc.) store
@@ -609,13 +609,13 @@ def has_deferred_actions(actions: ConfigType) -> bool:
unsafe. Actions that haven't been audited default to non-synchronous.
"""
if isinstance(actions, list):
return any(has_deferred_actions(item) for item in actions)
return any(has_non_synchronous_actions(item) for item in actions)
if isinstance(actions, dict):
for key in actions:
if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous:
return True
return any(
has_deferred_actions(v)
has_non_synchronous_actions(v)
for v in actions.values()
if isinstance(v, (list, dict))
)
+1 -1
View File
@@ -382,7 +382,7 @@ async def to_code(config: ConfigType) -> None:
# Check if action chain has non-synchronous actions that would make
# non-owning StringRef dangle (rx_buf_ reused after delay)
has_non_synchronous = automation.has_deferred_actions(
has_non_synchronous = automation.has_non_synchronous_actions(
conf.get(CONF_THEN, [])
)
+2
View File
@@ -84,6 +84,8 @@ class StringRef {
/// Write a null terminator at base_[len_] in-place.
/// Caller must guarantee that the byte at base_[len_] is writable memory
/// (e.g., the RX_BUF_NULL_TERMINATOR byte reserved by frame helpers after decode).
/// Marked const because StringRef itself is not modified; the underlying buffer
/// (owned by frame helper rx_buf_) is mutated via const_cast.
void null_terminate_in_place() const { const_cast<char *>(base_)[len_] = '\0'; }
/// Find first occurrence of substring, returns std::string::npos if not found.
+41 -39
View File
@@ -5,7 +5,7 @@ from unittest.mock import patch
import pytest
from esphome.automation import has_deferred_actions
from esphome.automation import has_non_synchronous_actions
from esphome.util import RegistryEntry
@@ -33,82 +33,84 @@ def mock_registry() -> Generator[dict[str, RegistryEntry]]:
yield registry
def test_has_deferred_actions_empty_list(
def test_has_non_synchronous_actions_empty_list(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_deferred_actions([]) is False
assert has_non_synchronous_actions([]) is False
def test_has_deferred_actions_empty_dict(
def test_has_non_synchronous_actions_empty_dict(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_deferred_actions({}) is False
assert has_non_synchronous_actions({}) is False
def test_has_deferred_actions_non_dict_non_list(
def test_has_non_synchronous_actions_non_dict_non_list(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_deferred_actions("string") is False
assert has_deferred_actions(42) is False
assert has_deferred_actions(None) is False
assert has_non_synchronous_actions("string") is False
assert has_non_synchronous_actions(42) is False
assert has_non_synchronous_actions(None) is False
def test_has_deferred_actions_delay(mock_registry: dict[str, RegistryEntry]) -> None:
assert has_deferred_actions([{"delay": "1s"}]) is True
def test_has_deferred_actions_wait_until(
def test_has_non_synchronous_actions_delay(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_deferred_actions([{"wait_until": {"condition": {}}}]) is True
assert has_non_synchronous_actions([{"delay": "1s"}]) is True
def test_has_deferred_actions_script_wait(
def test_has_non_synchronous_actions_wait_until(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_deferred_actions([{"script.wait": "script_id"}]) is True
assert has_non_synchronous_actions([{"wait_until": {"condition": {}}}]) is True
def test_has_deferred_actions_non_deferred(
def test_has_non_synchronous_actions_script_wait(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_deferred_actions([{"logger.log": "hello"}]) is False
assert has_non_synchronous_actions([{"script.wait": "script_id"}]) is True
def test_has_deferred_actions_unknown_not_in_registry(
def test_has_non_synchronous_actions_synchronous(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_non_synchronous_actions([{"logger.log": "hello"}]) is False
def test_has_non_synchronous_actions_unknown_not_in_registry(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""Unknown actions not in registry are not flagged (only registered actions count)."""
assert has_deferred_actions([{"unknown.action": "value"}]) is False
assert has_non_synchronous_actions([{"unknown.action": "value"}]) is False
def test_has_deferred_actions_default_non_synchronous(
def test_has_non_synchronous_actions_default_non_synchronous(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""Actions registered without explicit synchronous=True default to non-synchronous."""
mock_registry["some.action"] = RegistryEntry(
"some.action", lambda: None, None, None
)
assert has_deferred_actions([{"some.action": "value"}]) is True
assert has_non_synchronous_actions([{"some.action": "value"}]) is True
def test_has_deferred_actions_nested_in_then(
def test_has_non_synchronous_actions_nested_in_then(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""Deferred action nested inside a non-deferred action's then block."""
"""Non-synchronous action nested inside a synchronous action's then block."""
actions: list[dict[str, object]] = [
{
"logger.log": "first",
"then": [{"delay": "1s"}],
}
]
assert has_deferred_actions(actions) is True
assert has_non_synchronous_actions(actions) is True
def test_has_deferred_actions_deeply_nested(
def test_has_non_synchronous_actions_deeply_nested(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""Deferred action deeply nested in action structure."""
"""Non-synchronous action deeply nested in action structure."""
actions: list[dict[str, object]] = [
{
"if": {
@@ -119,13 +121,13 @@ def test_has_deferred_actions_deeply_nested(
}
}
]
assert has_deferred_actions(actions) is True
assert has_non_synchronous_actions(actions) is True
def test_has_deferred_actions_no_deferred_in_nested(
def test_has_non_synchronous_actions_none_in_nested(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""No deferred actions even with nesting."""
"""No non-synchronous actions even with nesting."""
actions: list[dict[str, object]] = [
{
"if": {
@@ -135,14 +137,14 @@ def test_has_deferred_actions_no_deferred_in_nested(
}
}
]
assert has_deferred_actions(actions) is False
assert has_non_synchronous_actions(actions) is False
def test_has_deferred_actions_multiple_one_deferred(
def test_has_non_synchronous_actions_multiple_one_non_synchronous(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert (
has_deferred_actions(
has_non_synchronous_actions(
[
{"logger.log": "first"},
{"delay": "1s"},
@@ -153,11 +155,11 @@ def test_has_deferred_actions_multiple_one_deferred(
)
def test_has_deferred_actions_multiple_none_deferred(
def test_has_non_synchronous_actions_multiple_all_synchronous(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert (
has_deferred_actions(
has_non_synchronous_actions(
[
{"logger.log": "first"},
{"logger.log": "second"},
@@ -167,9 +169,9 @@ def test_has_deferred_actions_multiple_none_deferred(
)
def test_has_deferred_actions_dict_input(
def test_has_non_synchronous_actions_dict_input(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""Direct dict input (single action)."""
assert has_deferred_actions({"delay": "1s"}) is True
assert has_deferred_actions({"logger.log": "hello"}) is False
assert has_non_synchronous_actions({"delay": "1s"}) is True
assert has_non_synchronous_actions({"logger.log": "hello"}) is False