mirror of
https://github.com/esphome/esphome.git
synced 2026-08-17 10:52:56 +08:00
[core] Enforce the preferences contracts with concepts (#18191)
This commit is contained in:
+10
-2
@@ -157,9 +157,17 @@
|
|||||||
#define USE_OUTPUT_FLOAT_POWER_SCALING
|
#define USE_OUTPUT_FLOAT_POWER_SCALING
|
||||||
#define USE_POWER_SUPPLY
|
#define USE_POWER_SUPPLY
|
||||||
#define USE_PREFERENCES_SYNC_EVERY_LOOP
|
#define USE_PREFERENCES_SYNC_EVERY_LOOP
|
||||||
// Only defined by key-lookup preference backends (esp32, libretiny, host, zephyr);
|
// Only defined by key-lookup preference backends; the slot-based platforms
|
||||||
// slot-based platforms (esp8266, rp2040) never set it in generated builds
|
// (esp8266, rp2040) never set it in generated builds, and their preferences
|
||||||
|
// managers do not provide load_from_key(), so the PreferencesKeyLookupContract
|
||||||
|
// assert would fail their clang-tidy environments. Written as a deny-list so
|
||||||
|
// the no-platform analysis configuration (whose Preferences stub provides
|
||||||
|
// load_from_key()) keeps covering the key-lookup code paths, and so a future
|
||||||
|
// slot-based platform fails the assert loudly instead of silently losing
|
||||||
|
// analysis coverage.
|
||||||
|
#if !defined(USE_ESP8266) && !defined(USE_RP2)
|
||||||
#define USE_PREFERENCE_KEY_LOOKUP
|
#define USE_PREFERENCE_KEY_LOOKUP
|
||||||
|
#endif
|
||||||
#define USE_PROVISIONING
|
#define USE_PROVISIONING
|
||||||
#define USE_QR_CODE
|
#define USE_QR_CODE
|
||||||
#define USE_SAFE_MODE_BOOT_IS_GOOD_ON_SHUTDOWN
|
#define USE_SAFE_MODE_BOOT_IS_GOOD_ON_SHUTDOWN
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <concepts>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
@@ -30,6 +31,15 @@
|
|||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
|
// The PreferenceBackend method surface, asserted on the alias each platform
|
||||||
|
// header binds. save() persists len bytes; load() fills dest only when the
|
||||||
|
// stored data exists and matches len. Both report success as their return.
|
||||||
|
template<typename T>
|
||||||
|
concept PreferenceBackendContract = requires(T backend, const uint8_t *src, uint8_t *dest, size_t len) {
|
||||||
|
{ backend.save(src, len) } -> std::same_as<bool>;
|
||||||
|
{ backend.load(dest, len) } -> std::same_as<bool>;
|
||||||
|
};
|
||||||
|
|
||||||
#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2) && !defined(USE_LIBRETINY) && \
|
#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2) && !defined(USE_LIBRETINY) && \
|
||||||
!defined(USE_HOST) && !(defined(USE_ZEPHYR) && defined(CONFIG_SETTINGS))
|
!defined(USE_HOST) && !(defined(USE_ZEPHYR) && defined(CONFIG_SETTINGS))
|
||||||
// Stub for static analysis when no platform is defined.
|
// Stub for static analysis when no platform is defined.
|
||||||
@@ -40,6 +50,8 @@ struct PreferenceBackend {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
using ESPPreferenceBackend = PreferenceBackend;
|
using ESPPreferenceBackend = PreferenceBackend;
|
||||||
|
static_assert(PreferenceBackendContract<PreferenceBackend>,
|
||||||
|
"The platform's preference backend is missing part of the PreferenceBackend surface");
|
||||||
|
|
||||||
class ESPPreferenceObject {
|
class ESPPreferenceObject {
|
||||||
public:
|
public:
|
||||||
@@ -68,6 +80,39 @@ class ESPPreferenceObject {
|
|||||||
PreferenceBackend *backend_{nullptr};
|
PreferenceBackend *backend_{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The preferences manager method surface, asserted in esphome/core/preferences.h
|
||||||
|
// on the ESPPreferences alias each platform's preferences.h binds through
|
||||||
|
// DECLARE_PREFERENCE_ALIASES. Semantics beyond the signatures:
|
||||||
|
// - make_preference: the two-argument form applies the platform's historic
|
||||||
|
// default storage; in_flash=false may fall back to flash where the platform
|
||||||
|
// has no faster storage.
|
||||||
|
// - sync: commit pending writes to flash, true on success.
|
||||||
|
// - reset: forget unsaved changes and re-initialize the permanent storage
|
||||||
|
// (usually followed by a restart), true on success.
|
||||||
|
// The template forms are what component call sites use; PreferencesMixin
|
||||||
|
// supplies them, but the derived class's non-template overloads hide them
|
||||||
|
// unless it also declares `using PreferencesMixin<X>::make_preference;`, so
|
||||||
|
// the concept pins those too.
|
||||||
|
template<typename T>
|
||||||
|
concept PreferencesContract = requires(T prefs, size_t len, uint32_t type, bool in_flash) {
|
||||||
|
{ prefs.make_preference(len, type, in_flash) } -> std::same_as<ESPPreferenceObject>;
|
||||||
|
{ prefs.make_preference(len, type) } -> std::same_as<ESPPreferenceObject>;
|
||||||
|
{ prefs.template make_preference<uint32_t>(type, in_flash) } -> std::same_as<ESPPreferenceObject>;
|
||||||
|
{ prefs.template make_preference<uint32_t>(type) } -> std::same_as<ESPPreferenceObject>;
|
||||||
|
{ prefs.sync() } -> std::same_as<bool>;
|
||||||
|
{ prefs.reset() } -> std::same_as<bool>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Key-lookup platforms additionally provide load_from_key(), a one-shot read
|
||||||
|
// of a stored preference by key that migrate_preference() relies on; see the
|
||||||
|
// key-lookup note at the top of this file. Not part of PreferencesContract,
|
||||||
|
// so it is asserted in preferences.h only where USE_PREFERENCE_KEY_LOOKUP
|
||||||
|
// is set.
|
||||||
|
template<typename T>
|
||||||
|
concept PreferencesKeyLookupContract = requires(T prefs, uint32_t type, uint8_t *data, size_t len) {
|
||||||
|
{ prefs.load_from_key(type, data, len) } -> std::same_as<bool>;
|
||||||
|
};
|
||||||
|
|
||||||
/// CRTP mixin providing type-safe template make_preference<T>() helpers.
|
/// CRTP mixin providing type-safe template make_preference<T>() helpers.
|
||||||
/// Platform preferences classes inherit this to avoid duplicating these templates.
|
/// Platform preferences classes inherit this to avoid duplicating these templates.
|
||||||
template<typename Derived> class PreferencesMixin {
|
template<typename Derived> class PreferencesMixin {
|
||||||
|
|||||||
@@ -45,8 +45,18 @@ extern ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-no
|
|||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
static_assert(PreferencesContract<ESPPreferences>,
|
||||||
|
"The platform's preferences manager is missing part of the ESPPreferences surface "
|
||||||
|
"(esphome/core/preference_backend.h)");
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
#ifdef USE_PREFERENCE_KEY_LOOKUP
|
#ifdef USE_PREFERENCE_KEY_LOOKUP
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
static_assert(PreferencesKeyLookupContract<ESPPreferences>,
|
||||||
|
"This platform emits USE_PREFERENCE_KEY_LOOKUP but its preferences manager does not provide "
|
||||||
|
"load_from_key() (esphome/core/preference_backend.h)");
|
||||||
|
|
||||||
/// Copy preference data stored under old_key into new_pref (created for new_key) if the keys
|
/// Copy preference data stored under old_key into new_pref (created for new_key) if the keys
|
||||||
/// differ and new_pref has no data yet. scratch must hold at least size bytes.
|
/// differ and new_pref has no data yet. scratch must hold at least size bytes.
|
||||||
/// Returns true when scratch holds the entity's current data (loaded or just migrated).
|
/// Returns true when scratch holds the entity's current data (loaded or just migrated).
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
esphome:
|
||||||
|
name: preftest
|
||||||
|
|
||||||
|
bk72xx:
|
||||||
|
board: generic-bk7252
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
esphome:
|
||||||
|
name: preftest
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32dev
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
esphome:
|
||||||
|
name: preftest
|
||||||
|
|
||||||
|
esp8266:
|
||||||
|
board: esp01_1m
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
esphome:
|
||||||
|
name: preftest
|
||||||
|
|
||||||
|
host:
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
esphome:
|
||||||
|
name: preftest
|
||||||
|
|
||||||
|
nrf52:
|
||||||
|
board: adafruit_itsybitsy_nrf52840
|
||||||
|
bootloader: adafruit_nrf52_sd140_v6
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
esphome:
|
||||||
|
name: preftest
|
||||||
|
|
||||||
|
rp2:
|
||||||
|
board: rpipicow
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Every preferences platform either emits USE_PREFERENCE_KEY_LOOKUP from
|
||||||
|
codegen (key-lookup backends) or must not (slot-based backends, whose managers
|
||||||
|
have no load_from_key()). Run each platform's real codegen and assert the
|
||||||
|
emission, mirroring the split the deny-list in esphome/core/defines.h assumes
|
||||||
|
for static analysis.
|
||||||
|
|
||||||
|
The fixtures cover every distinct preferences backend today: ln882x and
|
||||||
|
rtl87xx route through libretiny (bk72xx stands in for the family), rp2040 is
|
||||||
|
an alias of rp2, and nrf52 exercises zephyr. A seventh backend needs a new
|
||||||
|
fixture here."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("fixture", "emits"),
|
||||||
|
[
|
||||||
|
("esp32.yaml", True),
|
||||||
|
("bk72xx.yaml", True), # libretiny
|
||||||
|
("host.yaml", True),
|
||||||
|
("nrf52.yaml", True), # zephyr
|
||||||
|
("esp8266.yaml", False),
|
||||||
|
("rp2.yaml", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_key_lookup_define_matches_the_platform_backend(
|
||||||
|
fixture: str,
|
||||||
|
emits: bool,
|
||||||
|
generate_main: Callable[[str | Path], str],
|
||||||
|
component_config_path: Callable[[str], Path],
|
||||||
|
) -> None:
|
||||||
|
generate_main(component_config_path(fixture))
|
||||||
|
defines = {define.name for define in CORE.defines}
|
||||||
|
assert ("USE_PREFERENCE_KEY_LOOKUP" in defines) is emits
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// Pins the preferences contract concepts so the surface they enforce cannot
|
||||||
|
// drift unnoticed: a minimal conforming type must satisfy each concept, and a
|
||||||
|
// type missing a method or returning the wrong type must not.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "esphome/core/preference_backend.h"
|
||||||
|
|
||||||
|
namespace esphome::core::testing {
|
||||||
|
|
||||||
|
struct MinimalBackend {
|
||||||
|
bool save(const uint8_t *, size_t) { return true; }
|
||||||
|
bool load(uint8_t *, size_t) { return true; }
|
||||||
|
};
|
||||||
|
static_assert(PreferenceBackendContract<MinimalBackend>);
|
||||||
|
|
||||||
|
struct BackendMissingLoad {
|
||||||
|
bool save(const uint8_t *, size_t) { return true; }
|
||||||
|
};
|
||||||
|
static_assert(!PreferenceBackendContract<BackendMissingLoad>);
|
||||||
|
|
||||||
|
struct BackendWrongReturn {
|
||||||
|
void save(const uint8_t *, size_t) {}
|
||||||
|
bool load(uint8_t *, size_t) { return true; }
|
||||||
|
};
|
||||||
|
static_assert(!PreferenceBackendContract<BackendWrongReturn>);
|
||||||
|
|
||||||
|
struct MinimalPreferences : public PreferencesMixin<MinimalPreferences> {
|
||||||
|
using PreferencesMixin<MinimalPreferences>::make_preference;
|
||||||
|
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
|
||||||
|
ESPPreferenceObject make_preference(size_t, uint32_t) { return {}; }
|
||||||
|
bool sync() { return true; }
|
||||||
|
bool reset() { return true; }
|
||||||
|
};
|
||||||
|
static_assert(PreferencesContract<MinimalPreferences>);
|
||||||
|
|
||||||
|
struct PreferencesMissingTwoArgForm : public PreferencesMixin<PreferencesMissingTwoArgForm> {
|
||||||
|
using PreferencesMixin<PreferencesMissingTwoArgForm>::make_preference;
|
||||||
|
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
|
||||||
|
bool sync() { return true; }
|
||||||
|
bool reset() { return true; }
|
||||||
|
};
|
||||||
|
static_assert(!PreferencesContract<PreferencesMissingTwoArgForm>);
|
||||||
|
|
||||||
|
struct PreferencesMissingReset : public PreferencesMixin<PreferencesMissingReset> {
|
||||||
|
using PreferencesMixin<PreferencesMissingReset>::make_preference;
|
||||||
|
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
|
||||||
|
ESPPreferenceObject make_preference(size_t, uint32_t) { return {}; }
|
||||||
|
bool sync() { return true; }
|
||||||
|
};
|
||||||
|
static_assert(!PreferencesContract<PreferencesMissingReset>);
|
||||||
|
|
||||||
|
struct PreferencesWrongSyncReturn : public PreferencesMixin<PreferencesWrongSyncReturn> {
|
||||||
|
using PreferencesMixin<PreferencesWrongSyncReturn>::make_preference;
|
||||||
|
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
|
||||||
|
ESPPreferenceObject make_preference(size_t, uint32_t) { return {}; }
|
||||||
|
void sync() {}
|
||||||
|
bool reset() { return true; }
|
||||||
|
};
|
||||||
|
static_assert(!PreferencesContract<PreferencesWrongSyncReturn>);
|
||||||
|
|
||||||
|
// Forgot `using PreferencesMixin<X>::make_preference;`, so the derived
|
||||||
|
// overloads hide the template forms (see the PreferencesContract note in
|
||||||
|
// preference_backend.h); the concept must reject the class.
|
||||||
|
struct PreferencesForgotUsingDeclaration : public PreferencesMixin<PreferencesForgotUsingDeclaration> {
|
||||||
|
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
|
||||||
|
ESPPreferenceObject make_preference(size_t, uint32_t) { return {}; }
|
||||||
|
bool sync() { return true; }
|
||||||
|
bool reset() { return true; }
|
||||||
|
};
|
||||||
|
static_assert(!PreferencesContract<PreferencesForgotUsingDeclaration>);
|
||||||
|
|
||||||
|
struct MinimalKeyLookup {
|
||||||
|
bool load_from_key(uint32_t, uint8_t *, size_t) { return true; }
|
||||||
|
};
|
||||||
|
static_assert(PreferencesKeyLookupContract<MinimalKeyLookup>);
|
||||||
|
|
||||||
|
struct KeyLookupMissingMethod {};
|
||||||
|
static_assert(!PreferencesKeyLookupContract<KeyLookupMissingMethod>);
|
||||||
|
|
||||||
|
TEST(PreferenceContract, NullBackendRefusesBothOperations) {
|
||||||
|
// ESPPreferenceObject forwards to whichever backend the platform binds; a
|
||||||
|
// default-constructed object has no backend and must refuse both operations
|
||||||
|
// instead of crashing.
|
||||||
|
ESPPreferenceObject without_backend;
|
||||||
|
uint32_t value = 42;
|
||||||
|
EXPECT_FALSE(without_backend.save(&value));
|
||||||
|
EXPECT_FALSE(without_backend.load(&value));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::core::testing
|
||||||
Reference in New Issue
Block a user