diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 01be46a518..aa93df60a5 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -4,6 +4,8 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#include +#include #include #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 +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; + { backend.begin(image_size) } -> std::same_as; + backend.set_update_md5(md5); + { backend.write(data, len) } -> std::same_as; + { backend.end() } -> std::same_as; + backend.abort(); + { backend.supports_compression() } -> std::same_as; +}; + /** Listener interface for OTA state changes. * * Components can implement this interface to receive OTA state updates diff --git a/esphome/components/ota/ota_backend_factory.h b/esphome/components/ota/ota_backend_factory.h index c543983d8d..82d001ed9e 100644 --- a/esphome/components/ota/ota_backend_factory.h +++ b/esphome/components/ota/ota_backend_factory.h @@ -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 make_ota_backend(); } // namespace esphome::ota #endif namespace esphome::ota { using OTABackendPtr = decltype(make_ota_backend()); +static_assert(OTABackendContract, + "The platform's OTA backend is missing part of the backend surface (ota_backend.h)"); } // namespace esphome::ota diff --git a/tests/components/ota/test_backend_contract.cpp b/tests/components/ota/test_backend_contract.cpp new file mode 100644 index 0000000000..1b4fbbc32d --- /dev/null +++ b/tests/components/ota/test_backend_contract.cpp @@ -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); + +// 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); + +struct BackendMissingAbort : MinimalBackend { + void abort() = delete; +}; +static_assert(!OTABackendContract); + +struct BackendWrongWriteReturn : MinimalBackend { + bool write(uint8_t *data, size_t len) { return true; } +}; +static_assert(!OTABackendContract); + +// 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); +#endif + +} // namespace esphome::ota::testing