mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[ota] Enforce the backend contract with a concept (#18192)
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
@@ -78,6 +80,25 @@ enum OTAType : uint8_t {
|
||||
OTA_TYPE_UPDATE_BOOTLOADER = 0x02,
|
||||
};
|
||||
|
||||
// The OTA backend method surface. Exactly one backend exists per build,
|
||||
// selected in ota_backend_factory.h where this concept is asserted on
|
||||
// make_ota_backend()'s return type. Semantics beyond the signatures:
|
||||
// - begin: prepare for an image of the given size; ota_type defaults to an
|
||||
// app update, so both call forms must be accepted.
|
||||
// - set_update_md5: expected digest of the incoming image, hex string.
|
||||
// - write: consume the next chunk; end: finalize and mark bootable.
|
||||
// - abort: safe to call in any state, including after end().
|
||||
template<typename T>
|
||||
concept OTABackendContract = requires(T backend, size_t image_size, uint8_t *data, size_t len, const char *md5) {
|
||||
{ backend.begin(image_size, OTA_TYPE_UPDATE_APP) } -> std::same_as<OTAResponseTypes>;
|
||||
{ backend.begin(image_size) } -> std::same_as<OTAResponseTypes>;
|
||||
backend.set_update_md5(md5);
|
||||
{ backend.write(data, len) } -> std::same_as<OTAResponseTypes>;
|
||||
{ backend.end() } -> std::same_as<OTAResponseTypes>;
|
||||
backend.abort();
|
||||
{ backend.supports_compression() } -> std::same_as<bool>;
|
||||
};
|
||||
|
||||
/** Listener interface for OTA state changes.
|
||||
*
|
||||
* Components can implement this interface to receive OTA state updates
|
||||
|
||||
@@ -17,11 +17,22 @@
|
||||
#else
|
||||
// Stub for static analysis when no platform is defined
|
||||
namespace esphome::ota {
|
||||
struct StubOTABackend {};
|
||||
struct StubOTABackend {
|
||||
OTAResponseTypes begin(size_t image_size, OTAType ota_type = OTA_TYPE_UPDATE_APP) {
|
||||
return OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
}
|
||||
void set_update_md5(const char *md5) {}
|
||||
OTAResponseTypes write(uint8_t *data, size_t len) { return OTA_RESPONSE_ERROR_UNKNOWN; }
|
||||
OTAResponseTypes end() { return OTA_RESPONSE_ERROR_UNKNOWN; }
|
||||
void abort() {}
|
||||
bool supports_compression() { return false; }
|
||||
};
|
||||
std::unique_ptr<StubOTABackend> make_ota_backend();
|
||||
} // namespace esphome::ota
|
||||
#endif
|
||||
|
||||
namespace esphome::ota {
|
||||
using OTABackendPtr = decltype(make_ota_backend());
|
||||
static_assert(OTABackendContract<OTABackendPtr::element_type>,
|
||||
"The platform's OTA backend is missing part of the backend surface (ota_backend.h)");
|
||||
} // namespace esphome::ota
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Pins the OTA backend contract concept so the surface it enforces cannot
|
||||
// drift unnoticed: the build's real backend and a minimal conforming type
|
||||
// must satisfy it, and a type missing a method or returning the wrong type
|
||||
// must not.
|
||||
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#include "esphome/components/ota/ota_backend_host.h"
|
||||
|
||||
namespace esphome::ota::testing {
|
||||
|
||||
struct MinimalBackend {
|
||||
OTAResponseTypes begin(size_t image_size, OTAType ota_type = OTA_TYPE_UPDATE_APP) { return OTA_RESPONSE_OK; }
|
||||
void set_update_md5(const char *md5) {}
|
||||
OTAResponseTypes write(uint8_t *data, size_t len) { return OTA_RESPONSE_OK; }
|
||||
OTAResponseTypes end() { return OTA_RESPONSE_OK; }
|
||||
void abort() {}
|
||||
bool supports_compression() { return false; }
|
||||
};
|
||||
static_assert(OTABackendContract<MinimalBackend>);
|
||||
|
||||
// Each negative case derives from MinimalBackend and breaks exactly one
|
||||
// requirement; the declaration in the derived struct hides the conforming
|
||||
// one from the base.
|
||||
|
||||
// begin() without the default ota_type argument breaks consumers that only
|
||||
// pass the image size.
|
||||
struct BackendWithoutDefaultOTAType : MinimalBackend {
|
||||
OTAResponseTypes begin(size_t image_size, OTAType ota_type) { return OTA_RESPONSE_OK; }
|
||||
};
|
||||
static_assert(!OTABackendContract<BackendWithoutDefaultOTAType>);
|
||||
|
||||
struct BackendMissingAbort : MinimalBackend {
|
||||
void abort() = delete;
|
||||
};
|
||||
static_assert(!OTABackendContract<BackendMissingAbort>);
|
||||
|
||||
struct BackendWrongWriteReturn : MinimalBackend {
|
||||
bool write(uint8_t *data, size_t len) { return true; }
|
||||
};
|
||||
static_assert(!OTABackendContract<BackendWrongWriteReturn>);
|
||||
|
||||
// Pin the build's real backend, not just local mocks: the unit test harness
|
||||
// builds for the host platform, so this is the same check the factory's
|
||||
// static_assert performs in a firmware compile.
|
||||
#ifdef USE_HOST
|
||||
static_assert(OTABackendContract<HostOTABackend>);
|
||||
#endif
|
||||
|
||||
} // namespace esphome::ota::testing
|
||||
Reference in New Issue
Block a user