Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ void setup() {
}

serializeJsonPretty(Serial, res.data);
netConf.deinit();
}
```

Expand Down Expand Up @@ -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)`
Expand Down
1 change: 1 addition & 0 deletions examples/ClearAndReseed/ClearAndReseed.ino
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ void setup() {
usedDefault ? "default" : "stored");

serializeJsonPretty(res.data, Serial);
store.deinit();
}

void loop() {}
2 changes: 2 additions & 0 deletions examples/CodecAll/CodecAll.ino
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ void setup() {
static_cast<long long>(localDecoded.utc.epochSeconds),
localDecoded.offsetMinutes);
}

store.deinit();
}

void loop() {}
1 change: 1 addition & 0 deletions examples/Defaults/Defaults.ino
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ void setup() {

Serial.printf("Config source: %s\n", usedDefault ? "default" : "stored");
serializeJsonPretty(res.data, Serial);
netConf.deinit();
}

void loop() {}
1 change: 1 addition & 0 deletions examples/LocalDateTime/LocalDateTime.ino
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ void setup() {

Serial.printf("Loaded epoch: %lld\n", static_cast<long long>(loaded.utc.epochSeconds));
Serial.printf("Loaded offset minutes: %d\n", loaded.offsetMinutes);
store.deinit();
}

void loop() {}
1 change: 1 addition & 0 deletions examples/QuickStart/QuickStart.ino
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ void setup() {
}

serializeJsonPretty(res.data, Serial);
store.deinit();
}

void loop() {}
1 change: 1 addition & 0 deletions examples/RuntimeOverrides/RuntimeOverrides.ino
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ void setup() {
usedDefault ? "default" : "stored");

serializeJsonPretty(res2.data, Serial);
netConf.deinit();
}

void loop() {}
32 changes: 28 additions & 4 deletions src/esp_store/store.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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, ""};
Expand Down
5 changes: 4 additions & 1 deletion src/esp_store/store.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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; }

Expand All @@ -48,6 +50,7 @@ class ESPStore {
std::string _key;
JsonDocument _defaultDoc;
bool _hasDefault = false;
bool _initialized = false;

DbStatus ensureReady() const;
DbStatus registerSchema();
Expand Down
5 changes: 5 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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.")
106 changes: 106 additions & 0 deletions test/test_esp_store/test_esp_store.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#include <Arduino.h>
#include <ESPJsonDB.h>
#include <ESPStore.h>
#include <unity.h>

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<uint8_t>(DbStatusCode::InvalidArgument),
static_cast<uint8_t>(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<uint8_t>(DbStatusCode::InvalidArgument),
static_cast<uint8_t>(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<JsonVariantConst>()).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<uint8_t>(DbStatusCode::NotFound),
static_cast<uint8_t>(withoutDefault.status.code));

JsonDocument runtime;
runtime["retries"] = 3;
TEST_ASSERT_TRUE(store.set(runtime.as<JsonVariantConst>()).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));
}