[uart] clock_source option (#16486)

Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Ivan Kruglov
2026-09-24 15:16:40 +01:00
committed by GitHub
co-authored by J. Nick Koston
parent 071fc08e32
commit 7dc98c683b
7 changed files with 237 additions and 21 deletions
@@ -0,0 +1,112 @@
"""Tests for UART clock source validation and code generation."""
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.components.esp32 import KEY_VARIANT, VARIANTS
from esphome.components.uart import (
CONF_CLOCK_SOURCE,
CONFIG_SCHEMA,
UART_CLOCK_SOURCES,
UART_CLOCK_SOURCES_BY_VARIANT,
)
import esphome.config_validation as cv
from esphome.const import PlatformFramework
from tests.component_tests.types import SetCoreConfigCallable
@pytest.fixture
def clock_source_validator() -> cv.All:
"""Find the clock validator without depending on pre-validator ordering."""
schema = next(
validator
for validator in CONFIG_SCHEMA.validators
if isinstance(validator, cv.Schema)
)
return schema.schema[cv.Optional(CONF_CLOCK_SOURCE)]
def test_clock_source_table_covers_all_variants() -> None:
"""New ESP32 variants must declare their supported UART clocks."""
assert set(UART_CLOCK_SOURCES_BY_VARIANT) == set(VARIANTS)
for sources in UART_CLOCK_SOURCES_BY_VARIANT.values():
assert "DEFAULT" in sources
assert set(sources) <= UART_CLOCK_SOURCES.keys()
@pytest.mark.parametrize(
("variant", "source", "supported"),
[
("ESP32", "REF_TICK", True),
("ESP32", "XTAL", False),
("ESP32S2", "REF_TICK", True),
("ESP32S2", "RTC", False),
("ESP32C3", "APB", True),
("ESP32C3", "xtal", True),
("ESP32C3", "REF_TICK", False),
("ESP32S3", "RTC", True),
("ESP32S3", "REF_TICK", False),
("ESP32C2", "APB", False),
("ESP32C6", "APB", False),
("ESP32C6", "DEFAULT", True),
("ESP32C5", "RTC", True),
("ESP32C61", "XTAL", True),
("ESP32H4", "RTC", True),
("ESP32H21", "XTAL", True),
("ESP32S31", "RTC", True),
("ESP32H2", "XTAL", True),
("ESP32P4", "RTC", True),
("ESP32C3", "INVALID", False),
],
)
def test_clock_source_validation(
variant: str,
source: str,
supported: bool,
set_core_config: SetCoreConfigCallable,
clock_source_validator: cv.All,
) -> None:
"""Reject unsupported clocks and normalize accepted names."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_VARIANT: variant},
)
if supported:
assert clock_source_validator(source) == source.upper()
else:
with pytest.raises(cv.Invalid):
clock_source_validator(source)
def test_clock_source_requires_esp32(
set_core_config: SetCoreConfigCallable,
clock_source_validator: cv.All,
) -> None:
"""Reject the ESP32-only option before accessing the chip variant."""
set_core_config(PlatformFramework.ESP8266_ARDUINO)
with pytest.raises(cv.Invalid, match="ESP32"):
clock_source_validator("DEFAULT")
@pytest.mark.parametrize("source", [None, "default", "xtal"])
def test_clock_source_codegen(
source: str | None,
tmp_path: Path,
generate_main: Callable[[str | Path], str],
) -> None:
"""Emit IDF constants directly and skip the setter when nothing changes."""
config = tmp_path / "uart.yaml"
config.write_text(
"esphome:\n name: uart-clock-test\n"
"esp32:\n variant: esp32c3\n framework:\n type: esp-idf\n"
"uart:\n id: test_uart\n tx_pin: GPIO4\n baud_rate: 9600\n"
+ (f" clock_source: {source}\n" if source else ""),
encoding="utf-8",
)
main_cpp = generate_main(config)
if source in (None, "default"):
assert "set_clock_source(" not in main_cpp
else:
assert f"test_uart->set_clock_source(::UART_SCLK_{source.upper()});" in main_cpp
@@ -24,5 +24,6 @@ uart:
tx_pin: 18
rx_pin: 19
baud_rate: 115200
clock_source: XTAL
debug:
debug_prefix: "[UART1] "
@@ -28,6 +28,7 @@ uart:
tx_pin: 21
rx_pin: 22
baud_rate: 115200
clock_source: REF_TICK
debug:
debug_prefix: "[UART1] "
- id: uart_debug_custom
+37 -17
View File
@@ -248,6 +248,41 @@ def test_convert_keys_bare_callable_dotted_qualname() -> None:
assert set(converted["schema"]["config_vars"]) == {"string"}
@pytest.fixture(scope="module")
def language_schema_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""Run the full language-schema build once and return the output directory.
The build must run in a fresh interpreter: ``build_language_schema.py``
enables schema extraction *before* importing any esphome component, and the
extraction hooks are no-ops if the components were already imported (as they
are inside the pytest session). Running it as a subprocess mirrors how CI
generates the schema and keeps these tests isolated from import order.
"""
out_dir = tmp_path_factory.mktemp("language_schema")
subprocess.run(
[sys.executable, str(SCRIPT_PATH), "--output-path", str(out_dir)],
check=True,
capture_output=True,
text=True,
)
return out_dir
def test_uart_clock_source_preserves_variant_metadata(
language_schema_dir: Path,
) -> None:
"""UART clock choices retain chip restrictions in the editor schema."""
uart_schema = json.loads((language_schema_dir / "uart.json").read_text())
clock_source = uart_schema["uart"]["schemas"]["CONFIG_SCHEMA"]["schema"][
"config_vars"
]["clock_source"]
assert clock_source["type"] == "enum"
assert clock_source["values"]["REF_TICK"] == {"variants": ["ESP32", "ESP32S2"]}
assert "ESP32C6" in clock_source["values"]["XTAL"]["variants"]
assert "ESP32C6" not in clock_source["values"]["APB"]["variants"]
# ---------------------------------------------------------------------------
# Regression tests for the lvgl schema dump.
#
@@ -261,23 +296,8 @@ def test_convert_keys_bare_callable_dotted_qualname() -> None:
@pytest.fixture(scope="module")
def lvgl_schema(tmp_path_factory: pytest.TempPathFactory) -> dict:
"""Run the full language-schema build once and return parsed lvgl.json.
The build must run in a fresh interpreter: ``build_language_schema.py``
enables schema extraction *before* importing any esphome component, and the
extraction hooks are no-ops if the components were already imported (as they
are inside the pytest session). Running it as a subprocess mirrors how CI
generates the schema and keeps this test isolated from import order.
"""
out_dir = tmp_path_factory.mktemp("language_schema")
subprocess.run(
[sys.executable, str(SCRIPT_PATH), "--output-path", str(out_dir)],
check=True,
capture_output=True,
text=True,
)
return json.loads((out_dir / "lvgl.json").read_text())
def lvgl_schema(language_schema_dir: Path) -> dict:
return json.loads((language_schema_dir / "lvgl.json").read_text())
def _lvgl_config_vars(lvgl_schema: dict) -> dict: