mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
201 lines
7.0 KiB
Python
201 lines
7.0 KiB
Python
"""Tests for the openthread component."""
|
|
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from esphome.components.esp32 import (
|
|
KEY_ESP32,
|
|
KEY_SDKCONFIG_OPTIONS,
|
|
KEY_VARIANT,
|
|
VARIANT_ESP32C6,
|
|
)
|
|
from esphome.components.openthread import CONFIG_SCHEMA, OT_HEADER_DIR, OT_HEADER_FILE
|
|
from esphome.components.openthread.const import OPENTHREAD_CONFIG_PREFIX
|
|
import esphome.config_validation as cv
|
|
from esphome.const import PlatformFramework, Toolchain
|
|
from esphome.core import CORE, HexInt
|
|
from esphome.yaml_util import Sensitive
|
|
from tests.component_tests.types import SetCoreConfigCallable
|
|
|
|
NETWORK_KEY = 0xDFD34F0F05CAD978EC4E32B0413038FF
|
|
PSKC = 0xC23A76E98F1A6483639B1AC1271E2E27
|
|
TLV = "0e080000000000010000000300001435060004001fffe0"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("config", "field", "expected"),
|
|
[
|
|
({"network_key": hex(NETWORK_KEY)}, "network_key", NETWORK_KEY),
|
|
({"network_key": hex(NETWORK_KEY), "pskc": hex(PSKC)}, "pskc", PSKC),
|
|
({"tlv": TLV}, "tlv", TLV),
|
|
],
|
|
)
|
|
def test_credentials_are_sensitive(
|
|
config: dict[str, str],
|
|
field: str,
|
|
expected: int | str,
|
|
set_core_config: SetCoreConfigCallable,
|
|
) -> None:
|
|
"""Thread credentials are tagged for redaction without changing their value
|
|
or the hexadecimal integer type used to build sdkconfig options."""
|
|
set_core_config(
|
|
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32C6}
|
|
)
|
|
validated = CONFIG_SCHEMA(config)
|
|
assert isinstance(validated[field], Sensitive)
|
|
assert validated[field] == expected
|
|
assert isinstance(validated[field], HexInt) == isinstance(expected, int)
|
|
|
|
|
|
def _generate_in(
|
|
tmp_path: Path, yaml_text: str, generate_main: Callable[[str | Path], str]
|
|
) -> tuple[Path, MagicMock]:
|
|
"""Generate from ``yaml_text`` written to tmp_path, so the build directory lands there.
|
|
|
|
Returns the header path and the mocked ``clean_build``.
|
|
"""
|
|
config_path = tmp_path / "test.yaml"
|
|
config_path.write_text(yaml_text, encoding="utf-8")
|
|
with patch("esphome.components.openthread.clean_build") as clean_build:
|
|
generate_main(config_path)
|
|
return CORE.relative_build_path(OT_HEADER_DIR, OT_HEADER_FILE), clean_build
|
|
|
|
|
|
def test_core_options_written_to_header(
|
|
tmp_path: Path,
|
|
generate_main: Callable[[str | Path], str],
|
|
component_config_path: Callable[[str], Path],
|
|
) -> None:
|
|
header, _ = _generate_in(
|
|
tmp_path,
|
|
component_config_path("custom_header.yaml").read_text(encoding="utf-8"),
|
|
generate_main,
|
|
)
|
|
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
|
assert sdkconfig["CONFIG_OPENTHREAD_HEADER_CUSTOM"] is True
|
|
assert sdkconfig["CONFIG_OPENTHREAD_CUSTOM_HEADER_PATH"] == OT_HEADER_DIR
|
|
assert sdkconfig["CONFIG_OPENTHREAD_CUSTOM_HEADER_FILE_NAME"] == OT_HEADER_FILE
|
|
assert header.read_text(encoding="utf-8") == (
|
|
"// Auto-generated by ESPHome from openthread core_options - do not edit\n"
|
|
"#pragma once\n\n"
|
|
"#define OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE 1\n"
|
|
"#define OPENTHREAD_CONFIG_MAC_DEFAULT_MAX_FRAME_RETRIES_DIRECT 5\n"
|
|
)
|
|
|
|
|
|
def test_no_header_options_no_header(
|
|
tmp_path: Path,
|
|
generate_main: Callable[[str | Path], str],
|
|
component_config_path: Callable[[str], Path],
|
|
) -> None:
|
|
header, clean_build = _generate_in(
|
|
tmp_path,
|
|
component_config_path("no_custom_header.yaml").read_text(encoding="utf-8"),
|
|
generate_main,
|
|
)
|
|
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
|
assert "CONFIG_OPENTHREAD_HEADER_CUSTOM" not in sdkconfig
|
|
assert not header.exists()
|
|
clean_build.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("toolchain", "cleans"),
|
|
[(Toolchain.PLATFORMIO, True), (Toolchain.ESP_IDF, False)],
|
|
)
|
|
def test_header_change_cleans_only_on_platformio(
|
|
toolchain: Toolchain,
|
|
cleans: bool,
|
|
tmp_path: Path,
|
|
generate_main: Callable[[str | Path], str],
|
|
component_config_path: Callable[[str], Path],
|
|
) -> None:
|
|
source = component_config_path("custom_header.yaml").read_text(encoding="utf-8")
|
|
CORE.toolchain = toolchain
|
|
_, clean_build = _generate_in(tmp_path, source, generate_main)
|
|
assert clean_build.called is cleans
|
|
# Unchanged content: no clean on either toolchain.
|
|
CORE.reset()
|
|
CORE.toolchain = toolchain
|
|
_, clean_build = _generate_in(tmp_path, source, generate_main)
|
|
clean_build.assert_not_called()
|
|
# A changed value leaves sdkconfig as it was, so only this clean rebuilds OpenThread.
|
|
changed = source.replace('RETRIES_DIRECT: "5"', 'RETRIES_DIRECT: "7"')
|
|
assert changed != source
|
|
CORE.reset()
|
|
CORE.toolchain = toolchain
|
|
header, clean_build = _generate_in(tmp_path, changed, generate_main)
|
|
assert clean_build.called is cleans
|
|
assert (
|
|
"#define OPENTHREAD_CONFIG_MAC_DEFAULT_MAX_FRAME_RETRIES_DIRECT 7\n"
|
|
in header.read_text(encoding="utf-8")
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"name",
|
|
[
|
|
"CONFIG_OPENTHREAD_JOINER",
|
|
"openthread_config_x",
|
|
"OT_CONFIG_X",
|
|
OPENTHREAD_CONFIG_PREFIX,
|
|
],
|
|
)
|
|
def test_core_option_without_prefix_rejected(
|
|
name: str, set_core_config: SetCoreConfigCallable
|
|
) -> None:
|
|
set_core_config(
|
|
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32C6}
|
|
)
|
|
with pytest.raises(cv.Invalid, match=OPENTHREAD_CONFIG_PREFIX):
|
|
CONFIG_SCHEMA({"network_key": hex(NETWORK_KEY), "core_options": {name: "1"}})
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"name",
|
|
["OPENTHREAD_CONFIG_a", "OPENTHREAD_CONFIG_A B", "OPENTHREAD_CONFIG_A-B"],
|
|
)
|
|
def test_core_option_bad_characters_rejected(
|
|
name: str, set_core_config: SetCoreConfigCallable
|
|
) -> None:
|
|
set_core_config(
|
|
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32C6}
|
|
)
|
|
with pytest.raises(cv.Invalid, match="may only contain A-Z, 0-9 and _"):
|
|
CONFIG_SCHEMA({"network_key": hex(NETWORK_KEY), "core_options": {name: "1"}})
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "message"),
|
|
[
|
|
("", "must not be empty"),
|
|
(" ", "must not be empty"),
|
|
("1\n#define X 2", "must be a single line"),
|
|
("1\r2", "must be a single line"),
|
|
("1 \\", "must not end with"),
|
|
("1 \\ ", "must not end with"),
|
|
],
|
|
)
|
|
def test_core_option_bad_value_rejected(
|
|
value: str, message: str, set_core_config: SetCoreConfigCallable
|
|
) -> None:
|
|
set_core_config(
|
|
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32C6}
|
|
)
|
|
with pytest.raises(cv.Invalid, match=message):
|
|
CONFIG_SCHEMA(
|
|
{
|
|
"network_key": hex(NETWORK_KEY),
|
|
"core_options": {"OPENTHREAD_CONFIG_X": value},
|
|
}
|
|
)
|
|
|
|
|
|
def test_core_options_only_on_esp32(set_core_config: SetCoreConfigCallable) -> None:
|
|
set_core_config(PlatformFramework.NRF52_ZEPHYR)
|
|
with pytest.raises(cv.Invalid, match="ESP32"):
|
|
CONFIG_SCHEMA({"tlv": TLV, "core_options": {"OPENTHREAD_CONFIG_X": "1"}})
|