mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 11:26:39 +00:00
Merge branch 'neutral-ble-client' into radon-eye-single-node
This commit is contained in:
@@ -40,6 +40,9 @@ class SerialProxy {
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
void write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {}
|
||||
SerialProxyResult set_mode_from_client(api::APIConnection *api_connection, api::enums::SerialProxyMode mode) {
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
SerialProxyResult set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ def test_neutral_cap_check_guards_future_hub_platforms(
|
||||
# Both current platforms defer to their stack budgets; pin the message
|
||||
# and boundary of the branch a future budget-less hub platform takes.
|
||||
set_core_config(PlatformFramework.RP2_ARDUINO)
|
||||
monkeypatch.setattr(bluetooth_connection, "_STACK_BUDGET_PLATFORMS", set())
|
||||
monkeypatch.setattr(bluetooth_connection, "_STACK_BUDGETS", {})
|
||||
bluetooth_connection.consume_gatt_slot("bluetooth_proxy", 3)({})
|
||||
bluetooth_connection.FINAL_VALIDATE_SCHEMA({})
|
||||
bluetooth_connection.consume_gatt_slot("ble_client")({})
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"""The codegen hook external filtering components use to turn on the filter slot."""
|
||||
|
||||
from esphome.components import bluetooth_proxy
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def test_enable_advertisement_filter_emits_define() -> None:
|
||||
"""External components call this rather than emitting the define."""
|
||||
bluetooth_proxy.enable_advertisement_filter()
|
||||
|
||||
assert "USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER" in {
|
||||
define.name for define in CORE.defines
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
"""Tests for the bridge cdc_acm_uart platform's final validation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.cdc_acm_uart import bridge
|
||||
from esphome.components.cdc_acm_uart.bridge import CONF_USB_CDC_ACM_ID
|
||||
from esphome.config import Config
|
||||
from esphome.const import CONF_DEBUG, CONF_ID, CONF_UART_ID, PlatformFramework
|
||||
from esphome.core import ID
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
_final_validate = bridge._final_validate
|
||||
|
||||
|
||||
def _set_esp32_s3(set_core_config: SetCoreConfigCallable, **kwargs) -> None:
|
||||
from esphome.components.esp32 import KEY_VARIANT, VARIANT_ESP32S3
|
||||
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_VARIANT: VARIANT_ESP32S3},
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def _full_config(uarts: list[ConfigType] | None = None, **domains) -> Config:
|
||||
"""A full config declaring uart_0 and uart_1 (plus any extra entries), as the ID
|
||||
pass leaves it, so the debug check can resolve a uart_id to its declaration."""
|
||||
uarts = uarts or [{CONF_ID: ID("uart_0")}, {CONF_ID: ID("uart_1")}]
|
||||
full = Config()
|
||||
full["uart"] = uarts
|
||||
for index, uart_conf in enumerate(uarts):
|
||||
full.declare_ids.append((uart_conf[CONF_ID], ["uart", index, CONF_ID]))
|
||||
full.update(domains)
|
||||
return full
|
||||
|
||||
|
||||
def _bridge_config(uart_id: str, cdc_id: str) -> dict:
|
||||
return {CONF_UART_ID: ID(uart_id), CONF_USB_CDC_ACM_ID: ID(cdc_id)}
|
||||
|
||||
|
||||
def test_accepts_distinct_uart_and_cdc_interfaces(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_set_esp32_s3(set_core_config, full_config=_full_config())
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
_final_validate(_bridge_config("uart_1", "cdc_acm_2"))
|
||||
|
||||
|
||||
def test_rejects_two_bridges_sharing_a_uart(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_set_esp32_s3(set_core_config, full_config=_full_config())
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
with pytest.raises(cv.Invalid, match="already bridged"):
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_2"))
|
||||
|
||||
|
||||
def test_rejects_two_bridges_sharing_a_cdc_interface(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_set_esp32_s3(set_core_config, full_config=_full_config())
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
with pytest.raises(cv.Invalid, match="already bridged"):
|
||||
_final_validate(_bridge_config("uart_1", "cdc_acm_1"))
|
||||
|
||||
|
||||
def test_rejects_uart_shared_with_another_component(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_set_esp32_s3(
|
||||
set_core_config,
|
||||
full_config=_full_config(
|
||||
sensor=[{"platform": "pzemac", CONF_UART_ID: ID("uart_0")}],
|
||||
),
|
||||
)
|
||||
with pytest.raises(cv.Invalid, match="exclusive"):
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
|
||||
|
||||
def test_rejects_cdc_interface_shared_with_another_component(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
# The CDC instance is itself a uart::UARTComponent, so other components can bind
|
||||
# it as a plain UART via uart_id -- that must be rejected just like UART sharing.
|
||||
_set_esp32_s3(
|
||||
set_core_config,
|
||||
full_config=_full_config(
|
||||
sensor=[{"platform": "pzemac", CONF_UART_ID: ID("cdc_acm_1")}],
|
||||
),
|
||||
)
|
||||
with pytest.raises(cv.Invalid, match="exclusive"):
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
|
||||
|
||||
def test_rejects_uart_referenced_from_nested_config(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
# References can sit arbitrarily deep, e.g. inside an automation's action list.
|
||||
_set_esp32_s3(
|
||||
set_core_config,
|
||||
full_config=_full_config(
|
||||
binary_sensor=[
|
||||
{
|
||||
"platform": "gpio",
|
||||
"on_press": [{"then": [{CONF_UART_ID: ID("uart_0")}]}],
|
||||
}
|
||||
],
|
||||
),
|
||||
)
|
||||
with pytest.raises(cv.Invalid, match="exclusive"):
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
|
||||
|
||||
def test_ignores_other_components_on_other_uarts(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_set_esp32_s3(
|
||||
set_core_config,
|
||||
full_config=_full_config(
|
||||
sensor=[{"platform": "pzemac", CONF_UART_ID: ID("uart_1")}],
|
||||
# The bridge domain itself is skipped: this bridge's own entry (and any
|
||||
# bridge-vs-bridge sharing, which the seen-set already rejects) must not
|
||||
# trip the exclusivity scan.
|
||||
bridge=[_bridge_config("uart_0", "cdc_acm_1")],
|
||||
),
|
||||
)
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
|
||||
|
||||
def test_rejects_debug_on_bridged_uart(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
# The bridge talks to the IDF driver directly, so the uart debugger would see
|
||||
# nothing and its dummy_receiver would steal RX bytes.
|
||||
_set_esp32_s3(
|
||||
set_core_config,
|
||||
full_config=_full_config(uarts=[{CONF_ID: ID("uart_0"), CONF_DEBUG: {}}]),
|
||||
)
|
||||
with pytest.raises(cv.Invalid, match="debug"):
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
|
||||
|
||||
def test_allows_debug_on_other_uart(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_set_esp32_s3(
|
||||
set_core_config,
|
||||
full_config=_full_config(
|
||||
uarts=[{CONF_ID: ID("uart_0")}, {CONF_ID: ID("uart_1"), CONF_DEBUG: {}}]
|
||||
),
|
||||
)
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Power-user fields are marked as advanced on the shared schemas.
|
||||
|
||||
``filters``, ``manual_ip`` and the GPIO switch interlock options are knobs
|
||||
whose defaults suit nearly every user, so a schema-aware editor should keep
|
||||
them behind its "advanced settings" disclosure rather than on the main form.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import binary_sensor, ethernet, sensor, text_sensor, wifi
|
||||
import esphome.config_validation as cv
|
||||
|
||||
|
||||
def _markers(schema: cv.Schema) -> dict[str, object]:
|
||||
s = schema
|
||||
if hasattr(s, "validators"):
|
||||
# cv.All -> the schema is the first validator.
|
||||
s = s.validators[0]
|
||||
return {str(k): k for k in s.schema}
|
||||
|
||||
|
||||
def _gpio_switch_schema() -> cv.Schema:
|
||||
return importlib.import_module("esphome.components.gpio.switch").CONFIG_SCHEMA
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("label", "schema_factory", "fields"),
|
||||
[
|
||||
("sensor", sensor.sensor_schema, ["filters"]),
|
||||
("binary_sensor", binary_sensor.binary_sensor_schema, ["filters"]),
|
||||
("text_sensor", text_sensor.text_sensor_schema, ["filters"]),
|
||||
("wifi_network", lambda: wifi.WIFI_NETWORK_BASE, ["manual_ip"]),
|
||||
("wifi", lambda: wifi.CONFIG_SCHEMA, ["manual_ip"]),
|
||||
("ethernet", lambda: ethernet.BASE_SCHEMA, ["manual_ip"]),
|
||||
("gpio_switch", _gpio_switch_schema, ["interlock", "interlock_wait_time"]),
|
||||
],
|
||||
)
|
||||
def test_power_user_fields_are_advanced(
|
||||
label: str, schema_factory, fields: list[str]
|
||||
) -> None:
|
||||
markers = _markers(schema_factory())
|
||||
for field in fields:
|
||||
assert markers[field].visibility is cv.Visibility.ADVANCED, f"{label}.{field}"
|
||||
|
||||
|
||||
def test_interlock_wait_time_keeps_its_default() -> None:
|
||||
"""Marking the field advanced must not drop its default."""
|
||||
markers = _markers(_gpio_switch_schema())
|
||||
assert markers["interlock_wait_time"].default() == "0ms"
|
||||
@@ -60,7 +60,7 @@ def reset_core() -> Generator[None]:
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_full_config() -> Generator[None]:
|
||||
"""Give each test a clean final-validate config and restore it after."""
|
||||
token = final_validate.full_config.set({})
|
||||
token = final_validate.full_config.set(Config())
|
||||
yield
|
||||
final_validate.full_config.reset(token)
|
||||
|
||||
@@ -75,7 +75,7 @@ def set_core_config() -> Generator[SetCoreConfigCallable]:
|
||||
*,
|
||||
core_data: ConfigType | None = None,
|
||||
platform_data: ConfigType | None = None,
|
||||
full_config: dict[str, ConfigType] | None = None,
|
||||
full_config: dict[str, ConfigType] | Config | None = None,
|
||||
) -> None:
|
||||
platform, framework = platform_framework.value
|
||||
|
||||
@@ -94,7 +94,12 @@ def set_core_config() -> Generator[SetCoreConfigCallable]:
|
||||
CORE.data[platform.value] = platform_data
|
||||
|
||||
config.path_context.set([])
|
||||
final_validate.full_config.set(full_config or Config())
|
||||
# Production always installs a Config (a FinalValidateConfig), never a plain dict.
|
||||
if not isinstance(full_config, Config):
|
||||
full = Config()
|
||||
full.update(full_config or {})
|
||||
full_config = full
|
||||
final_validate.full_config.set(full_config)
|
||||
|
||||
yield setter
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
http_request:
|
||||
verify_ssl: true
|
||||
@@ -0,0 +1,19 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
|
||||
openthread:
|
||||
channel: 13
|
||||
network_name: OpenThread-8f28
|
||||
network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
|
||||
pan_id: 0x8f28
|
||||
ext_pan_id: 0xd63e8e3e495ebbc3
|
||||
pskc: 0xc23a76e98f1a6483639b1ac1271e2e27
|
||||
mesh_local_prefix: fd53:145f:ed22:ad81::/64
|
||||
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
disable_mbedtls_tls_server: false
|
||||
disable_mbedtls_tls_extras: false
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
http_request:
|
||||
verify_ssl: true
|
||||
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT: y
|
||||
CONFIG_MBEDTLS_CCM_C: y
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
http_request:
|
||||
verify_ssl: true
|
||||
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
eap:
|
||||
identity: "user@example.org"
|
||||
username: "user"
|
||||
password: "secret"
|
||||
|
||||
http_request:
|
||||
verify_ssl: true
|
||||
@@ -0,0 +1,7 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
framework:
|
||||
type: esp-idf
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
enable_full_printf: true
|
||||
@@ -11,9 +11,12 @@ import pytest
|
||||
|
||||
from esphome.components.esp32 import (
|
||||
KEY_FATFS_REQUIRED,
|
||||
KEY_MBEDTLS_TLS_EXTRAS_REQUIRED,
|
||||
KEY_MBEDTLS_TLS_SERVER_REQUIRED,
|
||||
KEY_VFS_DIR_REQUIRED,
|
||||
KEY_VFS_SELECT_REQUIRED,
|
||||
KEY_VFS_TERMIOS_REQUIRED,
|
||||
MBEDTLS_TLS_EXTRA_OPTIONS,
|
||||
VARIANT_ESP32,
|
||||
VARIANTS,
|
||||
NetworkSdkconfigData,
|
||||
@@ -1339,3 +1342,99 @@ def test_esp32_s31_gpio_validation(
|
||||
with caplog.at_level("WARNING"):
|
||||
validate_supports(pin)
|
||||
assert "GPIO36 is a strapping PIN" in caplog.text
|
||||
|
||||
|
||||
_TLS_SERVER_OPTIONS = (
|
||||
"CONFIG_MBEDTLS_TLS_CLIENT_ONLY",
|
||||
"CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "server", "extras"),
|
||||
[
|
||||
pytest.param("mbedtls_tls_default.yaml", (True, False), False, id="default"),
|
||||
pytest.param("mbedtls_tls_opt_out.yaml", (None, None), None, id="opt_out"),
|
||||
pytest.param("mbedtls_tls_wifi_eap.yaml", (True, False), None, id="wifi_eap"),
|
||||
],
|
||||
)
|
||||
def test_mbedtls_tls_trim_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
server: tuple[bool | None, bool | None],
|
||||
extras: bool | None,
|
||||
) -> None:
|
||||
"""Client-only TLS and the unused-feature trims apply unless opted out or required."""
|
||||
generate_main(component_config_path(config_file))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert tuple(sdkconfig.get(name) for name in _TLS_SERVER_OPTIONS) == server
|
||||
assert {sdkconfig.get(name) for name in MBEDTLS_TLS_EXTRA_OPTIONS} == {extras}
|
||||
|
||||
|
||||
_OPENTHREAD_EXTRAS = {"CONFIG_MBEDTLS_CCM_C", "CONFIG_MBEDTLS_ECDSA_DETERMINISTIC"}
|
||||
|
||||
|
||||
def test_mbedtls_tls_openthread_keeps_only_what_it_uses(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The OpenThread config keeps the DTLS server, CCM and deterministic ECDSA; the rest is trimmed."""
|
||||
generate_main(component_config_path("mbedtls_tls_openthread.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert tuple(sdkconfig.get(name) for name in _TLS_SERVER_OPTIONS) == (None, None)
|
||||
for name in MBEDTLS_TLS_EXTRA_OPTIONS:
|
||||
assert sdkconfig.get(name) is (None if name in _OPENTHREAD_EXTRAS else False)
|
||||
|
||||
|
||||
def test_mbedtls_tls_user_sdkconfig_wins(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A user-set TLS role member leaves the whole choice alone; other user values are kept."""
|
||||
generate_main(component_config_path("mbedtls_tls_user_sdkconfig.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_MBEDTLS_TLS_CLIENT_ONLY") is None
|
||||
role = sdkconfig["CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT"]
|
||||
assert isinstance(role, RawSdkconfigValue) and role.value == "y"
|
||||
ccm = sdkconfig["CONFIG_MBEDTLS_CCM_C"]
|
||||
assert isinstance(ccm, RawSdkconfigValue) and ccm.value == "y"
|
||||
assert {
|
||||
sdkconfig.get(name)
|
||||
for name in MBEDTLS_TLS_EXTRA_OPTIONS
|
||||
if name != "CONFIG_MBEDTLS_CCM_C"
|
||||
} == {False}
|
||||
|
||||
|
||||
def test_mbedtls_tls_openthread_requires_server_and_extras(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The OpenThread hooks mark the DTLS server and CCM/deterministic ECDSA as required."""
|
||||
generate_main(component_config_path("mbedtls_tls_openthread.yaml"))
|
||||
assert CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_SERVER_REQUIRED] is True
|
||||
assert CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_EXTRAS_REQUIRED] == _OPENTHREAD_EXTRAS
|
||||
|
||||
|
||||
_VASPRINTF_STUB_FLAGS = {"-Wl,--wrap=vasprintf", "-Wl,--undefined=__wrap_vasprintf"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "expected"),
|
||||
[
|
||||
pytest.param("vasprintf_stub_c6.yaml", True, id="c6"),
|
||||
pytest.param("vasprintf_stub_c6_full_printf.yaml", False, id="c6_full_printf"),
|
||||
pytest.param("exclusion_reincludes.yaml", False, id="esp32"),
|
||||
],
|
||||
)
|
||||
def test_vasprintf_stub_only_on_rom_vsnprintf_variants(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
"""The vasprintf wrap is emitted only where the ROM lacks vasprintf but has vsnprintf."""
|
||||
generate_main(component_config_path(config_file))
|
||||
assert (CORE.build_flags >= _VASPRINTF_STUB_FLAGS) is expected
|
||||
defines = {define.name for define in CORE.defines}
|
||||
assert ("USE_ESP32_VASPRINTF_STUB" in defines) is expected
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
# esp32_ble_server is only auto-loaded here, so it has no services of its own.
|
||||
esp32_improv:
|
||||
authorizer: none
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32
|
||||
|
||||
esp32_ble_server:
|
||||
id: ble_server
|
||||
manufacturer_data: [0x72, 0x04, 0x00, 0x23]
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32
|
||||
|
||||
esp32_ble_server:
|
||||
id: ble_server
|
||||
services:
|
||||
- uuid: 2a24b789-7aab-4535-af3e-ee76a35cc12d
|
||||
characteristics:
|
||||
- uuid: cad48e28-7fbe-41cf-bae9-d77a6c233423
|
||||
read: true
|
||||
value: [1, 2, 3, 4]
|
||||
@@ -1,5 +1,10 @@
|
||||
"""Tests for esp32_ble_server configuration helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32_ble_server import (
|
||||
@@ -45,3 +50,26 @@ def test_uuid_is_matches_descriptor_short_strings(uuid16) -> None:
|
||||
assert uuid_is(uuid16, uuid16)
|
||||
assert uuid_is(f"{uuid16:04X}", uuid16)
|
||||
assert uuid_is(f"{uuid16:08X}", uuid16)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "required"),
|
||||
[
|
||||
# Auto-loaded by esp32_improv only: nothing to find until Improv asks for it
|
||||
("improv_only.yaml", False),
|
||||
# The configuration defines a service clients are meant to connect to
|
||||
("own_service.yaml", True),
|
||||
# Manufacturer data is only useful if it is actually broadcast
|
||||
("manufacturer_data_only.yaml", True),
|
||||
],
|
||||
)
|
||||
def test_advertising_required(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
required: bool,
|
||||
) -> None:
|
||||
"""The server only requests advertising when the configuration needs it."""
|
||||
main_cpp = generate_main(component_config_path(config_file))
|
||||
|
||||
assert f"set_advertising_required({str(required).lower()})" in main_cpp
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
output:
|
||||
- platform: esp8266_pwm
|
||||
id: default_frequency
|
||||
pin: GPIO4
|
||||
frequency: 1kHz
|
||||
- platform: esp8266_pwm
|
||||
id: custom_frequency
|
||||
pin: GPIO5
|
||||
frequency: 2kHz
|
||||
- platform: esp8266_pwm
|
||||
id: schema_default_frequency
|
||||
pin: GPIO12
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Tests for the esp8266_pwm output codegen."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_default_frequency_is_not_emitted(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The 1 kHz default already lives in the C++ initializer."""
|
||||
main_cpp = generate_main(component_config_path("frequency.yaml"))
|
||||
|
||||
assert "default_frequency->set_frequency(" not in main_cpp
|
||||
assert "schema_default_frequency->set_frequency(" not in main_cpp
|
||||
assert "custom_frequency->set_frequency(2000.0f);" in main_cpp
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
output:
|
||||
- platform: ledc
|
||||
id: out_a
|
||||
pin: GPIO4
|
||||
- platform: ledc
|
||||
id: out_b
|
||||
pin: GPIO5
|
||||
|
||||
light:
|
||||
- platform: monochromatic
|
||||
id: plain_light
|
||||
output: out_a
|
||||
flash_transition_length: 0s
|
||||
- platform: monochromatic
|
||||
id: fancy_light
|
||||
output: out_b
|
||||
flash_transition_length: 500ms
|
||||
effects:
|
||||
- pulse:
|
||||
- platform: monochromatic
|
||||
id: bare_light
|
||||
output: out_a
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Tests that light codegen skips setters for default values."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_default_flash_length_and_empty_effects_are_not_emitted(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A 0 ms flash transition and an empty effect list match the C++ defaults."""
|
||||
main_cpp = generate_main(component_config_path("transitions.yaml"))
|
||||
|
||||
assert "plain_light->set_flash_transition_length(" not in main_cpp
|
||||
assert "plain_light->add_effects(" not in main_cpp
|
||||
assert "bare_light->set_flash_transition_length(" not in main_cpp
|
||||
assert "bare_light->add_effects(" not in main_cpp
|
||||
assert "fancy_light->set_flash_transition_length(500);" in main_cpp
|
||||
assert "fancy_light->add_effects({" in main_cpp
|
||||
@@ -52,3 +52,35 @@ def test_logger_pre_setup_before_other_components(generate_main):
|
||||
f"Component allocation '{alloc.group()}' at position {alloc.start()} "
|
||||
f"appears before logger pre_setup() at position {logger_pre_setup.start()}"
|
||||
)
|
||||
|
||||
|
||||
def test_default_uart_selection_is_not_emitted(generate_main):
|
||||
"""UART0 is the C++ initializer on ESP8266, so the setter is skipped."""
|
||||
main_cpp = generate_main("tests/component_tests/logger/test_logger.yaml")
|
||||
|
||||
assert "set_uart_selection(" not in main_cpp
|
||||
|
||||
|
||||
def test_custom_uart_selection_is_emitted(generate_main):
|
||||
"""A non default UART still reaches the setter before pre_setup()."""
|
||||
main_cpp = generate_main("tests/component_tests/logger/test_logger_uart1.yaml")
|
||||
|
||||
assert "set_uart_selection(logger::UART_SELECTION_UART1);" in main_cpp
|
||||
|
||||
|
||||
def test_libretiny_default_uart_selection_is_not_emitted(generate_main):
|
||||
"""DEFAULT is the C++ initializer on LibreTiny, so the setter is skipped."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/logger/test_logger_libretiny_default.yaml"
|
||||
)
|
||||
|
||||
assert "set_uart_selection(" not in main_cpp
|
||||
|
||||
|
||||
def test_libretiny_uart0_is_emitted(generate_main):
|
||||
"""UART0 is not the LibreTiny initializer, so it must still be set."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/logger/test_logger_libretiny_uart0.yaml"
|
||||
)
|
||||
|
||||
assert "set_uart_selection(logger::UART_SELECTION_UART0);" in main_cpp
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
rtl87xx:
|
||||
board: generic-rtl8710bn-2mb-788k
|
||||
|
||||
logger:
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
rtl87xx:
|
||||
board: generic-rtl8710bn-2mb-788k
|
||||
|
||||
logger:
|
||||
hardware_uart: UART0
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
logger:
|
||||
hardware_uart: UART1
|
||||
@@ -0,0 +1,32 @@
|
||||
esphome:
|
||||
name: test-keyboard-no-label
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
spi:
|
||||
- id: spi_bus
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
spi_id: spi_bus
|
||||
model: st7789v
|
||||
id: tft_display
|
||||
dimensions:
|
||||
width: 240
|
||||
height: 320
|
||||
cs_pin: GPIO22
|
||||
dc_pin: GPIO21
|
||||
auto_clear_enabled: false
|
||||
invert_colors: false
|
||||
update_interval: never
|
||||
|
||||
lvgl:
|
||||
displays: tft_display
|
||||
widgets:
|
||||
- keyboard:
|
||||
id: keyboard_widget
|
||||
@@ -0,0 +1,34 @@
|
||||
esphome:
|
||||
name: test-qrcode-no-label
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
spi:
|
||||
- id: spi_bus
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
spi_id: spi_bus
|
||||
model: st7789v
|
||||
id: tft_display
|
||||
dimensions:
|
||||
width: 240
|
||||
height: 320
|
||||
cs_pin: GPIO22
|
||||
dc_pin: GPIO21
|
||||
auto_clear_enabled: false
|
||||
invert_colors: false
|
||||
update_interval: never
|
||||
|
||||
lvgl:
|
||||
displays: tft_display
|
||||
widgets:
|
||||
- qrcode:
|
||||
id: qr_widget
|
||||
size: 100
|
||||
text: "esphome.io"
|
||||
@@ -0,0 +1,35 @@
|
||||
esphome:
|
||||
name: test-tabview-no-label
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
spi:
|
||||
- id: spi_bus
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
spi_id: spi_bus
|
||||
model: st7789v
|
||||
id: tft_display
|
||||
dimensions:
|
||||
width: 240
|
||||
height: 320
|
||||
cs_pin: GPIO22
|
||||
dc_pin: GPIO21
|
||||
auto_clear_enabled: false
|
||||
invert_colors: false
|
||||
update_interval: never
|
||||
|
||||
lvgl:
|
||||
displays: tft_display
|
||||
widgets:
|
||||
- tabview:
|
||||
id: tabview_widget
|
||||
tabs:
|
||||
- name: "Tab 1"
|
||||
id: tab_1
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Widgets whose LVGL C implementation creates or references labels
|
||||
internally (tab titles, key legends, the QR canvas fallback) must declare
|
||||
the label dependency in ``get_uses()``. Otherwise a config that contains
|
||||
no ``label`` widget of its own compiles LVGL without ``LV_USE_LABEL`` and
|
||||
fails at C compile time with undefined ``lv_label_*`` symbols.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.lvgl import defines as df
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"yaml_file",
|
||||
[
|
||||
"qrcode_no_label.yaml",
|
||||
"keyboard_no_label.yaml",
|
||||
"tabview_no_label.yaml",
|
||||
],
|
||||
)
|
||||
def test_label_less_config_enables_lv_use_label(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
yaml_file: str,
|
||||
) -> None:
|
||||
generate_main(component_config_path(yaml_file))
|
||||
assert "LV_USE_LABEL" in df.get_defines()
|
||||
@@ -5,11 +5,7 @@ from __future__ import annotations
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.noise import (
|
||||
decode_encryption_key,
|
||||
is_reserved_key,
|
||||
validate_encryption_key,
|
||||
)
|
||||
from esphome.components.noise import decode_encryption_key, validate_encryption_key
|
||||
|
||||
KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
||||
|
||||
@@ -41,6 +37,8 @@ def test_decode_encryption_key_rejects_short_decode() -> None:
|
||||
decode_encryption_key("AAECAw==")
|
||||
|
||||
|
||||
def test_is_reserved_key() -> None:
|
||||
assert is_reserved_key("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
|
||||
assert not is_reserved_key(KEY)
|
||||
def test_validate_encryption_key_rejects_all_zeros() -> None:
|
||||
"""The all-zeros key is the provisioning sentinel the device treats as no
|
||||
key, so it never reaches a build."""
|
||||
with pytest.raises(cv.Invalid, match="all-zeros key is reserved"):
|
||||
validate_encryption_key("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
number:
|
||||
- platform: template
|
||||
id: auto_number
|
||||
min_value: 0
|
||||
max_value: 10
|
||||
step: 1
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: box_number
|
||||
min_value: 0
|
||||
max_value: 10
|
||||
step: 1
|
||||
mode: box
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: explicit_auto_number
|
||||
min_value: 0
|
||||
max_value: 10
|
||||
step: 1
|
||||
mode: auto
|
||||
optimistic: true
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Tests for the number component codegen."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_default_mode_is_not_emitted(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Mode auto is the C++ initializer, so only a non default mode is set."""
|
||||
main_cpp = generate_main(component_config_path("mode.yaml"))
|
||||
|
||||
assert "auto_number->traits.set_mode(" not in main_cpp
|
||||
assert "explicit_auto_number->traits.set_mode(" not in main_cpp
|
||||
assert "box_number->traits.set_mode(number::NUMBER_MODE_BOX);" in main_cpp
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -14,6 +15,7 @@ from esphome.components.esphome.ota import (
|
||||
_validate_no_password_with_encryption,
|
||||
ota_esphome_final_validate,
|
||||
)
|
||||
from esphome.components.noise import static_encryption_key
|
||||
from esphome.const import (
|
||||
CONF_API,
|
||||
CONF_ENCRYPTION,
|
||||
@@ -115,7 +117,6 @@ def test_non_esphome_ota_unaffected() -> None:
|
||||
|
||||
API_KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
||||
OTHER_KEY = "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA="
|
||||
ZEROS_KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
|
||||
|
||||
def test_encryption_key_inherited_from_api() -> None:
|
||||
@@ -197,36 +198,6 @@ def test_encryption_without_any_key_rejected() -> None:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_encryption_explicit_all_zeros_key_rejected() -> None:
|
||||
"""The all-zeros key is the provisioning sentinel; the device would treat
|
||||
it as no PSK and accept plaintext, so it must fail validation."""
|
||||
full_conf = {
|
||||
CONF_OTA: [
|
||||
_make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: ZEROS_KEY}})
|
||||
],
|
||||
}
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with pytest.raises(cv.Invalid, match="all-zeros key is reserved"):
|
||||
ota_esphome_final_validate({})
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_encryption_inherited_all_zeros_key_rejected() -> None:
|
||||
"""An all-zeros api key must not silently disable ota encryption either."""
|
||||
full_conf = {
|
||||
CONF_API: {CONF_ENCRYPTION: {CONF_KEY: ZEROS_KEY}},
|
||||
CONF_OTA: [_make_ota_config(port=3232, **{CONF_ENCRYPTION: {}})],
|
||||
}
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with pytest.raises(cv.Invalid, match="all-zeros key is reserved"):
|
||||
ota_esphome_final_validate({})
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_encryption_key_mismatch_between_merged_configs_rejected() -> None:
|
||||
"""Same-port configs with different encryption keys raise."""
|
||||
full_conf = {
|
||||
@@ -295,13 +266,14 @@ def test_encryption_explicit_key_with_runtime_provisioned_api_accepted() -> None
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("component", ["web_server", "prometheus"])
|
||||
def test_encryption_with_web_server_ota_warns(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
caplog: pytest.LogCaptureFixture, component: str
|
||||
) -> None:
|
||||
"""With the web_server component the plaintext /update endpoint is always
|
||||
on; the combination validates with a warning."""
|
||||
"""web_server and prometheus keep the shared listener up, so the
|
||||
plaintext /update endpoint is always on and the combination warns."""
|
||||
full_conf = {
|
||||
"web_server": {},
|
||||
component: {},
|
||||
CONF_OTA: [
|
||||
_make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}),
|
||||
{CONF_PLATFORM: "web_server", CONF_ID: ID("ota_ws", is_manual=False)},
|
||||
@@ -316,12 +288,12 @@ def test_encryption_with_web_server_ota_warns(
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_encryption_with_captive_portal_web_server_ota_warns(
|
||||
def test_encryption_with_captive_portal_does_not_warn(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""captive_portal auto-loads the web_server ota platform without the
|
||||
web_server component; encryption stays usable and only warns, so the
|
||||
fallback AP recovery path is not lost."""
|
||||
web_server component; its endpoint only exists while the fallback AP is
|
||||
active and is the intended recovery path, so there is no warning."""
|
||||
full_conf = {
|
||||
"captive_portal": {},
|
||||
CONF_OTA: [
|
||||
@@ -333,7 +305,10 @@ def test_encryption_with_captive_portal_web_server_ota_warns(
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
ota_esphome_final_validate({})
|
||||
assert any("captive_portal" in record.message for record in caplog.records)
|
||||
assert not any(
|
||||
"OTA encryption does not cover" in record.message
|
||||
for record in caplog.records
|
||||
)
|
||||
esphome_conf = next(
|
||||
conf
|
||||
for conf in fv.full_config.get()[CONF_OTA]
|
||||
@@ -344,6 +319,100 @@ def test_encryption_with_captive_portal_web_server_ota_warns(
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_password_with_api_key_warns(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""A static api key makes the device offer encryption and the CLI take
|
||||
it, so the password is dead weight; the config validates with a warning."""
|
||||
full_conf = {
|
||||
CONF_API: {CONF_ENCRYPTION: {CONF_KEY: API_KEY}},
|
||||
CONF_OTA: [_make_ota_config(port=3232, **{CONF_PASSWORD: "pw"})],
|
||||
}
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
ota_esphome_final_validate({})
|
||||
assert any("wastes significant flash" in r.message for r in caplog.records)
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_password_with_runtime_api_key_warns_differently(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""The CLI still needs the password, but the provisioned key also
|
||||
authenticates uploads; the warning says so without the flash advice."""
|
||||
full_conf = {
|
||||
CONF_API: {CONF_ENCRYPTION: {}},
|
||||
CONF_OTA: [_make_ota_config(port=3232, **{CONF_PASSWORD: "pw"})],
|
||||
}
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
ota_esphome_final_validate({})
|
||||
messages = [r.message for r in caplog.records]
|
||||
assert any("provisioned at runtime also authenticates" in m for m in messages)
|
||||
assert not any("wastes significant flash" in m for m in messages)
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_password_without_api_key_no_warning(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Without an api key there is no offer, so nothing to warn about."""
|
||||
full_conf = {
|
||||
CONF_API: {},
|
||||
CONF_OTA: [_make_ota_config(port=3232, **{CONF_PASSWORD: "pw"})],
|
||||
}
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
ota_esphome_final_validate({})
|
||||
assert not any("authenticates" in r.message for r in caplog.records)
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_web_server_component_without_ota_platform_does_not_warn(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""The web_server component alone has no /update endpoint."""
|
||||
full_conf = {
|
||||
"web_server": {},
|
||||
CONF_OTA: [
|
||||
_make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}})
|
||||
],
|
||||
}
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
ota_esphome_final_validate({})
|
||||
assert not any(
|
||||
"OTA encryption does not cover" in r.message for r in caplog.records
|
||||
)
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_web_server_ota_platform_alone_does_not_warn(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Only the web_server component starts the shared listener, so the ota
|
||||
platform on its own never exposes /update."""
|
||||
full_conf = {
|
||||
CONF_OTA: [
|
||||
_make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}),
|
||||
{CONF_PLATFORM: "web_server", CONF_ID: ID("ota_ws", is_manual=False)},
|
||||
],
|
||||
}
|
||||
token = fv.full_config.set(full_conf)
|
||||
try:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
ota_esphome_final_validate({})
|
||||
assert not any("plaintext /update" in r.message for r in caplog.records)
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_web_server_ota_without_encryption_unaffected() -> None:
|
||||
"""web_server ota stays valid alongside an unencrypted esphome entry."""
|
||||
full_conf = {
|
||||
@@ -370,20 +439,87 @@ def test_auto_load_pulls_noise_only_for_encryption() -> None:
|
||||
assert "noise" in AUTO_LOAD({})
|
||||
|
||||
|
||||
def test_filter_source_files_excludes_noise_without_encryption() -> None:
|
||||
"""The noise transport source compiles only for encrypted builds."""
|
||||
old_config = CORE.config
|
||||
try:
|
||||
CORE.config = {CONF_OTA: [_make_ota_config(port=3232)]}
|
||||
assert FILTER_SOURCE_FILES() == ["ota_esphome_noise.cpp"]
|
||||
CORE.config = {
|
||||
CONF_OTA: [
|
||||
_make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: API_KEY}})
|
||||
]
|
||||
}
|
||||
assert FILTER_SOURCE_FILES() == []
|
||||
finally:
|
||||
CORE.config = old_config
|
||||
def test_static_encryption_key() -> None:
|
||||
"""Only a build-time key counts; a runtime provisioned one does not."""
|
||||
assert static_encryption_key({}) is None
|
||||
assert static_encryption_key({CONF_ENCRYPTION: {}}) is None
|
||||
assert static_encryption_key({CONF_ENCRYPTION: {CONF_KEY: API_KEY}}) == API_KEY
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("yaml_name", "defines_present", "defines_absent"),
|
||||
[
|
||||
# An api key alone compiles the transport in without requiring it;
|
||||
# the device uses the api server's key, not a copy
|
||||
(
|
||||
"api_key_offer",
|
||||
{"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_FROM_API"},
|
||||
{"USE_OTA_ENCRYPTION_REQUIRED", "USE_OTA_ENCRYPTION_PROVISIONED"},
|
||||
),
|
||||
# A password still guards plaintext uploads on an offering device
|
||||
(
|
||||
"api_key_offer_password",
|
||||
{"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_FROM_API", "USE_OTA_PASSWORD"},
|
||||
{"USE_OTA_ENCRYPTION_REQUIRED", "USE_OTA_ENCRYPTION_PROVISIONED"},
|
||||
),
|
||||
# The ota encryption block is what makes the device refuse plaintext
|
||||
(
|
||||
"encryption_required",
|
||||
{
|
||||
"USE_OTA_ENCRYPTION",
|
||||
"USE_OTA_ENCRYPTION_REQUIRED",
|
||||
"USE_OTA_ENCRYPTION_FROM_API",
|
||||
},
|
||||
{"USE_OTA_ENCRYPTION_PROVISIONED"},
|
||||
),
|
||||
# Without api encryption the ota key is the device's own
|
||||
(
|
||||
"own_key",
|
||||
{"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_REQUIRED"},
|
||||
{"USE_OTA_ENCRYPTION_FROM_API", "USE_OTA_ENCRYPTION_PROVISIONED"},
|
||||
),
|
||||
# A key provisioned at runtime lives in the api server; the device
|
||||
# offers with it once provisioned and never requires it
|
||||
(
|
||||
"runtime_api_key",
|
||||
{
|
||||
"USE_OTA_ENCRYPTION",
|
||||
"USE_OTA_ENCRYPTION_FROM_API",
|
||||
"USE_OTA_ENCRYPTION_PROVISIONED",
|
||||
},
|
||||
{"USE_OTA_ENCRYPTION_REQUIRED"},
|
||||
),
|
||||
# No api encryption at all keeps the noise glue out of the build
|
||||
(
|
||||
"plain",
|
||||
set(),
|
||||
{
|
||||
"USE_OTA_ENCRYPTION",
|
||||
"USE_OTA_ENCRYPTION_REQUIRED",
|
||||
"USE_OTA_ENCRYPTION_FROM_API",
|
||||
"USE_OTA_ENCRYPTION_PROVISIONED",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_encryption_offer_codegen(
|
||||
generate_main: Callable[[str], str],
|
||||
yaml_name: str,
|
||||
defines_present: set[str],
|
||||
defines_absent: set[str],
|
||||
) -> None:
|
||||
main_cpp = generate_main(
|
||||
f"tests/component_tests/ota/test_esphome_ota_{yaml_name}.yaml"
|
||||
)
|
||||
defines = {define.name for define in CORE.defines}
|
||||
assert defines_present <= defines
|
||||
assert not (defines_absent & defines)
|
||||
encrypted = "USE_OTA_ENCRYPTION" in defines_present
|
||||
own_key = encrypted and "USE_OTA_ENCRYPTION_FROM_API" not in defines_present
|
||||
assert ("esphome_esphomeotacomponent_id->set_noise_psk(" in main_cpp) is own_key
|
||||
assert ("set_auth_password(" in main_cpp) is ("USE_OTA_PASSWORD" in defines_present)
|
||||
# The noise transport source compiles only when the define is set
|
||||
assert FILTER_SOURCE_FILES() == ([] if encrypted else ["ota_esphome_noise.cpp"])
|
||||
|
||||
|
||||
def test_password_with_encryption_rejected() -> None:
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: ota-offer
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
@@ -0,0 +1,12 @@
|
||||
esphome:
|
||||
name: ota-offer-password
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "superlongpasswordthatnoonewillknow"
|
||||
@@ -0,0 +1,12 @@
|
||||
esphome:
|
||||
name: ota-encryption-required
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
encryption:
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: ota-own-key
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
encryption:
|
||||
key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: ota-plain
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
@@ -0,0 +1,10 @@
|
||||
esphome:
|
||||
name: ota-runtime-key
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
encryption:
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
output:
|
||||
- platform: ac_dimmer
|
||||
id: dimmer
|
||||
gate_pin: GPIO4
|
||||
zero_cross_pin: GPIO5
|
||||
min_power: 0%
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
output:
|
||||
- platform: ledc
|
||||
id: default_power
|
||||
pin: GPIO4
|
||||
max_power: 100%
|
||||
min_power: 0%
|
||||
- platform: ledc
|
||||
id: custom_power
|
||||
pin: GPIO5
|
||||
max_power: 90%
|
||||
min_power: 1%
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Tests for the output platform codegen."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def test_default_power_limits_are_not_emitted(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""max_power 100% and min_power 0% already live in the C++ initializers."""
|
||||
main_cpp = generate_main(component_config_path("power_limits.yaml"))
|
||||
|
||||
assert "default_power->set_max_power(" not in main_cpp
|
||||
assert "default_power->set_min_power(" not in main_cpp
|
||||
assert "custom_power->set_max_power(0.9f);" in main_cpp
|
||||
assert "custom_power->set_min_power(0.01f);" in main_cpp
|
||||
assert "USE_OUTPUT_FLOAT_POWER_SCALING" in {d.name for d in CORE.defines}
|
||||
|
||||
|
||||
def test_default_min_power_keeps_scaling_fields(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""ac_dimmer reads min_power_ directly, so the define must stay on for min_power 0%."""
|
||||
main_cpp = generate_main(component_config_path("ac_dimmer_min_power_zero.yaml"))
|
||||
|
||||
assert "dimmer->set_min_power(" not in main_cpp
|
||||
assert "USE_OUTPUT_FLOAT_POWER_SCALING" in {d.name for d in CORE.defines}
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr
|
||||
pin: GPIO4
|
||||
@@ -0,0 +1,10 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr
|
||||
pin: GPIO4
|
||||
buffer_size: 2kb
|
||||
@@ -0,0 +1,12 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-c2-devkitm-1
|
||||
variant: esp32c2
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr
|
||||
pin: GPIO4
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr
|
||||
pin: GPIO4
|
||||
@@ -0,0 +1,24 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
logger:
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr
|
||||
pin: GPIO4
|
||||
dump:
|
||||
- nec
|
||||
- rc_switch
|
||||
on_nec:
|
||||
then:
|
||||
- logger.log: nec
|
||||
|
||||
binary_sensor:
|
||||
- platform: remote_receiver
|
||||
name: Remote Input
|
||||
nec:
|
||||
address: 0x1234
|
||||
command: 0x5678
|
||||
@@ -0,0 +1,22 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr_ir
|
||||
pin: GPIO4
|
||||
- id: rcvr_rf
|
||||
pin: GPIO5
|
||||
|
||||
infrared:
|
||||
- platform: ir_rf_proxy
|
||||
name: IR Receiver
|
||||
remote_receiver_id: rcvr_ir
|
||||
|
||||
radio_frequency:
|
||||
- platform: ir_rf_proxy
|
||||
name: RF Receiver
|
||||
frequency: 433.92MHz
|
||||
remote_receiver_id: rcvr_rf
|
||||
@@ -0,0 +1,28 @@
|
||||
"""buffer_size reaches the receiver when set, and always on the pulse ring targets."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_explicit_buffer_size_is_passed_through(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("receiver_buffer_size.yaml"))
|
||||
assert "rcvr->set_buffer_size(2000);" in main_cpp
|
||||
|
||||
|
||||
def test_pulse_ring_target_keeps_a_default(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("receiver_esp8266.yaml"))
|
||||
assert "rcvr->set_buffer_size(1000);" in main_cpp
|
||||
|
||||
|
||||
def test_esp32_variant_without_rmt_keeps_a_default(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("receiver_esp32_c2.yaml"))
|
||||
assert "rcvr->set_buffer_size(1000);" in main_cpp
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Listener and dumper StaticVector sizes come from codegen slot counts."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.automation import ACTION_REGISTRY
|
||||
from esphome.components import remote_base
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from ..helpers import get_define_value
|
||||
|
||||
|
||||
def test_dumper_and_listener_counts(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path("receiver_with_dumpers.yaml"))
|
||||
# nec and rc_switch dumpers
|
||||
assert get_define_value("REMOTE_BASE_DUMPER_COUNT") == "2"
|
||||
# on_nec trigger plus the remote_receiver binary sensor
|
||||
assert get_define_value("REMOTE_BASE_LISTENER_COUNT") == "2"
|
||||
|
||||
|
||||
def test_bare_receiver_emits_no_counts(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("receiver_bare.yaml"))
|
||||
# the RMT ring is sized in setup() unless buffer_size is set
|
||||
assert "set_buffer_size" not in main_cpp
|
||||
assert get_define_value("REMOTE_BASE_DUMPER_COUNT") is None
|
||||
assert get_define_value("REMOTE_BASE_LISTENER_COUNT") is None
|
||||
|
||||
|
||||
def test_proxy_receivers_count_as_listeners(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path("receiver_with_proxies.yaml"))
|
||||
# one proxy entity listens on each of the two receivers; every receiver's list gets the
|
||||
# capacity of the busiest one, so this is the largest per receiver count, not the sum
|
||||
assert get_define_value("REMOTE_BASE_LISTENER_COUNT") == "1"
|
||||
assert get_define_value("REMOTE_BASE_DUMPER_COUNT") is None
|
||||
|
||||
|
||||
def test_only_used_protocol_sources_are_compiled(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path("receiver_with_dumpers.yaml"))
|
||||
excluded = set(remote_base.FILTER_SOURCE_FILES())
|
||||
assert "nec_protocol.cpp" not in excluded
|
||||
assert "rc_switch_protocol.cpp" not in excluded
|
||||
assert "sony_protocol.cpp" in excluded
|
||||
assert "remote_base.cpp" not in excluded
|
||||
|
||||
|
||||
def test_every_registry_name_maps_to_a_protocol_source() -> None:
|
||||
"""A registry name must resolve to a source file or request_protocol rejects it."""
|
||||
names = (
|
||||
set(remote_base.BINARY_SENSOR_REGISTRY)
|
||||
| set(remote_base.DUMPER_REGISTRY)
|
||||
| {key.removeprefix("on_") for key in remote_base.TRIGGER_REGISTRY}
|
||||
| {
|
||||
key.removeprefix("remote_transmitter.transmit_")
|
||||
for key in ACTION_REGISTRY
|
||||
if key.startswith("remote_transmitter.transmit_")
|
||||
}
|
||||
)
|
||||
assert len(names) > 40
|
||||
for name in names:
|
||||
assert remote_base._protocol_stem(name) in remote_base._PROTOCOL_STEMS, name
|
||||
|
||||
|
||||
def test_request_protocol_rejects_unknown_names() -> None:
|
||||
"""A misspelled protocol would otherwise surface only as a link error."""
|
||||
with pytest.raises(ValueError, match="Unknown remote protocol 'toshiba'"):
|
||||
remote_base.request_protocol("toshiba")
|
||||
|
||||
|
||||
def test_dump_list_is_deduplicated_across_forms() -> None:
|
||||
dumpers = remote_base.validate_dumpers(["raw", {"raw": None}, "nec", "nec"])
|
||||
assert [
|
||||
next(k for k in entry if k in remote_base.DUMPER_REGISTRY) for entry in dumpers
|
||||
] == ["raw", "nec"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", [["nec", None], [5]])
|
||||
def test_dump_list_rejects_invalid_entries_with_a_validation_error(bad: list) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
remote_base.validate_dumpers(bad)
|
||||
@@ -26,7 +26,7 @@ from ..types import SetCoreConfigCallable
|
||||
(PlatformFramework.ESP32_IDF, None, True),
|
||||
(PlatformFramework.RTL87XX_ARDUINO, FAMILY_RTL8720C, True),
|
||||
(PlatformFramework.RTL87XX_ARDUINO, FAMILY_RTL8710B, False),
|
||||
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231N, True),
|
||||
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231N, False),
|
||||
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7238, True),
|
||||
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231T, False),
|
||||
(PlatformFramework.ESP8266_ARDUINO, None, False),
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Tests for the sdl display schema, in particular the headless option."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.sdl.display import (
|
||||
CONF_SDL_ID,
|
||||
CONFIG_SCHEMA,
|
||||
headless_final_validate,
|
||||
)
|
||||
from esphome.config import Config
|
||||
from esphome.const import PlatformFramework
|
||||
from esphome.core import ID
|
||||
from esphome.final_validate import full_config
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _host_platform(set_core_config: SetCoreConfigCallable) -> None:
|
||||
set_core_config(PlatformFramework.HOST_NATIVE)
|
||||
|
||||
|
||||
def _config(**extra: object) -> ConfigType:
|
||||
config: ConfigType = {
|
||||
"dimensions": {"width": 320, "height": 240},
|
||||
# sdl2-config is not necessarily installed in the test environment
|
||||
"sdl_options": "-lSDL2",
|
||||
}
|
||||
config.update(extra)
|
||||
return config
|
||||
|
||||
|
||||
def test_defaults_to_windowed() -> None:
|
||||
"""A display without the option is not headless."""
|
||||
assert CONFIG_SCHEMA(_config())["headless"] is False
|
||||
|
||||
|
||||
def test_headless_accepted() -> None:
|
||||
"""A headless display needs nothing beyond the dimensions."""
|
||||
assert CONFIG_SCHEMA(_config(headless=True))["headless"] is True
|
||||
|
||||
|
||||
def test_headless_rejects_window_options() -> None:
|
||||
"""Window options are meaningless without a window."""
|
||||
with pytest.raises(cv.Invalid, match="has no effect"):
|
||||
CONFIG_SCHEMA(
|
||||
_config(headless=True, window_options={"position": {"x": 0, "y": 0}})
|
||||
)
|
||||
|
||||
|
||||
def test_headless_rejects_snapshot_key() -> None:
|
||||
"""A headless display has no keyboard, so the action is the only way in."""
|
||||
with pytest.raises(cv.Invalid, match="snapshot.take"):
|
||||
CONFIG_SCHEMA(_config(headless=True, snapshot_key="SDLK_F12"))
|
||||
|
||||
|
||||
def test_snapshot_key_accepted_when_windowed() -> None:
|
||||
"""The key is only valid alongside a window."""
|
||||
config = CONFIG_SCHEMA(_config(snapshot_key="SDLK_F12"))
|
||||
assert str(config["snapshot_key"]) == "SDLK_F12"
|
||||
|
||||
|
||||
def _declare_sdl_display(headless: bool) -> ID:
|
||||
"""Register a full_config with a single sdl display declaration and return a reference to it.
|
||||
|
||||
Mirrors what the real config pipeline leaves behind: a "display" domain entry plus a
|
||||
declare_ids record id_declaration_match_schema uses to find it again.
|
||||
"""
|
||||
declared_id = ID("my_sdl", is_declaration=True)
|
||||
fc = Config()
|
||||
fc["display"] = [
|
||||
{
|
||||
"platform": "sdl",
|
||||
"id": declared_id,
|
||||
"headless": headless,
|
||||
"dimensions": {"width": 320, "height": 240},
|
||||
}
|
||||
]
|
||||
fc.declare_ids.append((declared_id, ["display", 0, "id"]))
|
||||
full_config.set(fc)
|
||||
return ID("my_sdl")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", ["binary_sensor", "touchscreen"])
|
||||
def test_headless_final_validate_rejects_headless_display(platform: str) -> None:
|
||||
"""binary_sensor and touchscreen both need a window, so a headless display is rejected."""
|
||||
sdl_ref = _declare_sdl_display(headless=True)
|
||||
schema = headless_final_validate(platform)
|
||||
with pytest.raises(cv.Invalid, match="needs a window"):
|
||||
schema({CONF_SDL_ID: sdl_ref})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", ["binary_sensor", "touchscreen"])
|
||||
def test_headless_final_validate_accepts_windowed_display(platform: str) -> None:
|
||||
"""The same platforms are accepted once the display has a window."""
|
||||
sdl_ref = _declare_sdl_display(headless=False)
|
||||
schema = headless_final_validate(platform)
|
||||
schema({CONF_SDL_ID: sdl_ref}) # Should not raise.
|
||||
@@ -0,0 +1,12 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
sendspin:
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: test
|
||||
project:
|
||||
name: project_manufacturer.project_model
|
||||
version: 9.9.9
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
sendspin:
|
||||
manufacturer: Explicit Manufacturer
|
||||
model: Explicit Model
|
||||
firmware_version: 1.2.3
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test
|
||||
project:
|
||||
name: project_manufacturer.project_model
|
||||
version: 9.9.9
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
sendspin:
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Tests for the device information the sendspin hub reports to the server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.const import CONF_MANUFACTURER
|
||||
from esphome.components.sendspin import CONF_FIRMWARE_VERSION, CONFIG_SCHEMA
|
||||
from esphome.const import CONF_MODEL, PlatformFramework
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def test_explicit_device_info_wins_over_project(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Configured values take precedence over the project information."""
|
||||
main_cpp = generate_main(component_config_path("device_info_explicit.yaml"))
|
||||
|
||||
assert 'set_manufacturer("Explicit Manufacturer")' in main_cpp
|
||||
assert 'set_model("Explicit Model")' in main_cpp
|
||||
assert 'set_firmware_version("1.2.3")' in main_cpp
|
||||
|
||||
|
||||
def test_project_supplies_device_info(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Without configured values, the project name splits into manufacturer and model."""
|
||||
main_cpp = generate_main(component_config_path("device_info_project.yaml"))
|
||||
|
||||
assert 'set_manufacturer("project_manufacturer")' in main_cpp
|
||||
assert 'set_model("project_model")' in main_cpp
|
||||
assert 'set_firmware_version("9.9.9")' in main_cpp
|
||||
|
||||
|
||||
def test_no_device_info_leaves_hub_defaults(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""With neither source, nothing is emitted and the hub keeps its own defaults."""
|
||||
main_cpp = generate_main(component_config_path("device_info_default.yaml"))
|
||||
|
||||
assert "set_manufacturer(" not in main_cpp
|
||||
assert "set_model(" not in main_cpp
|
||||
assert "set_firmware_version(" not in main_cpp
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"conf_key", [CONF_MANUFACTURER, CONF_MODEL, CONF_FIRMWARE_VERSION]
|
||||
)
|
||||
def test_empty_device_info_rejected(
|
||||
set_core_config: SetCoreConfigCallable, conf_key: str
|
||||
) -> None:
|
||||
"""An empty string would be sent to the server as an empty value, so it is not accepted."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA({conf_key: ""})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"conf_key", [CONF_MANUFACTURER, CONF_MODEL, CONF_FIRMWARE_VERSION]
|
||||
)
|
||||
def test_device_info_capped_at_127_bytes(
|
||||
set_core_config: SetCoreConfigCallable, conf_key: str
|
||||
) -> None:
|
||||
"""The cap is in bytes so the protobuf length prefix stays a single byte."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
CONFIG_SCHEMA({conf_key: "a" * 127})
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA({conf_key: "a" * 128})
|
||||
# 64 two-byte characters is 128 bytes.
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA({conf_key: "é" * 64})
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Validation tests for the sendspin media_source platform.
|
||||
|
||||
These cover the codec preference list, whose rejection branches a compile test
|
||||
cannot reach: a `test*.yaml` can only assert that a configuration is accepted.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.sendspin import CONF_CODECS, _get_data
|
||||
from esphome.components.sendspin.media_source import CONFIG_SCHEMA
|
||||
from esphome.const import PlatformFramework
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def _media_source_config(**overrides: Any) -> ConfigType:
|
||||
"""Build a minimal valid media source config, allowing field overrides."""
|
||||
config: ConfigType = {
|
||||
"id": "sendspin_media_source",
|
||||
"sendspin_id": "sendspin_hub",
|
||||
}
|
||||
config.update(overrides)
|
||||
return config
|
||||
|
||||
|
||||
def test_default_codecs_at_48_khz(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""Every codec is advertised when the sample rate suits all of them."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
config = CONFIG_SCHEMA(_media_source_config())
|
||||
|
||||
assert config[CONF_CODECS] == ["flac", "opus", "pcm"]
|
||||
|
||||
|
||||
def test_default_codecs_drop_opus_at_other_rates(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""Opus only supports 48 kHz, so it leaves the default list at other rates."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
config = CONFIG_SCHEMA(_media_source_config(sample_rate=44100))
|
||||
|
||||
assert config[CONF_CODECS] == ["flac", "pcm"]
|
||||
|
||||
|
||||
def test_configured_order_is_preserved(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""The list is a preference order, so it reaches the player role as written."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
CONFIG_SCHEMA(_media_source_config(codecs=["pcm", "flac"]))
|
||||
|
||||
assert _get_data().player_config[CONF_CODECS] == ["pcm", "flac"]
|
||||
|
||||
|
||||
def test_empty_codec_list_rejected(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""A player with no codecs at all could never be given a stream."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
with pytest.raises(cv.Invalid, match="length of value must be at least 1"):
|
||||
CONFIG_SCHEMA(_media_source_config(codecs=[]))
|
||||
|
||||
|
||||
def test_duplicate_codec_rejected(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""A repeated codec has no meaning in a preference order."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
with pytest.raises(cv.Invalid, match="may only be listed once"):
|
||||
CONFIG_SCHEMA(_media_source_config(codecs=["flac", "flac"]))
|
||||
|
||||
|
||||
def test_unknown_codec_rejected(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""Only codecs the player role can decode are accepted."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
with pytest.raises(cv.Invalid, match="Unknown value"):
|
||||
CONFIG_SCHEMA(_media_source_config(codecs=["mp3"]))
|
||||
|
||||
|
||||
def test_opus_at_wrong_sample_rate_rejected(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""Asking for Opus at a rate it cannot handle fails rather than silently
|
||||
dropping the stated preference."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
with pytest.raises(cv.Invalid, match="requires a sample_rate of 48000"):
|
||||
CONFIG_SCHEMA(_media_source_config(codecs=["opus"], sample_rate=44100))
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Tests for template climate config validation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.template.climate import (
|
||||
CONF_SET_TARGET_HUMIDITY_ACTION,
|
||||
CONF_SET_TARGET_TEMPERATURE_ACTION,
|
||||
CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION,
|
||||
CONF_SET_TARGET_TEMPERATURE_LOW_ACTION,
|
||||
CONF_SUPPORTS_CURRENT_HUMIDITY,
|
||||
CONF_SUPPORTS_CURRENT_TEMPERATURE,
|
||||
CONF_SUPPORTS_TARGET_HUMIDITY,
|
||||
CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE,
|
||||
CONF_TARGET_HUMIDITY,
|
||||
_resolve_supports,
|
||||
_validate_initial_state,
|
||||
_validate_set_actions,
|
||||
)
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY_SENSOR,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_SENSOR,
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
CONF_TARGET_TEMPERATURE_HIGH,
|
||||
CONF_TARGET_TEMPERATURE_LOW,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
def test_supports_current_temperature_derived_from_sensor() -> None:
|
||||
config: ConfigType = {CONF_SENSOR: "some_sensor"}
|
||||
assert _resolve_supports(config)[CONF_SUPPORTS_CURRENT_TEMPERATURE] is True
|
||||
|
||||
|
||||
def test_supports_current_temperature_false_without_sensor() -> None:
|
||||
assert _resolve_supports({})[CONF_SUPPORTS_CURRENT_TEMPERATURE] is False
|
||||
|
||||
|
||||
def test_supports_current_temperature_explicit_true_without_sensor_allowed() -> None:
|
||||
# The value can still be reported with climate.template.publish.
|
||||
config: ConfigType = {CONF_SUPPORTS_CURRENT_TEMPERATURE: True}
|
||||
assert _resolve_supports(config)[CONF_SUPPORTS_CURRENT_TEMPERATURE] is True
|
||||
|
||||
|
||||
def test_supports_current_temperature_false_with_sensor_rejected() -> None:
|
||||
config: ConfigType = {
|
||||
CONF_SENSOR: "some_sensor",
|
||||
CONF_SUPPORTS_CURRENT_TEMPERATURE: False,
|
||||
}
|
||||
with pytest.raises(cv.Invalid, match="cannot be false"):
|
||||
_resolve_supports(config)
|
||||
|
||||
|
||||
def test_supports_current_humidity_false_with_sensor_rejected() -> None:
|
||||
config: ConfigType = {
|
||||
CONF_HUMIDITY_SENSOR: "some_sensor",
|
||||
CONF_SUPPORTS_CURRENT_HUMIDITY: False,
|
||||
}
|
||||
with pytest.raises(cv.Invalid, match="cannot be false"):
|
||||
_resolve_supports(config)
|
||||
|
||||
|
||||
def test_two_point_derived_from_set_actions() -> None:
|
||||
config: ConfigType = {
|
||||
CONF_SET_TARGET_TEMPERATURE_LOW_ACTION: [{}],
|
||||
CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION: [{}],
|
||||
}
|
||||
assert _resolve_supports(config)[CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE] is True
|
||||
|
||||
|
||||
def test_two_point_false_with_set_action_rejected() -> None:
|
||||
config: ConfigType = {
|
||||
CONF_SET_TARGET_TEMPERATURE_LOW_ACTION: [{}],
|
||||
CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: False,
|
||||
}
|
||||
with pytest.raises(cv.Invalid, match="cannot be false"):
|
||||
_resolve_supports(config)
|
||||
|
||||
|
||||
def test_target_humidity_derived_from_set_action() -> None:
|
||||
config: ConfigType = {CONF_SET_TARGET_HUMIDITY_ACTION: [{}]}
|
||||
assert _resolve_supports(config)[CONF_SUPPORTS_TARGET_HUMIDITY] is True
|
||||
|
||||
|
||||
def test_set_target_temperature_low_requires_high() -> None:
|
||||
config: ConfigType = {CONF_SET_TARGET_TEMPERATURE_LOW_ACTION: [{}]}
|
||||
with pytest.raises(cv.Invalid, match="must be used together"):
|
||||
_validate_set_actions(config)
|
||||
|
||||
|
||||
def test_set_target_temperature_conflicts_with_two_point_actions() -> None:
|
||||
config: ConfigType = {
|
||||
CONF_SET_TARGET_TEMPERATURE_ACTION: [{}],
|
||||
CONF_SET_TARGET_TEMPERATURE_LOW_ACTION: [{}],
|
||||
CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION: [{}],
|
||||
}
|
||||
with pytest.raises(cv.Invalid, match="cannot be used together"):
|
||||
_validate_set_actions(config)
|
||||
|
||||
|
||||
def test_initial_state_target_temperature_rejected_with_two_point() -> None:
|
||||
config: ConfigType = {
|
||||
CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: True,
|
||||
CONF_SUPPORTS_TARGET_HUMIDITY: False,
|
||||
CONF_INITIAL_STATE: {CONF_TARGET_TEMPERATURE: 21.0},
|
||||
}
|
||||
with pytest.raises(cv.Invalid, match="is not available"):
|
||||
_validate_initial_state(config)
|
||||
|
||||
|
||||
def test_initial_state_two_point_values_rejected_without_two_point() -> None:
|
||||
config: ConfigType = {
|
||||
CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: False,
|
||||
CONF_SUPPORTS_TARGET_HUMIDITY: False,
|
||||
CONF_INITIAL_STATE: {
|
||||
CONF_TARGET_TEMPERATURE_LOW: 18.0,
|
||||
CONF_TARGET_TEMPERATURE_HIGH: 24.0,
|
||||
},
|
||||
}
|
||||
with pytest.raises(cv.Invalid, match="requires"):
|
||||
_validate_initial_state(config)
|
||||
|
||||
|
||||
def test_initial_state_target_humidity_rejected_without_support() -> None:
|
||||
config: ConfigType = {
|
||||
CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: False,
|
||||
CONF_SUPPORTS_TARGET_HUMIDITY: False,
|
||||
CONF_INITIAL_STATE: {CONF_TARGET_HUMIDITY: 50},
|
||||
}
|
||||
with pytest.raises(cv.Invalid, match="requires"):
|
||||
_validate_initial_state(config)
|
||||
|
||||
|
||||
def test_initial_state_matching_two_point_accepted() -> None:
|
||||
config: ConfigType = {
|
||||
CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: True,
|
||||
CONF_SUPPORTS_TARGET_HUMIDITY: True,
|
||||
CONF_INITIAL_STATE: {
|
||||
CONF_TARGET_TEMPERATURE_LOW: 18.0,
|
||||
CONF_TARGET_TEMPERATURE_HIGH: 24.0,
|
||||
CONF_TARGET_HUMIDITY: 50,
|
||||
},
|
||||
}
|
||||
assert _validate_initial_state(config) is config
|
||||
@@ -0,0 +1,76 @@
|
||||
"""The template platforms surface value-describing metadata on the main form.
|
||||
|
||||
Hardware platforms get sensible defaults for unit/device_class/etc., so those
|
||||
fields fall through to the editor's advanced disclosure. A ``template`` entity
|
||||
has no such defaults -- the user is expected to define them -- so the template
|
||||
platforms pass ``visibility=cv.Visibility.UI`` to promote them onto the form.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
import esphome.config_validation as cv
|
||||
|
||||
|
||||
def _markers(schema: cv.Schema) -> dict[str, object]:
|
||||
s = schema
|
||||
if hasattr(s, "validators"):
|
||||
# cv.All -> the schema is the first validator.
|
||||
s = s.validators[0]
|
||||
return {str(k): k for k in s.schema}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("platform", "fields"),
|
||||
[
|
||||
(
|
||||
"sensor",
|
||||
[
|
||||
"unit_of_measurement",
|
||||
"accuracy_decimals",
|
||||
"device_class",
|
||||
"state_class",
|
||||
"force_update",
|
||||
],
|
||||
),
|
||||
("binary_sensor", ["device_class"]),
|
||||
("switch", ["device_class"]),
|
||||
("cover", ["device_class"]),
|
||||
("button", ["device_class"]),
|
||||
("valve", ["device_class"]),
|
||||
("event", ["device_class"]),
|
||||
("text_sensor", ["device_class"]),
|
||||
("number", ["device_class", "unit_of_measurement"]),
|
||||
],
|
||||
)
|
||||
def test_template_metadata_is_ui(platform: str, fields: list[str]) -> None:
|
||||
mod = importlib.import_module(f"esphome.components.template.{platform}")
|
||||
markers = _markers(mod.CONFIG_SCHEMA)
|
||||
for field in fields:
|
||||
assert markers[field].visibility is cv.Visibility.UI, f"{platform}.{field}"
|
||||
|
||||
|
||||
def test_template_sensor_promotion_preserves_defaults() -> None:
|
||||
"""Promoting to UI must not drop the fields' defaults."""
|
||||
from esphome.components.template.sensor import CONFIG_SCHEMA
|
||||
|
||||
markers = _markers(CONFIG_SCHEMA)
|
||||
assert markers["accuracy_decimals"].default() == 1
|
||||
assert markers["force_update"].default() is False
|
||||
|
||||
|
||||
def test_hardware_platform_metadata_not_promoted() -> None:
|
||||
"""Without ``visibility=`` the builders leave metadata unset.
|
||||
|
||||
Unset markers fall through to the consumer's ``Optional`` default of
|
||||
advanced, so hardware platforms are unaffected by the template promotion.
|
||||
"""
|
||||
from esphome.components import binary_sensor, sensor
|
||||
|
||||
hw_sensor = _markers(sensor.sensor_schema(device_class="temperature"))
|
||||
assert hw_sensor["device_class"].visibility is None
|
||||
hw_bs = _markers(binary_sensor.binary_sensor_schema(device_class="motion"))
|
||||
assert hw_bs["device_class"].visibility is None
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
from esphome.config import Config
|
||||
from esphome.const import PlatformFramework
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -18,5 +19,5 @@ class SetCoreConfigCallable(Protocol):
|
||||
*,
|
||||
core_data: ConfigType | None = None,
|
||||
platform_data: ConfigType | None = None,
|
||||
full_config: dict[str, ConfigType] | None = None,
|
||||
full_config: dict[str, ConfigType] | Config | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
web_server:
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
web_server:
|
||||
port: 8080
|
||||
log: false
|
||||
include_internal: true
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
log: true
|
||||
include_internal: false
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Tests that web_server only emits setters for non default values."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config_file", ["bare.yaml", "defaults.yaml"])
|
||||
def test_default_values_are_not_emitted(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
) -> None:
|
||||
"""Port 80, log on and include_internal off already live in the C++ initializers.
|
||||
|
||||
Both the schema defaults and the same values written explicitly take the skip path.
|
||||
"""
|
||||
main_cpp = generate_main(component_config_path(config_file))
|
||||
|
||||
assert "set_port(" not in main_cpp
|
||||
assert "set_expose_log(" not in main_cpp
|
||||
assert "set_include_internal(" not in main_cpp
|
||||
|
||||
|
||||
def test_custom_values_are_emitted(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Non default values still reach the C++ setters."""
|
||||
main_cpp = generate_main(component_config_path("custom.yaml"))
|
||||
|
||||
assert "set_port(8080);" in main_cpp
|
||||
assert "set_expose_log(false);" in main_cpp
|
||||
assert "set_include_internal(true);" in main_cpp
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
ap:
|
||||
ssid: fallback
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
wifi:
|
||||
networks:
|
||||
- ssid: test
|
||||
password: testtest
|
||||
priority: 5
|
||||
ap:
|
||||
ssid: fallback
|
||||
ap_timeout: 2min
|
||||
reboot_timeout: 0s
|
||||
power_save_mode: light
|
||||
min_auth_mode: wpa
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
wifi:
|
||||
networks:
|
||||
- ssid: test
|
||||
password: testtest
|
||||
priority: 0
|
||||
ap:
|
||||
ssid: fallback
|
||||
ap_timeout: 90s
|
||||
reboot_timeout: 15min
|
||||
power_save_mode: none
|
||||
min_auth_mode: wpa2
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Tests that wifi codegen skips setters for default values."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config_file", ["bare.yaml", "defaults.yaml"])
|
||||
def test_default_values_are_not_emitted(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
) -> None:
|
||||
"""Priority 0, 90 s AP timeout, 15 min reboot, power save none, WPA2 are C++ defaults.
|
||||
|
||||
Both the schema defaults and the same values written explicitly take the skip path.
|
||||
"""
|
||||
main_cpp = generate_main(component_config_path(config_file))
|
||||
|
||||
assert "set_priority(" not in main_cpp
|
||||
assert "set_ap_timeout(" not in main_cpp
|
||||
assert "set_reboot_timeout(" not in main_cpp
|
||||
assert "set_power_save_mode(" not in main_cpp
|
||||
assert "set_min_auth_mode(" not in main_cpp
|
||||
|
||||
|
||||
def test_custom_values_are_emitted(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Non default values still reach the C++ setters."""
|
||||
main_cpp = generate_main(component_config_path("custom.yaml"))
|
||||
|
||||
assert "set_priority(5);" in main_cpp
|
||||
assert "set_ap_timeout(120000);" in main_cpp
|
||||
assert "set_reboot_timeout(0);" in main_cpp
|
||||
assert "set_power_save_mode(wifi::WIFI_POWER_SAVE_LIGHT);" in main_cpp
|
||||
assert "set_min_auth_mode(wifi::WIFI_MIN_AUTH_MODE_WPA);" in main_cpp
|
||||
@@ -4,3 +4,4 @@ output:
|
||||
gate_pin: ${gate_pin}
|
||||
zero_cross_pin: ${zero_cross_pin}
|
||||
zero_cross_interrupt_type: ANY
|
||||
min_power: 0%
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.core import CORE
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# USE_API compiles every api source, so emit what they need. No socket
|
||||
# override: an __init__.py there makes pytest import its conftest as socket.conftest.
|
||||
async def to_code_testing(config):
|
||||
cg.add_define("USE_API")
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
cg.add_define("API_MAX_SEND_QUEUE", 8)
|
||||
cg.add_define("MAX_API_CONNECTIONS", 1)
|
||||
cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")
|
||||
CORE.register_controller() # api_server registers with the controller registry
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -0,0 +1,65 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/components/api/api_buffer.h"
|
||||
|
||||
namespace esphome::api::testing {
|
||||
|
||||
// Pointer plus two 16 bit sizes
|
||||
static_assert(sizeof(APIBuffer) <= 2 * sizeof(void *));
|
||||
|
||||
TEST(APIBuffer, RefusesSizesAbove16Bits) {
|
||||
APIBuffer buf;
|
||||
ASSERT_TRUE(buf.resize(16));
|
||||
EXPECT_FALSE(buf.reserve(UINT16_MAX + 1));
|
||||
EXPECT_EQ(buf.size(), 16u);
|
||||
EXPECT_EQ(buf.capacity(), 16u);
|
||||
EXPECT_TRUE(buf.reserve(UINT16_MAX));
|
||||
EXPECT_EQ(buf.capacity(), UINT16_MAX);
|
||||
}
|
||||
|
||||
static const uint8_t BYTES[] = {1, 2, 3, 4, 5, 6};
|
||||
|
||||
TEST(APIBuffer, AppendReturnsTheNewBytes) {
|
||||
APIBuffer buf;
|
||||
ASSERT_TRUE(buf.reserve(8));
|
||||
uint8_t *first = buf.append(3);
|
||||
ASSERT_NE(first, nullptr);
|
||||
std::memcpy(first, BYTES, 3);
|
||||
EXPECT_EQ(buf.size(), 3u);
|
||||
EXPECT_EQ(buf.capacity(), 8u);
|
||||
|
||||
// Grows through realloc and keeps what was there
|
||||
uint8_t *second = buf.append(6);
|
||||
ASSERT_EQ(second, buf.data() + 3);
|
||||
std::memcpy(second, BYTES + 3, 3);
|
||||
EXPECT_EQ(buf.size(), 9u);
|
||||
EXPECT_EQ(buf.capacity(), 9u);
|
||||
EXPECT_EQ(std::memcmp(buf.data(), BYTES, 6), 0);
|
||||
}
|
||||
|
||||
TEST(APIBuffer, DropFrontSlidesTheRestDown) {
|
||||
APIBuffer buf;
|
||||
uint8_t *bytes = buf.append(6);
|
||||
ASSERT_NE(bytes, nullptr);
|
||||
std::memcpy(bytes, BYTES, 6);
|
||||
|
||||
buf.drop_front(2);
|
||||
EXPECT_EQ(buf.size(), 4u);
|
||||
EXPECT_EQ(buf.capacity(), 6u);
|
||||
EXPECT_EQ(std::memcmp(buf.data(), BYTES + 2, 4), 0);
|
||||
|
||||
// Growing afterwards keeps the slid bytes
|
||||
ASSERT_TRUE(buf.reserve(64));
|
||||
EXPECT_EQ(buf.size(), 4u);
|
||||
EXPECT_EQ(std::memcmp(buf.data(), BYTES + 2, 4), 0);
|
||||
|
||||
// Dropping everything leaves an empty buffer with its capacity
|
||||
buf.drop_front(4);
|
||||
EXPECT_EQ(buf.size(), 0u);
|
||||
EXPECT_EQ(buf.capacity(), 64u);
|
||||
}
|
||||
|
||||
} // namespace esphome::api::testing
|
||||
@@ -0,0 +1,510 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/api/api_overflow_buffer.h"
|
||||
|
||||
#ifdef USE_HOST
|
||||
namespace esphome::api::testing {
|
||||
|
||||
// Idle cost is the buffer plus one word of bookkeeping
|
||||
static_assert(sizeof(APIOverflowBuffer) <= sizeof(APIBuffer) + sizeof(void *));
|
||||
|
||||
// Exposes storage so tests can check it is reused, not reallocated
|
||||
class TestOverflowBuffer : public APIOverflowBuffer {
|
||||
public:
|
||||
using APIOverflowBuffer::LEN_PREFIX;
|
||||
using APIOverflowBuffer::MAX_BYTES;
|
||||
using APIOverflowBuffer::MAX_LONE_BYTES;
|
||||
struct Storage {
|
||||
size_t capacity;
|
||||
const uint8_t *data;
|
||||
bool operator==(const Storage &) const = default;
|
||||
};
|
||||
size_t capacity() const { return this->buf_.capacity(); }
|
||||
Storage storage() const { return {this->buf_.capacity(), this->buf_.data()}; }
|
||||
uint8_t count() const { return this->count_; }
|
||||
size_t live() const { return this->buf_.size() - this->head_; }
|
||||
/// Simulates a socket write inside try_drain() re-entering the send path
|
||||
void set_draining(bool draining) { this->draining_ = draining; }
|
||||
};
|
||||
|
||||
static std::vector<uint8_t> make_message(size_t len, uint8_t seed) {
|
||||
std::vector<uint8_t> msg(len);
|
||||
for (size_t i = 0; i < len; i++)
|
||||
msg[i] = static_cast<uint8_t>(seed + i);
|
||||
return msg;
|
||||
}
|
||||
|
||||
static bool enqueue(TestOverflowBuffer &buf, const std::vector<uint8_t> &msg, uint16_t skip = 0) {
|
||||
struct iovec iov = {const_cast<uint8_t *>(msg.data()), msg.size()};
|
||||
return buf.enqueue_iov(&iov, 1, static_cast<uint16_t>(msg.size()), skip);
|
||||
}
|
||||
|
||||
static void append(std::vector<uint8_t> &dst, const std::vector<uint8_t> &src, size_t skip = 0) {
|
||||
dst.insert(dst.end(), src.begin() + skip, src.end());
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> concat(std::initializer_list<std::vector<uint8_t>> parts) {
|
||||
std::vector<uint8_t> out;
|
||||
for (const auto &part : parts)
|
||||
append(out, part);
|
||||
return out;
|
||||
}
|
||||
|
||||
/// The pipe delivers the filler first, then the drained messages.
|
||||
static void expect_after_filler(const std::vector<uint8_t> &received, size_t filler,
|
||||
const std::vector<uint8_t> &expected) {
|
||||
ASSERT_EQ(received.size(), filler + expected.size());
|
||||
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), received.begin() + filler));
|
||||
}
|
||||
|
||||
// Non-blocking socket pair with small buffers, so the writer fills like a stalled TCP connection
|
||||
class OverflowBufferTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
int fds[2];
|
||||
ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0);
|
||||
int size = 4096;
|
||||
ASSERT_EQ(::setsockopt(fds[0], SOL_SOCKET, SO_SNDBUF, &size, sizeof(size)), 0);
|
||||
ASSERT_EQ(::setsockopt(fds[1], SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)), 0);
|
||||
ASSERT_EQ(::fcntl(fds[1], F_SETFL, O_NONBLOCK), 0);
|
||||
this->reader_ = fds[1];
|
||||
this->sock_ = std::make_unique<socket::Socket>(fds[0]);
|
||||
ASSERT_EQ(this->sock_->setblocking(false), 0);
|
||||
}
|
||||
void TearDown() override { ::close(this->reader_); }
|
||||
|
||||
/// Write filler until the socket refuses; returns the bytes accepted
|
||||
size_t fill_pipe_() {
|
||||
uint8_t junk[512];
|
||||
std::memset(junk, 0xEE, sizeof(junk));
|
||||
size_t total = 0;
|
||||
for (;;) {
|
||||
ssize_t written = this->sock_->write(junk, sizeof(junk));
|
||||
if (written <= 0)
|
||||
break;
|
||||
total += static_cast<size_t>(written);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// Append whatever the pipe currently holds.
|
||||
void read_into_(std::vector<uint8_t> &out) {
|
||||
uint8_t tmp[1024];
|
||||
for (;;) {
|
||||
ssize_t n = ::read(this->reader_, tmp, sizeof(tmp));
|
||||
if (n <= 0)
|
||||
break;
|
||||
out.insert(out.end(), tmp, tmp + n);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain once; a refusal must be a would-block, never a hard error.
|
||||
ssize_t drain_(TestOverflowBuffer &buf) {
|
||||
ssize_t sent = buf.try_drain(this->sock_.get());
|
||||
if (sent == -1) {
|
||||
EXPECT_TRUE(errno == EWOULDBLOCK || errno == EAGAIN);
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
|
||||
/// Read and drain until the backlog is empty; returns all bytes received
|
||||
std::vector<uint8_t> drain_all_(TestOverflowBuffer &buf) {
|
||||
std::vector<uint8_t> received;
|
||||
for (int i = 0; i < 10000 && !buf.empty(); i++) {
|
||||
this->read_into_(received);
|
||||
// A hard socket error would never clear the backlog; stop instead of spinning
|
||||
if (this->drain_(buf) == -1 && errno != EWOULDBLOCK && errno != EAGAIN)
|
||||
break;
|
||||
}
|
||||
EXPECT_TRUE(buf.empty());
|
||||
this->read_into_(received);
|
||||
return received;
|
||||
}
|
||||
|
||||
struct Stall {
|
||||
size_t filler;
|
||||
std::vector<uint8_t> first, second, received;
|
||||
TestOverflowBuffer::Storage before;
|
||||
};
|
||||
/// Park two messages, then drain the first fully and the second part way
|
||||
void stall_mid_message_(TestOverflowBuffer &buf, Stall &s) {
|
||||
s.filler = this->fill_pipe_();
|
||||
s.first = make_message(1500, 20);
|
||||
ASSERT_GT(s.filler, s.first.size()); // the first message must drain in one go
|
||||
// Larger than the whole pipe, so a drain always stops inside it
|
||||
s.second = make_message(std::max<size_t>(s.filler + 1, std::min<size_t>(s.filler * 3, 12000)), 60);
|
||||
ASSERT_GT(s.second.size(), s.filler);
|
||||
ASSERT_TRUE(enqueue(buf, s.first));
|
||||
ASSERT_TRUE(enqueue(buf, s.second));
|
||||
s.before = buf.storage();
|
||||
this->read_into_(s.received);
|
||||
ASSERT_GT(this->drain_(buf), 0);
|
||||
ASSERT_EQ(buf.count(), 1);
|
||||
}
|
||||
|
||||
int reader_{-1};
|
||||
std::unique_ptr<socket::Socket> sock_;
|
||||
};
|
||||
|
||||
TEST_F(OverflowBufferTest, IdleBufferOwnsNoStorage) {
|
||||
TestOverflowBuffer buf;
|
||||
EXPECT_TRUE(buf.empty());
|
||||
EXPECT_EQ(buf.capacity(), 0u);
|
||||
EXPECT_EQ(buf.storage().data, nullptr);
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, StorageIsReusedAcrossStalls) {
|
||||
TestOverflowBuffer buf;
|
||||
auto msg = make_message(1000, 1);
|
||||
|
||||
size_t filler = this->fill_pipe_();
|
||||
ASSERT_TRUE(enqueue(buf, msg));
|
||||
const auto storage = buf.storage();
|
||||
EXPECT_GE(storage.capacity, msg.size() + TestOverflowBuffer::LEN_PREFIX);
|
||||
|
||||
for (int stall = 0; stall < 5; stall++) {
|
||||
expect_after_filler(this->drain_all_(buf), filler, msg);
|
||||
EXPECT_TRUE(buf.empty());
|
||||
// Same allocation every time: no free, no new allocation
|
||||
EXPECT_EQ(buf.storage(), storage);
|
||||
|
||||
filler = this->fill_pipe_();
|
||||
ASSERT_TRUE(enqueue(buf, msg));
|
||||
EXPECT_EQ(buf.storage(), storage);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, ReleaseWhileQueuedFreesOnceDrained) {
|
||||
TestOverflowBuffer buf;
|
||||
auto msg = make_message(1000, 7);
|
||||
|
||||
size_t filler = this->fill_pipe_();
|
||||
ASSERT_TRUE(enqueue(buf, msg));
|
||||
const size_t capacity = buf.capacity();
|
||||
|
||||
// Requested while the backlog still holds data: storage must stay until sent
|
||||
buf.release();
|
||||
EXPECT_FALSE(buf.empty());
|
||||
EXPECT_EQ(buf.capacity(), capacity);
|
||||
|
||||
expect_after_filler(this->drain_all_(buf), filler, msg);
|
||||
EXPECT_TRUE(buf.empty());
|
||||
EXPECT_EQ(buf.capacity(), 0u);
|
||||
EXPECT_EQ(buf.storage().data, nullptr);
|
||||
|
||||
// A later stall allocates again and keeps it, since nobody asked for a release
|
||||
filler = this->fill_pipe_();
|
||||
ASSERT_TRUE(enqueue(buf, msg));
|
||||
EXPECT_GT(buf.capacity(), 0u);
|
||||
this->drain_all_(buf);
|
||||
EXPECT_GT(buf.capacity(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, ReleaseWhenEmptyFreesImmediately) {
|
||||
TestOverflowBuffer buf;
|
||||
auto msg = make_message(100, 3);
|
||||
|
||||
this->fill_pipe_();
|
||||
ASSERT_TRUE(enqueue(buf, msg));
|
||||
this->drain_all_(buf);
|
||||
EXPECT_GT(buf.capacity(), 0u);
|
||||
|
||||
buf.release();
|
||||
EXPECT_EQ(buf.capacity(), 0u);
|
||||
EXPECT_EQ(buf.storage().data, nullptr);
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, PreservesOrderAndSkipsSentPrefix) {
|
||||
TestOverflowBuffer buf;
|
||||
auto first = make_message(700, 10);
|
||||
auto second_a = make_message(300, 50);
|
||||
auto second_b = make_message(400, 90);
|
||||
auto third = make_message(200, 130);
|
||||
|
||||
size_t filler = this->fill_pipe_();
|
||||
// 100 bytes of the first message were already accepted by the socket
|
||||
ASSERT_TRUE(enqueue(buf, first, 100));
|
||||
// Two iovecs with the skip covering all of the first one plus part of the second
|
||||
struct iovec iov[2] = {{second_a.data(), second_a.size()}, {second_b.data(), second_b.size()}};
|
||||
const uint16_t second_skip = static_cast<uint16_t>(second_a.size() + 5);
|
||||
ASSERT_TRUE(buf.enqueue_iov(iov, 2, static_cast<uint16_t>(second_a.size() + second_b.size()), second_skip));
|
||||
ASSERT_TRUE(enqueue(buf, third));
|
||||
EXPECT_EQ(buf.count(), 3);
|
||||
|
||||
// Nothing can go out while the pipe is full
|
||||
EXPECT_EQ(this->drain_(buf), -1);
|
||||
EXPECT_EQ(buf.count(), 3);
|
||||
|
||||
std::vector<uint8_t> expected;
|
||||
append(expected, first, 100);
|
||||
append(expected, second_b, 5);
|
||||
append(expected, third);
|
||||
expect_after_filler(this->drain_all_(buf), filler, expected);
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, RefusesWhenQueueIsFull) {
|
||||
TestOverflowBuffer buf;
|
||||
auto msg = make_message(16, 1);
|
||||
|
||||
size_t filler = this->fill_pipe_();
|
||||
for (int i = 0; i < API_MAX_SEND_QUEUE; i++) {
|
||||
ASSERT_TRUE(enqueue(buf, msg)) << "message " << i;
|
||||
}
|
||||
EXPECT_FALSE(enqueue(buf, msg));
|
||||
EXPECT_EQ(buf.count(), API_MAX_SEND_QUEUE);
|
||||
|
||||
// Draining frees the slots again
|
||||
std::vector<uint8_t> expected;
|
||||
for (int i = 0; i < API_MAX_SEND_QUEUE; i++)
|
||||
append(expected, msg);
|
||||
expect_after_filler(this->drain_all_(buf), filler, expected);
|
||||
this->fill_pipe_();
|
||||
EXPECT_TRUE(enqueue(buf, msg));
|
||||
EXPECT_EQ(buf.count(), 1);
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, SkipAtIovecBoundary) {
|
||||
TestOverflowBuffer buf;
|
||||
auto sent = make_message(300, 50);
|
||||
auto unsent = make_message(400, 90);
|
||||
|
||||
size_t filler = this->fill_pipe_();
|
||||
// The skip covers the first iovec exactly, so only the second is copied
|
||||
struct iovec iov[2] = {{sent.data(), sent.size()}, {unsent.data(), unsent.size()}};
|
||||
ASSERT_TRUE(
|
||||
buf.enqueue_iov(iov, 2, static_cast<uint16_t>(sent.size() + unsent.size()), static_cast<uint16_t>(sent.size())));
|
||||
EXPECT_EQ(buf.live(), unsent.size() + TestOverflowBuffer::LEN_PREFIX);
|
||||
expect_after_filler(this->drain_all_(buf), filler, unsent);
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, AppendsBehindSentPrefixWhenItFits) {
|
||||
TestOverflowBuffer buf;
|
||||
size_t filler = this->fill_pipe_();
|
||||
auto first = make_message(200, 20);
|
||||
// Size the second message so the two land half way into a 256 byte step,
|
||||
// leaving exactly 128 bytes of slack whatever the pipe accepted
|
||||
const size_t base = std::max<size_t>(filler + 1, std::min<size_t>(filler * 3, 12000));
|
||||
const size_t second_len = (base / 256 + 1) * 256 + 128 - first.size() - 2 * TestOverflowBuffer::LEN_PREFIX;
|
||||
auto second = make_message(second_len, 60);
|
||||
ASSERT_GT(second.size(), filler);
|
||||
ASSERT_TRUE(enqueue(buf, first));
|
||||
ASSERT_TRUE(enqueue(buf, second));
|
||||
const auto storage = buf.storage();
|
||||
const size_t slack = storage.capacity - first.size() - second.size() - 2 * TestOverflowBuffer::LEN_PREFIX;
|
||||
ASSERT_EQ(slack, 128u);
|
||||
auto third = make_message(slack - TestOverflowBuffer::LEN_PREFIX, 200);
|
||||
|
||||
std::vector<uint8_t> received;
|
||||
this->read_into_(received);
|
||||
ASSERT_GT(this->drain_(buf), 0);
|
||||
ASSERT_EQ(buf.count(), 1);
|
||||
const size_t live = buf.live();
|
||||
|
||||
// Fits in the tail, so the sent prefix is left alone
|
||||
ASSERT_TRUE(enqueue(buf, third));
|
||||
EXPECT_EQ(buf.storage(), storage);
|
||||
EXPECT_EQ(buf.live(), live + third.size() + TestOverflowBuffer::LEN_PREFIX);
|
||||
|
||||
append(received, this->drain_all_(buf));
|
||||
expect_after_filler(received, filler, concat({first, second, third}));
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, ReleaseSurvivesFurtherEnqueues) {
|
||||
TestOverflowBuffer buf;
|
||||
auto first = make_message(300, 7);
|
||||
auto second = make_message(300, 70);
|
||||
|
||||
size_t filler = this->fill_pipe_();
|
||||
ASSERT_TRUE(enqueue(buf, first));
|
||||
buf.release();
|
||||
ASSERT_TRUE(enqueue(buf, second));
|
||||
EXPECT_GT(buf.capacity(), 0u);
|
||||
|
||||
expect_after_filler(this->drain_all_(buf), filler, concat({first, second}));
|
||||
EXPECT_EQ(buf.capacity(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, RefusesWhenByteLimitIsExceeded) {
|
||||
TestOverflowBuffer buf;
|
||||
// Two of these fill the byte budget exactly, well before the slot count is reached
|
||||
static_assert(API_MAX_SEND_QUEUE >= 3);
|
||||
auto msg = make_message(TestOverflowBuffer::MAX_BYTES / 2 - TestOverflowBuffer::LEN_PREFIX, 1);
|
||||
|
||||
this->fill_pipe_();
|
||||
ASSERT_TRUE(enqueue(buf, msg));
|
||||
ASSERT_TRUE(enqueue(buf, msg));
|
||||
EXPECT_FALSE(enqueue(buf, msg));
|
||||
EXPECT_EQ(buf.count(), 2);
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, LoneMessageMayExceedByteLimit) {
|
||||
TestOverflowBuffer buf;
|
||||
// The oversized message must still fit under the lone message ceiling
|
||||
static_assert(TestOverflowBuffer::MAX_BYTES + 100 + TestOverflowBuffer::LEN_PREFIX <=
|
||||
TestOverflowBuffer::MAX_LONE_BYTES);
|
||||
auto big = make_message(TestOverflowBuffer::MAX_BYTES + 100, 5);
|
||||
auto small = make_message(16, 9);
|
||||
|
||||
// Refusing the only message would drop the connection for nothing
|
||||
size_t filler = this->fill_pipe_();
|
||||
ASSERT_TRUE(enqueue(buf, big));
|
||||
EXPECT_EQ(buf.count(), 1);
|
||||
// With a backlog present the byte limit applies again
|
||||
EXPECT_FALSE(enqueue(buf, small));
|
||||
EXPECT_EQ(buf.count(), 1);
|
||||
|
||||
expect_after_filler(this->drain_all_(buf), filler, big);
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, LoneMessageAboveOffsetLimitIsRefused) {
|
||||
TestOverflowBuffer buf;
|
||||
// Payload plus prefix is past the lone message ceiling
|
||||
auto msg = make_message(TestOverflowBuffer::MAX_LONE_BYTES, 3);
|
||||
|
||||
this->fill_pipe_();
|
||||
EXPECT_FALSE(enqueue(buf, msg));
|
||||
EXPECT_TRUE(buf.empty());
|
||||
EXPECT_EQ(buf.capacity(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, HardSocketErrorLeavesBacklogIntact) {
|
||||
TestOverflowBuffer buf;
|
||||
auto msg = make_message(300, 40);
|
||||
|
||||
this->fill_pipe_();
|
||||
ASSERT_TRUE(enqueue(buf, msg));
|
||||
// A closed socket fails every write outright, unlike a full one
|
||||
ASSERT_EQ(this->sock_->close(), 0);
|
||||
|
||||
errno = 0;
|
||||
EXPECT_EQ(buf.try_drain(this->sock_.get()), -1);
|
||||
EXPECT_NE(errno, EWOULDBLOCK);
|
||||
EXPECT_NE(errno, EAGAIN);
|
||||
EXPECT_EQ(buf.count(), 1);
|
||||
EXPECT_EQ(buf.live(), msg.size() + TestOverflowBuffer::LEN_PREFIX);
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, GrowsWhileReclaimingSentPrefix) {
|
||||
TestOverflowBuffer buf;
|
||||
Stall s;
|
||||
ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s));
|
||||
|
||||
// One byte too many to fit even after the sent prefix is reclaimed: grows in one copy
|
||||
auto third = make_message(s.before.capacity - buf.live() + 1, 200);
|
||||
ASSERT_TRUE(enqueue(buf, third));
|
||||
EXPECT_GT(buf.capacity(), s.before.capacity);
|
||||
EXPECT_EQ(buf.count(), 2);
|
||||
|
||||
append(s.received, this->drain_all_(buf));
|
||||
expect_after_filler(s.received, s.filler, concat({s.first, s.second, third}));
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, NestedDrainMakesNoProgress) {
|
||||
TestOverflowBuffer buf;
|
||||
auto msg = make_message(300, 40);
|
||||
|
||||
size_t filler = this->fill_pipe_();
|
||||
ASSERT_TRUE(enqueue(buf, msg));
|
||||
std::vector<uint8_t> received;
|
||||
this->read_into_(received);
|
||||
|
||||
// Room is available, but a nested drain must leave the outer one's message alone
|
||||
buf.set_draining(true);
|
||||
EXPECT_EQ(this->drain_(buf), 0);
|
||||
EXPECT_EQ(buf.count(), 1);
|
||||
std::vector<uint8_t> nothing;
|
||||
this->read_into_(nothing);
|
||||
EXPECT_TRUE(nothing.empty());
|
||||
|
||||
buf.set_draining(false);
|
||||
append(received, this->drain_all_(buf));
|
||||
expect_after_filler(received, filler, msg);
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, NestedEnqueueAppendsWithinCapacity) {
|
||||
TestOverflowBuffer buf;
|
||||
auto first = make_message(500, 10);
|
||||
auto second = make_message(4, 90);
|
||||
|
||||
size_t filler = this->fill_pipe_();
|
||||
ASSERT_TRUE(enqueue(buf, first));
|
||||
const auto storage = buf.storage();
|
||||
ASSERT_GE(storage.capacity, first.size() + second.size() + 2 * TestOverflowBuffer::LEN_PREFIX);
|
||||
|
||||
buf.set_draining(true);
|
||||
EXPECT_TRUE(enqueue(buf, second));
|
||||
EXPECT_EQ(buf.count(), 2);
|
||||
EXPECT_EQ(buf.storage(), storage);
|
||||
buf.set_draining(false);
|
||||
|
||||
expect_after_filler(this->drain_all_(buf), filler, concat({first, second}));
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, NestedEnqueueRefusesToGrow) {
|
||||
TestOverflowBuffer buf;
|
||||
auto first = make_message(500, 10);
|
||||
auto second = make_message(100, 90);
|
||||
|
||||
size_t filler = this->fill_pipe_();
|
||||
ASSERT_TRUE(enqueue(buf, first));
|
||||
const auto storage = buf.storage();
|
||||
ASSERT_LT(storage.capacity, first.size() + second.size() + 2 * TestOverflowBuffer::LEN_PREFIX);
|
||||
|
||||
// Growing would free the bytes the outer write() is sending from
|
||||
buf.set_draining(true);
|
||||
EXPECT_FALSE(enqueue(buf, second));
|
||||
EXPECT_EQ(buf.count(), 1);
|
||||
EXPECT_EQ(buf.storage(), storage);
|
||||
buf.set_draining(false);
|
||||
|
||||
expect_after_filler(this->drain_all_(buf), filler, first);
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, NestedEnqueueRefusesToCompact) {
|
||||
TestOverflowBuffer buf;
|
||||
Stall s;
|
||||
ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s));
|
||||
auto third = make_message(1000, 200);
|
||||
|
||||
// Sliding the remainder down would move the bytes the outer write() points at
|
||||
buf.set_draining(true);
|
||||
EXPECT_FALSE(enqueue(buf, third));
|
||||
EXPECT_EQ(buf.count(), 1);
|
||||
EXPECT_EQ(buf.storage(), s.before);
|
||||
buf.set_draining(false);
|
||||
|
||||
// Once the drain is over the same enqueue compacts and succeeds
|
||||
ASSERT_TRUE(enqueue(buf, third));
|
||||
EXPECT_EQ(buf.storage(), s.before);
|
||||
append(s.received, this->drain_all_(buf));
|
||||
expect_after_filler(s.received, s.filler, concat({s.first, s.second, third}));
|
||||
}
|
||||
|
||||
TEST_F(OverflowBufferTest, CompactsInsteadOfGrowingAfterPartialDrain) {
|
||||
TestOverflowBuffer buf;
|
||||
Stall s;
|
||||
ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s));
|
||||
auto third = make_message(1000, 200);
|
||||
|
||||
// The sent first message is reclaimed by sliding the remainder down, not by reallocating
|
||||
ASSERT_TRUE(enqueue(buf, third));
|
||||
EXPECT_EQ(buf.storage(), s.before);
|
||||
|
||||
append(s.received, this->drain_all_(buf));
|
||||
expect_after_filler(s.received, s.filler, concat({s.first, s.second, third}));
|
||||
}
|
||||
|
||||
} // namespace esphome::api::testing
|
||||
#endif // USE_HOST
|
||||
@@ -0,0 +1,5 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.dependencies = manifest.dependencies + ["sensor", "spi"]
|
||||
@@ -0,0 +1,62 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/atm90e32/atm90e32.h"
|
||||
|
||||
namespace esphome::atm90e32::testing {
|
||||
|
||||
TEST(ATM90E32OffsetRegisterVerification, AcceptsExactSignedReadback) {
|
||||
EXPECT_TRUE(offset_register_value_matches(0x007B, 123));
|
||||
EXPECT_TRUE(offset_register_value_matches(0xFF85, -123));
|
||||
}
|
||||
|
||||
TEST(ATM90E32OffsetRegisterVerification, RejectsMismatchedReadback) {
|
||||
EXPECT_FALSE(offset_register_value_matches(0x007C, 123));
|
||||
EXPECT_FALSE(offset_register_value_matches(0xFF84, -123));
|
||||
}
|
||||
|
||||
TEST(ATM90E32OffsetRestoreState, ReportsVerifiedStoredValuesAsRestored) {
|
||||
const auto state = resolve_offset_restore_state(true, true, false);
|
||||
|
||||
EXPECT_TRUE(state.restored);
|
||||
EXPECT_TRUE(state.values_verified);
|
||||
}
|
||||
|
||||
TEST(ATM90E32OffsetRestoreState, ReportsVerifiedConfigFallbackAsNotRestored) {
|
||||
const auto state = resolve_offset_restore_state(true, false, true);
|
||||
|
||||
EXPECT_FALSE(state.restored);
|
||||
EXPECT_TRUE(state.values_verified);
|
||||
}
|
||||
|
||||
TEST(ATM90E32OffsetRestoreState, ReportsFailedConfigFallbackAsUnverified) {
|
||||
const auto state = resolve_offset_restore_state(true, false, false);
|
||||
|
||||
EXPECT_FALSE(state.restored);
|
||||
EXPECT_FALSE(state.values_verified);
|
||||
}
|
||||
|
||||
TEST(ATM90E32OffsetRestoreState, ReportsConfigWithoutStoredValuesAsNotRestored) {
|
||||
const auto state = resolve_offset_restore_state(false, true, false);
|
||||
|
||||
EXPECT_FALSE(state.restored);
|
||||
EXPECT_TRUE(state.values_verified);
|
||||
}
|
||||
|
||||
TEST(ATM90E32OffsetPersistence, RollsBackStoredValuesOrZeroSentinel) {
|
||||
const OffsetCalibration previous[3]{{1, -1}, {2, -2}, {3, -3}};
|
||||
OffsetCalibration rollback[3]{};
|
||||
|
||||
prepare_offset_rollback(previous, true, rollback);
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
EXPECT_EQ(rollback[phase].first_offset, previous[phase].first_offset);
|
||||
EXPECT_EQ(rollback[phase].second_offset, previous[phase].second_offset);
|
||||
}
|
||||
|
||||
prepare_offset_rollback(previous, false, rollback);
|
||||
for (const auto &phase : rollback) {
|
||||
EXPECT_EQ(phase.first_offset, 0);
|
||||
EXPECT_EQ(phase.second_offset, 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::atm90e32::testing
|
||||
@@ -6,15 +6,14 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# close_service_batch compiles only under USE_BLUETOOTH_PROXY_CONNECTIONS;
|
||||
# emit the backend define so the host build exercises it.
|
||||
async def to_code_testing(config):
|
||||
# These defines are global to the merged host test binary; safe
|
||||
# because no co-compiled test observes them.
|
||||
# These defines are global to the merged host test binary. The api sources are
|
||||
# compiled in it too (the api tests define USE_API), and USE_BLUETOOTH_PROXY would make
|
||||
# them include and call bluetooth_proxy, which has no host build without a BLE hub.
|
||||
cg.add_define("USE_BLE_GATT_CLIENT")
|
||||
cg.add_define("USE_BLE_GATT_CLIENT_STUB_BACKEND")
|
||||
cg.add_define("USE_BLUETOOTH_PROXY")
|
||||
# Gates the connection half of the API surface, which is what
|
||||
# close_service_batch and the GATT response types live behind.
|
||||
cg.add_define("USE_BLUETOOTH_PROXY_CONNECTIONS")
|
||||
cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16)
|
||||
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 1)
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Compile the gated filter path; no external component is in-tree to call
|
||||
# enable_advertisement_filter(), so the define is forced here.
|
||||
<<: !include common.yaml
|
||||
|
||||
esphome:
|
||||
build_flags:
|
||||
- "-DUSE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER"
|
||||
|
||||
esp32_ble_tracker:
|
||||
|
||||
bluetooth_proxy:
|
||||
active: true
|
||||
@@ -0,0 +1,18 @@
|
||||
tinyusb:
|
||||
id: tinyusb_test
|
||||
usb_lang_id: 0x0123
|
||||
usb_manufacturer_str: ESPHomeTestManufacturer
|
||||
usb_product_id: 0x1234
|
||||
usb_product_str: ESPHomeTestProduct
|
||||
usb_serial_str: ESPHomeTestSerialNumber
|
||||
usb_vendor_id: 0x2345
|
||||
|
||||
uart:
|
||||
- id: uart_0
|
||||
tx_pin: 14
|
||||
rx_pin: 13
|
||||
baud_rate: 115200
|
||||
|
||||
usb_cdc_acm:
|
||||
interfaces:
|
||||
- id: cdc_acm_1
|
||||
@@ -0,0 +1,12 @@
|
||||
# Second UART/CDC pair for a two-bridge setup. Kept out of common.yaml because the
|
||||
# ESP32-S2 has only two UART controllers and the logger occupies one, so a second
|
||||
# uart there would fail at runtime.
|
||||
uart:
|
||||
- id: uart_1
|
||||
tx_pin: 15
|
||||
rx_pin: 16
|
||||
baud_rate: 115200
|
||||
|
||||
usb_cdc_acm:
|
||||
interfaces:
|
||||
- id: cdc_acm_2
|
||||
@@ -0,0 +1,15 @@
|
||||
packages:
|
||||
cdc_acm_uart: !include common.yaml
|
||||
cdc_acm_uart_dual: !include common_dual.yaml
|
||||
|
||||
bridge:
|
||||
- platform: cdc_acm_uart
|
||||
uart_id: uart_0
|
||||
usb_cdc_acm_id: cdc_acm_1
|
||||
dtr_pin: 40
|
||||
rts_pin: 41
|
||||
- platform: cdc_acm_uart
|
||||
uart_id: uart_1
|
||||
usb_cdc_acm_id: cdc_acm_2
|
||||
dtr_pin: 20
|
||||
rts_pin: 21
|
||||
@@ -0,0 +1,14 @@
|
||||
# ESP32-S2 has no USB_SERIAL_JTAG, so the logger defaults to USB_CDC, which shares
|
||||
# the USB OTG peripheral with tinyusb. Use a hardware UART for logging instead.
|
||||
logger:
|
||||
hardware_uart: UART0
|
||||
|
||||
packages:
|
||||
cdc_acm_uart: !include common.yaml
|
||||
|
||||
bridge:
|
||||
- platform: cdc_acm_uart
|
||||
uart_id: uart_0
|
||||
usb_cdc_acm_id: cdc_acm_1
|
||||
dtr_pin: 40
|
||||
rts_pin: 41
|
||||
@@ -0,0 +1,17 @@
|
||||
packages:
|
||||
cdc_acm_uart: !include common.yaml
|
||||
cdc_acm_uart_dual: !include common_dual.yaml
|
||||
|
||||
bridge:
|
||||
- platform: cdc_acm_uart
|
||||
uart_id: uart_0
|
||||
usb_cdc_acm_id: cdc_acm_1
|
||||
dtr_pin: 40
|
||||
rts_pin: 41
|
||||
- platform: cdc_acm_uart
|
||||
uart_id: uart_1
|
||||
usb_cdc_acm_id: cdc_acm_2
|
||||
# GPIO19/20 are USB D-/D+ on the S3 (which the CDC side itself uses); use
|
||||
# unrelated free pins here.
|
||||
dtr_pin: 17
|
||||
rts_pin: 18
|
||||
@@ -30,8 +30,7 @@ climate:
|
||||
- switch.turn_on: climate_heater_switch
|
||||
- switch.turn_off: climate_cooler_switch
|
||||
# Thermostat-based climate so climate.control: action variants get build
|
||||
# coverage (bang_bang doesn't support fan modes, presets, etc.). Climate
|
||||
# has no template platform, so thermostat is the right vehicle.
|
||||
# coverage (bang_bang doesn't support fan modes, presets, etc.).
|
||||
- platform: thermostat
|
||||
id: climate_test_thermostat
|
||||
name: Test Thermostat
|
||||
|
||||
@@ -348,4 +348,75 @@ TEST(StepToAccuracyDecimals, NonFiniteAndZero) {
|
||||
EXPECT_EQ(step_to_accuracy_decimals(-INFINITY), 0);
|
||||
}
|
||||
|
||||
// --- FixedVector::try_init() ---
|
||||
|
||||
// Keeps the block observable, else the compiler may drop the malloc and free pair and fold the check
|
||||
static void escape(const void *p) { asm volatile("" : : "g"(p) : "memory"); }
|
||||
|
||||
TEST(FixedVectorTryInit, ReportsExhaustionAndStaysEmpty) {
|
||||
FixedVector<uint32_t> v;
|
||||
const bool ok = v.try_init(SIZE_MAX / sizeof(uint32_t));
|
||||
escape(&v);
|
||||
EXPECT_FALSE(ok);
|
||||
EXPECT_EQ(v.capacity(), 0u);
|
||||
EXPECT_FALSE(v.try_init(SIZE_MAX / sizeof(uint32_t) + 1)); // byte count would wrap
|
||||
EXPECT_EQ(v.capacity(), 0u);
|
||||
EXPECT_TRUE(v.try_init(0));
|
||||
EXPECT_TRUE(v.try_init(4));
|
||||
v.push_back(7);
|
||||
EXPECT_EQ(v.size(), 1u);
|
||||
}
|
||||
|
||||
// --- RAMAllocator::make_unique() ---
|
||||
|
||||
namespace {
|
||||
struct Probe {
|
||||
static inline int live = 0;
|
||||
int a;
|
||||
int b;
|
||||
Probe(int a, int b) : a(a), b(b) { live++; }
|
||||
~Probe() { live--; }
|
||||
};
|
||||
} // namespace
|
||||
|
||||
static_assert(sizeof(RAMUniquePtr<Probe>) == sizeof(Probe *), "the deleter must not add storage");
|
||||
|
||||
TEST(RAMAllocatorMakeUnique, ForwardsArgsAndDestroysOnce) {
|
||||
auto p = RAMAllocator<Probe>().make_unique(3, 4);
|
||||
ASSERT_NE(p, nullptr);
|
||||
EXPECT_EQ(p->a, 3);
|
||||
EXPECT_EQ(p->b, 4);
|
||||
EXPECT_EQ(Probe::live, 1);
|
||||
p.reset();
|
||||
EXPECT_EQ(Probe::live, 0);
|
||||
}
|
||||
|
||||
TEST(RAMAllocatorMakeUnique, ValueInitializesLikeMakeUnique) {
|
||||
struct Plain {
|
||||
uint32_t words[8];
|
||||
};
|
||||
// Dirty a block of the same size first so a recycled allocation is not zero by chance
|
||||
auto dirty = RAMAllocator<uint8_t>().make_unique_array_for_overwrite(sizeof(Plain));
|
||||
std::memset(dirty.get(), 0xFF, sizeof(Plain));
|
||||
dirty.reset();
|
||||
auto p = RAMAllocator<Plain>().make_unique();
|
||||
ASSERT_NE(p, nullptr);
|
||||
// Under ASan fresh blocks are filled with 0xbe, so this holds even when the dirtied block is not reused
|
||||
EXPECT_TRUE(std::all_of(std::begin(p->words), std::end(p->words), [](uint32_t w) { return w == 0; }));
|
||||
}
|
||||
|
||||
TEST(RAMAllocatorMakeUnique, ArrayFormRejectsOverflowAndZero) {
|
||||
EXPECT_EQ(RAMAllocator<uint32_t>().make_unique_array_for_overwrite(SIZE_MAX / sizeof(uint32_t) + 1), nullptr);
|
||||
EXPECT_EQ(RAMAllocator<uint32_t>().make_unique_array_for_overwrite(0), nullptr);
|
||||
EXPECT_NE(RAMAllocator<uint32_t>().make_unique_array_for_overwrite(1), nullptr);
|
||||
}
|
||||
|
||||
TEST(RAMAllocatorMakeUnique, ArrayFormAllocatesElements) {
|
||||
RAMUniquePtr<uint8_t[]> buf = RAMAllocator<uint8_t>().make_unique_array_for_overwrite(256);
|
||||
ASSERT_NE(buf, nullptr);
|
||||
std::memset(buf.get(), 0xA5, 256);
|
||||
EXPECT_EQ(buf[0], 0xA5);
|
||||
EXPECT_EQ(buf[255], 0xA5);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
|
||||
@@ -3,6 +3,6 @@ substitutions:
|
||||
rx_pin: GPIO14
|
||||
|
||||
packages:
|
||||
uart_38400: !include ../../test_build_components/common/uart_38400/esp32-idf.yaml
|
||||
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -3,6 +3,6 @@ substitutions:
|
||||
rx_pin: GPIO3
|
||||
|
||||
packages:
|
||||
uart_38400: !include ../../test_build_components/common/uart_38400/esp8266-ard.yaml
|
||||
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -3,6 +3,6 @@ substitutions:
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
uart_38400: !include ../../test_build_components/common/uart_38400/rp2040-ard.yaml
|
||||
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -17,6 +17,8 @@ esp32:
|
||||
disable_dev_null_vfs: true
|
||||
disable_mbedtls_peer_cert: true
|
||||
disable_mbedtls_pkcs7: true
|
||||
disable_mbedtls_tls_server: true
|
||||
disable_mbedtls_tls_extras: true
|
||||
disable_regi2c_in_iram: true
|
||||
disable_fatfs: true
|
||||
sram1_as_iram: true
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Exercises the ESP-NOW-over-hosted shim: on the ESP32-P4 host, esp32_hosted
|
||||
# supplies the esp_now_* symbols that the espnow component links against.
|
||||
packages:
|
||||
esp32_hosted: !include common.yaml
|
||||
espnow: !include ../espnow/common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
substitutions:
|
||||
verify_ssl: "true"
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,4 +1,4 @@
|
||||
packages:
|
||||
uart_1200: !include ../../test_build_components/common/uart_1200/esp32-idf.yaml
|
||||
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -3,6 +3,6 @@ substitutions:
|
||||
uart_rx_pin: GPIO3
|
||||
|
||||
packages:
|
||||
uart_1200: !include ../../test_build_components/common/uart_1200/esp8266-ard.yaml
|
||||
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -30,6 +30,18 @@ binary_sensor:
|
||||
widget: button_button
|
||||
state: pressed
|
||||
|
||||
globals:
|
||||
- id: counter
|
||||
type: int
|
||||
|
||||
script:
|
||||
- id: add_row
|
||||
then:
|
||||
- lvgl.list.add:
|
||||
id: test_list_id
|
||||
label:
|
||||
text: row
|
||||
|
||||
lvgl:
|
||||
id: lvgl_id
|
||||
rotation: 90
|
||||
@@ -1291,7 +1303,7 @@ lvgl:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "table selected row %u col %u"
|
||||
args: [row, column]
|
||||
args: [(unsigned)row, (unsigned)column]
|
||||
on_click:
|
||||
then:
|
||||
- lvgl.table.cell.update:
|
||||
@@ -1347,10 +1359,12 @@ lvgl:
|
||||
- logger.log:
|
||||
format: "list entry added at %d"
|
||||
args: [list_index]
|
||||
- lambda: "id(counter)++;"
|
||||
on_remove:
|
||||
- logger.log:
|
||||
format: "list entry removed at %d"
|
||||
args: [list_index]
|
||||
- lambda: "id(counter)--;"
|
||||
on_click:
|
||||
- lvgl.list.add_text:
|
||||
id: test_list_id
|
||||
|
||||
@@ -68,6 +68,14 @@ class Initiator {
|
||||
|
||||
static const uint8_t PROLOGUE[] = {'t', 'e', 's', 't', 'p', 'r', 'o', 'l', 'o', 'g', 'u', 'e'};
|
||||
|
||||
// The context only points at the key and init() copies it before returning,
|
||||
// so a temporary context over a temporary key is safe within one call
|
||||
static NoiseContext ctx_for(const psk_t &psk) {
|
||||
NoiseContext ctx;
|
||||
ctx.set_psk(psk.data());
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static psk_t make_psk(uint8_t seed) {
|
||||
psk_t psk;
|
||||
for (size_t i = 0; i < psk.size(); i++) {
|
||||
@@ -102,7 +110,7 @@ TEST(NoiseResponderHandshakeTest, MessageMethodsErrorBeforeInit) {
|
||||
TEST(NoiseResponderHandshakeTest, FullHandshakeAndTransportRoundTrip) {
|
||||
const psk_t psk = make_psk(7);
|
||||
NoiseResponderHandshake responder;
|
||||
ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
ASSERT_EQ(responder.init(ctx_for(psk), PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
EXPECT_EQ(responder.action(), Action::ACTION_READ);
|
||||
|
||||
Initiator initiator(psk, PROLOGUE, sizeof(PROLOGUE));
|
||||
@@ -155,8 +163,8 @@ TEST(NoiseResponderHandshakeTest, ReInitRestartsHandshake) {
|
||||
// proves the restart took effect; the old state surviving would fail the
|
||||
// MAC here.
|
||||
NoiseResponderHandshake responder;
|
||||
ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
ASSERT_EQ(responder.init(make_psk(9), PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
ASSERT_EQ(responder.init(ctx_for(make_psk(7)), PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
ASSERT_EQ(responder.init(ctx_for(make_psk(9)), PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
EXPECT_EQ(responder.action(), Action::ACTION_READ);
|
||||
|
||||
Initiator initiator(make_psk(9), PROLOGUE, sizeof(PROLOGUE));
|
||||
@@ -168,7 +176,7 @@ TEST(NoiseResponderHandshakeTest, ReInitRestartsHandshake) {
|
||||
|
||||
TEST(NoiseResponderHandshakeTest, WrongPskFailsWithMacFailure) {
|
||||
NoiseResponderHandshake responder;
|
||||
ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
ASSERT_EQ(responder.init(ctx_for(make_psk(7)), PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
|
||||
Initiator initiator(make_psk(200), PROLOGUE, sizeof(PROLOGUE));
|
||||
uint8_t msg[MAX_HANDSHAKE_SIZE];
|
||||
@@ -185,7 +193,7 @@ TEST(NoiseResponderHandshakeTest, MismatchedPrologueFailsWithMacFailure) {
|
||||
// tampered preamble must fail even with the right key.
|
||||
const psk_t psk = make_psk(7);
|
||||
NoiseResponderHandshake responder;
|
||||
ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
ASSERT_EQ(responder.init(ctx_for(psk), PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
|
||||
static const uint8_t TAMPERED[] = {'x'};
|
||||
Initiator initiator(psk, TAMPERED, sizeof(TAMPERED));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user