diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a15324..75a00c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project are documented in this file. The format follows Keep a Changelog and the project adheres to Semantic Versioning. ## [Unreleased] +- Added teardown contract support with `deinit()`, `isInitialized()`, and destructor-driven cleanup. +- Added lifecycle tests covering pre-init `deinit()`, repeated `deinit()`, and `init -> deinit -> init`. +- Updated README and examples to use explicit `deinit()`. +- Removed `ready()` in favor of `isInitialized()` (no compatibility alias). - Added `setDefault()` and the no-argument `getOr()` overload for stored defaults. - Added `ESPStoreCodec` helpers for IPAddress and ESPDate DateTime (epoch + ISO-8601). - Added examples for codec usage, runtime overrides, and clearing/reseeding stores. diff --git a/README.md b/README.md index d0859fd..a10d57b 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ void setup() { } serializeJsonPretty(Serial, res.data); + netConf.deinit(); } ``` @@ -87,6 +88,8 @@ if (res.ok()) { - `DbStatus init(ESPJsonDB* db, const String& collection)` - `DbStatus init(ESPJsonDB* db, const char* collection, const char* key)` - `DbStatus init(ESPJsonDB* db, const String& collection, const String& key)` +- `void deinit()` (safe before `init()`, idempotent) +- `bool isInitialized() const` - `StoreResponse get()` - `DbStatus setDefault(JsonVariantConst value)` - `StoreResponse getOr(bool* usedDefault = nullptr)` diff --git a/examples/ClearAndReseed/ClearAndReseed.ino b/examples/ClearAndReseed/ClearAndReseed.ino index ecfe624..8458d26 100644 --- a/examples/ClearAndReseed/ClearAndReseed.ino +++ b/examples/ClearAndReseed/ClearAndReseed.ino @@ -38,6 +38,7 @@ void setup() { usedDefault ? "default" : "stored"); serializeJsonPretty(res.data, Serial); + store.deinit(); } void loop() {} diff --git a/examples/CodecAll/CodecAll.ino b/examples/CodecAll/CodecAll.ino index b887c1c..b2e1762 100644 --- a/examples/CodecAll/CodecAll.ino +++ b/examples/CodecAll/CodecAll.ino @@ -62,6 +62,8 @@ void setup() { static_cast(localDecoded.utc.epochSeconds), localDecoded.offsetMinutes); } + + store.deinit(); } void loop() {} diff --git a/examples/Defaults/Defaults.ino b/examples/Defaults/Defaults.ino index f2a428c..53d0153 100644 --- a/examples/Defaults/Defaults.ino +++ b/examples/Defaults/Defaults.ino @@ -31,6 +31,7 @@ void setup() { Serial.printf("Config source: %s\n", usedDefault ? "default" : "stored"); serializeJsonPretty(res.data, Serial); + netConf.deinit(); } void loop() {} diff --git a/examples/LocalDateTime/LocalDateTime.ino b/examples/LocalDateTime/LocalDateTime.ino index 7b6d578..242e071 100644 --- a/examples/LocalDateTime/LocalDateTime.ino +++ b/examples/LocalDateTime/LocalDateTime.ino @@ -38,6 +38,7 @@ void setup() { Serial.printf("Loaded epoch: %lld\n", static_cast(loaded.utc.epochSeconds)); Serial.printf("Loaded offset minutes: %d\n", loaded.offsetMinutes); + store.deinit(); } void loop() {} diff --git a/examples/QuickStart/QuickStart.ino b/examples/QuickStart/QuickStart.ino index ff42fe6..1f084c0 100644 --- a/examples/QuickStart/QuickStart.ino +++ b/examples/QuickStart/QuickStart.ino @@ -29,6 +29,7 @@ void setup() { } serializeJsonPretty(res.data, Serial); + store.deinit(); } void loop() {} diff --git a/examples/RuntimeOverrides/RuntimeOverrides.ino b/examples/RuntimeOverrides/RuntimeOverrides.ino index 02af0e4..47d5486 100644 --- a/examples/RuntimeOverrides/RuntimeOverrides.ino +++ b/examples/RuntimeOverrides/RuntimeOverrides.ino @@ -41,6 +41,7 @@ void setup() { usedDefault ? "default" : "stored"); serializeJsonPretty(res2.data, Serial); + netConf.deinit(); } void loop() {} diff --git a/src/esp_store/store.cpp b/src/esp_store/store.cpp index 319dbff..5c671ef 100644 --- a/src/esp_store/store.cpp +++ b/src/esp_store/store.cpp @@ -22,17 +22,41 @@ DbStatus ESPStore::init(ESPJsonDB *db, const char *collection, const char *key) return {DbStatusCode::InvalidArgument, kErrBadKey}; } _db = db; - _collection = collection; - _key = key; - return registerSchema(); + _collection.assign(collection); + _key.assign(key); + _initialized = false; + + const DbStatus st = registerSchema(); + if (!st.ok()) { + _db = nullptr; + _collection.clear(); + _key.clear(); + return st; + } + + _initialized = true; + return st; } DbStatus ESPStore::init(ESPJsonDB *db, const String &collection, const String &key) { return init(db, collection.c_str(), key.c_str()); } +void ESPStore::deinit() { + if (!_initialized && _db == nullptr && _collection.empty() && _key.empty() && !_hasDefault) { + return; + } + + _db = nullptr; + _initialized = false; + std::string().swap(_collection); + std::string().swap(_key); + _defaultDoc = JsonDocument(); + _hasDefault = false; +} + DbStatus ESPStore::ensureReady() const { - if (!_db || _collection.empty() || _key.empty()) { + if (!_initialized || !_db || _collection.empty() || _key.empty()) { return {DbStatusCode::InvalidArgument, kErrNotReady}; } return {DbStatusCode::Ok, ""}; diff --git a/src/esp_store/store.h b/src/esp_store/store.h index 60ed3b1..b22919a 100644 --- a/src/esp_store/store.h +++ b/src/esp_store/store.h @@ -23,11 +23,14 @@ struct StoreResponse { class ESPStore { public: ESPStore() = default; + ~ESPStore() { deinit(); } DbStatus init(ESPJsonDB *db, const char *collection); DbStatus init(ESPJsonDB *db, const String &collection); DbStatus init(ESPJsonDB *db, const char *collection, const char *key); DbStatus init(ESPJsonDB *db, const String &collection, const String &key); + void deinit(); + bool isInitialized() const { return _initialized; } DbStatus setDefault(JsonVariantConst value); StoreResponse get(); @@ -38,7 +41,6 @@ class ESPStore { DbStatus clear(); DbStatus syncNow(); - bool ready() const { return _db != nullptr && !_collection.empty() && !_key.empty(); } const std::string &collection() const { return _collection; } const std::string &key() const { return _key; } @@ -48,6 +50,7 @@ class ESPStore { std::string _key; JsonDocument _defaultDoc; bool _hasDefault = false; + bool _initialized = false; DbStatus ensureReady() const; DbStatus registerSchema(); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..5609e8c --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,5 @@ +# Host-side tests are not enabled because ESPStore depends on Arduino runtime +# and ESPJsonDB filesystem integrations that are not available on the default +# host toolchain. Component/PlatformIO tests live under test/test_esp_store +# and should be executed on-device. +message(STATUS "ESPStore: tests are disabled for the host build.") diff --git a/test/test_esp_store/test_esp_store.cpp b/test/test_esp_store/test_esp_store.cpp new file mode 100644 index 0000000..ebd2fbe --- /dev/null +++ b/test/test_esp_store/test_esp_store.cpp @@ -0,0 +1,106 @@ +#include +#include +#include +#include + +static constexpr const char *kTestDbPath = "/db_store_contract"; + +static void test_deinit_is_safe_before_init() { + ESPStore store; + TEST_ASSERT_FALSE(store.isInitialized()); + + store.deinit(); + store.deinit(); + + TEST_ASSERT_FALSE(store.isInitialized()); + + auto res = store.get(); + TEST_ASSERT_FALSE(res.ok()); + TEST_ASSERT_EQUAL_UINT8(static_cast(DbStatusCode::InvalidArgument), + static_cast(res.status.code)); +} + +static void test_deinit_is_idempotent_after_init() { + ESPJsonDB db; + TEST_ASSERT_TRUE(db.init(kTestDbPath).ok()); + + ESPStore store; + TEST_ASSERT_TRUE(store.init(&db, "store_teardown_contract", "idempotent_key").ok()); + TEST_ASSERT_TRUE(store.isInitialized()); + + store.deinit(); + TEST_ASSERT_FALSE(store.isInitialized()); + + store.deinit(); + TEST_ASSERT_FALSE(store.isInitialized()); + + auto res = store.get(); + TEST_ASSERT_FALSE(res.ok()); + TEST_ASSERT_EQUAL_UINT8(static_cast(DbStatusCode::InvalidArgument), + static_cast(res.status.code)); + + db.deinit(); +} + +static void test_init_deinit_init_lifecycle() { + ESPJsonDB db; + TEST_ASSERT_TRUE(db.init(kTestDbPath).ok()); + + ESPStore store; + + JsonDocument defaults; + defaults["mode"] = "safe"; + TEST_ASSERT_TRUE(store.setDefault(defaults.as()).ok()); + + TEST_ASSERT_TRUE(store.init(&db, "store_teardown_contract", "first_key").ok()); + TEST_ASSERT_TRUE(store.clear().ok()); + + bool usedDefault = false; + auto first = store.getOr(&usedDefault); + TEST_ASSERT_TRUE(first.ok()); + TEST_ASSERT_TRUE(usedDefault); + + store.deinit(); + TEST_ASSERT_FALSE(store.isInitialized()); + + TEST_ASSERT_TRUE(store.init(&db, "store_teardown_contract", "second_key").ok()); + TEST_ASSERT_TRUE(store.isInitialized()); + TEST_ASSERT_TRUE(store.clear().ok()); + + usedDefault = false; + auto withoutDefault = store.getOr(&usedDefault); + TEST_ASSERT_FALSE(withoutDefault.ok()); + TEST_ASSERT_FALSE(usedDefault); + TEST_ASSERT_EQUAL_UINT8(static_cast(DbStatusCode::NotFound), + static_cast(withoutDefault.status.code)); + + JsonDocument runtime; + runtime["retries"] = 3; + TEST_ASSERT_TRUE(store.set(runtime.as()).ok()); + + auto loaded = store.get(); + TEST_ASSERT_TRUE(loaded.ok()); + TEST_ASSERT_EQUAL_INT(3, loaded.data["retries"] | 0); + + store.deinit(); + db.deinit(); +} + +void setUp() { +} + +void tearDown() { +} + +void setup() { + delay(2000); + UNITY_BEGIN(); + RUN_TEST(test_deinit_is_safe_before_init); + RUN_TEST(test_deinit_is_idempotent_after_init); + RUN_TEST(test_init_deinit_init_lifecycle); + UNITY_END(); +} + +void loop() { + vTaskDelay(pdMS_TO_TICKS(1000)); +}