Merge branch 'wifi-fast-connect-substitution-string' into integration

This commit is contained in:
J. Nick Koston
2026-07-05 23:00:39 -05:00
40 changed files with 693 additions and 48 deletions
+3 -2
View File
@@ -4,5 +4,6 @@ substitutions:
reset_pin: "21"
packages:
- !include ../../test_build_components/common/i2c/esp32-idf.yaml
- !include common.yaml
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
@@ -2,3 +2,5 @@ nrf52:
dfu: true
reg0:
voltage: 1.8V
framework:
libc_nano: false
@@ -0,0 +1,4 @@
# Exercises the opt-in that compiles the RTC-backed preference storage into
# the ESP32 backend without any other option selecting it.
preferences:
rtc_storage: true
@@ -0,0 +1,4 @@
# Exercises the ESP32 RTC-backed preferences path (storage: rtc) for safe_mode.
safe_mode:
num_attempts: 3
storage: rtc
@@ -0,0 +1,7 @@
# Exercises the dict form of fast_connect with RTC-backed preference storage.
wifi:
ssid: MySSID
password: password1
fast_connect:
enabled: true
storage: rtc
@@ -0,0 +1,7 @@
# Exercises the dict form of fast_connect overriding the ESP8266 default (rtc)
# back to flash storage.
wifi:
ssid: MySSID
password: password1
fast_connect:
storage: flash
@@ -0,0 +1,9 @@
# fast_connect passed through a substitution arrives as a string ("false"),
# which must be accepted like the historic plain boolean form.
substitutions:
fast_connect_value: "false"
wifi:
ssid: MySSID
password: password1
fast_connect: ${fast_connect_value}
+23
View File
@@ -243,6 +243,7 @@ def test_get_project_cmakelists_no_cpp_standard(tmp_path: Path) -> None:
patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"),
patch.object(CORE, "name", "test"),
patch.object(CORE, "cpp_standard", None),
patch.object(CORE, "cxx_build_flags", set()),
):
from esphome.build_gen.espidf import get_project_cmakelists
@@ -251,6 +252,28 @@ def test_get_project_cmakelists_no_cpp_standard(tmp_path: Path) -> None:
assert "CXX_COMPILE_OPTIONS" not in content
def test_get_project_cmakelists_cxx_build_flags(tmp_path: Path) -> None:
"""Flags registered via cg.add_cxx_build_flag() are appended to
CXX_COMPILE_OPTIONS (C++-only, GCC warns if they reach C compiles)
between include(project.cmake) and project()."""
with (
patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"),
patch.object(CORE, "name", "test"),
patch.object(CORE, "cpp_standard", None),
patch.object(CORE, "cxx_build_flags", {"-Wno-volatile"}),
):
from esphome.build_gen.espidf import get_project_cmakelists
content = get_project_cmakelists(minimal=True)
flag_line = 'idf_build_set_property(CXX_COMPILE_OPTIONS "-Wno-volatile" APPEND)'
assert flag_line in content
include_pos = content.index("tools/cmake/project.cmake")
flag_pos = content.index(flag_line)
project_pos = content.index("project(test)")
assert include_pos < flag_pos < project_pos
def test_get_component_cmakelists_no_compile_features() -> None:
"""The C++ standard is pinned project-wide via CXX_COMPILE_OPTIONS in the
top-level CMakeLists; the src component must not set its own."""
@@ -200,3 +200,32 @@ def test_get_ini_content_no_cpp_standard(
content = platformio.get_ini_content()
assert "-std=" not in content
def test_write_cxx_flags_script_emits_registered_flags(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Flags registered via cg.add_cxx_build_flag() are emitted as CXXFLAGS,
sorted, so they apply to C++ compiles only."""
CORE.build_path = str(tmp_path)
monkeypatch.setattr(CORE, "cxx_build_flags", {"-Wno-volatile", "-Wno-deprecated"})
platformio.write_cxx_flags_script()
content = (tmp_path / platformio.CXX_FLAGS_FILE_NAME).read_text()
assert (
'env.Append(CXXFLAGS=["-Wno-deprecated"])\n'
'env.Append(CXXFLAGS=["-Wno-volatile"])\n'
) in content
def test_write_cxx_flags_script_no_flags(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
CORE.build_path = str(tmp_path)
monkeypatch.setattr(CORE, "cxx_build_flags", set())
platformio.write_cxx_flags_script()
content = (tmp_path / platformio.CXX_FLAGS_FILE_NAME).read_text()
assert "CXXFLAGS" not in content
@@ -26,6 +26,7 @@ from esphome.framework_helpers import (
create_venv,
download_from_mirrors,
get_project_compile_flags,
get_project_cxx_compile_flags,
get_project_link_flags,
get_python_env_executable_path,
get_system_python_path,
@@ -1048,3 +1049,25 @@ class TestGetProjectLinkFlags:
):
result = get_project_link_flags()
assert result == sorted(result)
def _make_core_cxx(flags: set[str]) -> MagicMock:
core = MagicMock()
core.cxx_build_flags = flags
return core
class TestGetProjectCxxCompileFlags:
def test_returns_sorted_flags(self) -> None:
with patch(
"esphome.core.CORE",
_make_core_cxx({"-Wno-volatile", "-Wno-deprecated"}),
):
assert get_project_cxx_compile_flags() == [
"-Wno-deprecated",
"-Wno-volatile",
]
def test_empty_flags(self) -> None:
with patch("esphome.core.CORE", _make_core_cxx(set())):
assert get_project_cxx_compile_flags() == []
+149
View File
@@ -0,0 +1,149 @@
"""Tests for esphome.preferences storage backend selection."""
import pytest
from esphome import preferences
from esphome.components.esp32 import KEY_ESP32
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C61,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_STORAGE,
KEY_CORE,
KEY_TARGET_PLATFORM,
KEY_VARIANT,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
)
from esphome.core import CORE
def _set_platform(platform: str) -> None:
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
def _set_esp32(variant: str) -> None:
_set_platform(PLATFORM_ESP32)
CORE.data[KEY_ESP32] = {KEY_VARIANT: variant}
def _validate(value: dict):
return cv.Schema(preferences.storage_schema())(value)
def _define_names() -> set[str]:
return {define.name for define in CORE.defines}
def test_is_in_flash() -> None:
_set_platform(PLATFORM_ESP8266)
assert preferences.is_in_flash(preferences.STORAGE_FLASH) is True
assert preferences.is_in_flash(preferences.STORAGE_RTC) is False
# The RTC storage define is ESP32-specific.
assert "USE_ESP32_RTC_PREFERENCES" not in _define_names()
def test_is_in_flash_esp32_rtc_emits_define() -> None:
_set_esp32(VARIANT_ESP32)
assert preferences.is_in_flash(preferences.STORAGE_FLASH) is True
assert "USE_ESP32_RTC_PREFERENCES" not in _define_names()
assert preferences.is_in_flash(preferences.STORAGE_RTC) is False
assert "USE_ESP32_RTC_PREFERENCES" in _define_names()
def test_request_rtc_storage_esp32_only() -> None:
_set_platform(PLATFORM_ESP8266)
preferences.request_rtc_storage()
# ESP8266 always has its RTC backend; no define is needed or emitted.
assert "USE_ESP32_RTC_PREFERENCES" not in _define_names()
def test_request_rtc_storage_esp32_emits_define() -> None:
_set_esp32(VARIANT_ESP32)
preferences.request_rtc_storage()
assert "USE_ESP32_RTC_PREFERENCES" in _define_names()
@pytest.mark.parametrize("variant", [VARIANT_ESP32, VARIANT_ESP32C3])
def test_validate_rtc_storage_accepted(variant: str) -> None:
_set_esp32(variant)
assert preferences.validate_rtc_storage(True) is True
assert preferences.validate_rtc_storage(False) is False
def test_validate_rtc_storage_esp8266() -> None:
_set_platform(PLATFORM_ESP8266)
# Tolerated no-op: the ESP8266 backend always has RTC storage.
assert preferences.validate_rtc_storage(True) is True
# But it cannot be disabled, so an explicit false is an error.
with pytest.raises(cv.Invalid, match="always enabled on ESP8266"):
preferences.validate_rtc_storage(False)
@pytest.mark.parametrize("variant", [VARIANT_ESP32C2, VARIANT_ESP32C61])
def test_validate_rtc_storage_rejected_without_rtc_memory(variant: str) -> None:
_set_esp32(variant)
with pytest.raises(cv.Invalid, match="not supported on this platform"):
preferences.validate_rtc_storage(True)
# Disabling it is always fine.
assert preferences.validate_rtc_storage(False) is False
def test_validate_rtc_storage_rejected_on_unsupported_platform() -> None:
_set_platform(PLATFORM_RP2040)
with pytest.raises(cv.Invalid, match="not supported on this platform"):
preferences.validate_rtc_storage(True)
@pytest.mark.parametrize(
("platform", "expected"),
[
# Defaults preserve each platform's historic behavior.
(PLATFORM_ESP8266, preferences.STORAGE_RTC),
(PLATFORM_RP2040, preferences.STORAGE_FLASH),
],
)
def test_default_storage_per_platform(platform: str, expected: str) -> None:
_set_platform(platform)
assert _validate({})[CONF_STORAGE] == expected
@pytest.mark.parametrize("variant", [VARIANT_ESP32, VARIANT_ESP32C2])
def test_default_storage_esp32_is_flash(variant: str) -> None:
# ESP32 defaults to flash on every variant, including those without RTC memory.
_set_esp32(variant)
assert _validate({})[CONF_STORAGE] == preferences.STORAGE_FLASH
def test_rtc_allowed_on_esp8266() -> None:
_set_platform(PLATFORM_ESP8266)
assert _validate({CONF_STORAGE: "rtc"})[CONF_STORAGE] == preferences.STORAGE_RTC
@pytest.mark.parametrize("variant", [VARIANT_ESP32, VARIANT_ESP32C3])
def test_rtc_allowed_on_esp32_with_rtc_memory(variant: str) -> None:
_set_esp32(variant)
assert _validate({CONF_STORAGE: "rtc"})[CONF_STORAGE] == preferences.STORAGE_RTC
@pytest.mark.parametrize("variant", [VARIANT_ESP32C2, VARIANT_ESP32C61])
def test_rtc_rejected_on_esp32_without_rtc_memory(variant: str) -> None:
_set_esp32(variant)
with pytest.raises(cv.Invalid, match="not supported on this platform"):
_validate({CONF_STORAGE: "rtc"})
def test_rtc_rejected_on_unsupported_platform() -> None:
_set_platform(PLATFORM_RP2040)
with pytest.raises(cv.Invalid, match="not supported on this platform"):
_validate({CONF_STORAGE: "rtc"})
def test_flash_allowed_on_unsupported_platform() -> None:
_set_platform(PLATFORM_RP2040)
assert _validate({CONF_STORAGE: "flash"})[CONF_STORAGE] == preferences.STORAGE_FLASH