Merge branch 'feature/nvs_tests' into 'master'

Power off recovery tests for non-volatile storage library, bug fixes, test coverage

This merge request:
- fixes several issues with internal state handling in nvs::Page
- fixes possible infinite loop when initializing namespace list
- fixes nvs_get_{str,blob} APIs to behave according to header file doc (they didn't return *length)
- adds extra consistency checks in nvs::PageManger initialization.
- adds test coverage via gcov and lcov
- adds host side tests during CI builds

See merge request !9
This commit is contained in:
Ivan Grokhotkov 2016-08-23 15:06:06 +08:00
commit 0f35ff8a98
15 changed files with 514 additions and 215 deletions

View File

@ -1,9 +1,8 @@
stages: stages:
- build - build
# - test - test
- deploy - deploy
build_template_app: build_template_app:
stage: build stage: build
image: espressif/esp32-ci-env image: espressif/esp32-ci-env
@ -18,6 +17,13 @@ build_template_app:
- cd esp-idf-template - cd esp-idf-template
- make all - make all
test_nvs_on_host:
stage: test
image: espressif/esp32-ci-env
script:
- cd components/nvs_flash/test
- make test
push_master_to_github: push_master_to_github:
stage: deploy stage: deploy
only: only:

View File

@ -1 +1,7 @@
test/test_nvs test/test_nvs
test/coverage
test/coverage.info
*.gcno
*.gcda
*.gcov
*.o

View File

@ -61,7 +61,7 @@ Internals
Log of key-value pairs Log of key-value pairs
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
NVS stores key-value pairs sequentially, with new key-value pairs being added at the end. When a value of any given key has to be updated, old key-value pair is marked as erased and new key-value pair is added at the end of the log. NVS stores key-value pairs sequentially, with new key-value pairs being added at the end. When a value of any given key has to be updated, new key-value pair is added at the end of the log and old key-value pair is marked as erased.
Pages and entries Pages and entries
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
@ -69,10 +69,10 @@ Pages and entries
NVS library uses two main entities in its operation: pages and entries. Page is a logical structure which stores a portion of the overall log. Logical page corresponds to one physical sector of flash memory. Pages which are in use have a *sequence number* associated with them. Sequence numbers impose an ordering on pages. Higher sequence numbers correspond to pages which were created later. Each page can be in one of the following states: NVS library uses two main entities in its operation: pages and entries. Page is a logical structure which stores a portion of the overall log. Logical page corresponds to one physical sector of flash memory. Pages which are in use have a *sequence number* associated with them. Sequence numbers impose an ordering on pages. Higher sequence numbers correspond to pages which were created later. Each page can be in one of the following states:
Empty/uninitialized Empty/uninitialized
Flash storage for the page is empty (all bytes are ``0xff``). Page isn't used to store any data at this point and doesnt have Flash storage for the page is empty (all bytes are ``0xff``). Page isn't used to store any data at this point and doesnt have a sequence number.
Active Active
Flash storage is initialized, page header has been written to flash, page has a valid sequence number. Page has some empty entries and data can be written there. Normally only one page can be in this state. Flash storage is initialized, page header has been written to flash, page has a valid sequence number. Page has some empty entries and data can be written there. At most one page can be in this state at any given moment.
Full Full
Flash storage is in a consistent state and is filled with key-value pairs. Flash storage is in a consistent state and is filled with key-value pairs.
@ -213,7 +213,7 @@ As mentioned above, each key-value pair belongs to one of the namespaces. Namesp
+-------------------------------------------+ +-------------------------------------------+
| NS=0 Type=uint8_t Key="pwm" Value=2 | Entry describing namespace "pwm" | NS=0 Type=uint8_t Key="pwm" Value=2 | Entry describing namespace "pwm"
+-------------------------------------------+ +-------------------------------------------+
| NS=0 Type=uint16_t Key="channel" Value=20 | Key "channel" in namespace "pwm" | NS=2 Type=uint16_t Key="channel" Value=20 | Key "channel" in namespace "pwm"
+-------------------------------------------+ +-------------------------------------------+

View File

@ -36,6 +36,7 @@ typedef uint32_t nvs_handle;
#define ESP_ERR_NVS_NOT_ENOUGH_SPACE (ESP_ERR_NVS_BASE + 0x05) #define ESP_ERR_NVS_NOT_ENOUGH_SPACE (ESP_ERR_NVS_BASE + 0x05)
#define ESP_ERR_NVS_INVALID_NAME (ESP_ERR_NVS_BASE + 0x06) #define ESP_ERR_NVS_INVALID_NAME (ESP_ERR_NVS_BASE + 0x06)
#define ESP_ERR_NVS_INVALID_HANDLE (ESP_ERR_NVS_BASE + 0x07) #define ESP_ERR_NVS_INVALID_HANDLE (ESP_ERR_NVS_BASE + 0x07)
#define ESP_ERR_NVS_REMOVE_FAILED (ESP_ERR_NVS_BASE + 0x08)
#define ESP_ERR_NVS_KEY_TOO_LONG (ESP_ERR_NVS_BASE + 0x09) #define ESP_ERR_NVS_KEY_TOO_LONG (ESP_ERR_NVS_BASE + 0x09)
#define ESP_ERR_NVS_PAGE_FULL (ESP_ERR_NVS_BASE + 0x0a) #define ESP_ERR_NVS_PAGE_FULL (ESP_ERR_NVS_BASE + 0x0a)
#define ESP_ERR_NVS_INVALID_STATE (ESP_ERR_NVS_BASE + 0x0b) #define ESP_ERR_NVS_INVALID_STATE (ESP_ERR_NVS_BASE + 0x0b)
@ -92,6 +93,10 @@ esp_err_t nvs_open(const char* name, nvs_open_mode open_mode, nvs_handle *out_ha
* - ESP_ERR_NVS_INVALID_NAME if key name doesn't satisfy constraints * - ESP_ERR_NVS_INVALID_NAME if key name doesn't satisfy constraints
* - ESP_ERR_NVS_NOT_ENOUGH_SPACE if there is not enough space in the * - ESP_ERR_NVS_NOT_ENOUGH_SPACE if there is not enough space in the
* underlying storage to save the value * underlying storage to save the value
* - ESP_ERR_NVS_REMOVE_FAILED if the value wasn't updated because flash
* write operation has failed. The value was written however, and
* update will be finished after re-initialization of nvs, provided that
* flash operation doesn't fail again.
*/ */
esp_err_t nvs_set_i8 (nvs_handle handle, const char* key, int8_t value); esp_err_t nvs_set_i8 (nvs_handle handle, const char* key, int8_t value);
esp_err_t nvs_set_u8 (nvs_handle handle, const char* key, uint8_t value); esp_err_t nvs_set_u8 (nvs_handle handle, const char* key, uint8_t value);

View File

@ -33,7 +33,7 @@ public:
return mData; return mData;
} }
Tenum get(size_t index) Tenum get(size_t index) const
{ {
assert(index >= 0 && index < Nitems); assert(index >= 0 && index < Nitems);
size_t wordIndex = index / ITEMS_PER_WORD; size_t wordIndex = index / ITEMS_PER_WORD;

View File

@ -45,11 +45,18 @@ static intrusive_list<HandleEntry> s_nvs_handles;
static uint32_t s_nvs_next_handle = 1; static uint32_t s_nvs_next_handle = 1;
static nvs::Storage s_nvs_storage; static nvs::Storage s_nvs_storage;
extern "C" void nvs_dump()
{
Lock lock;
s_nvs_storage.debugDump();
}
extern "C" esp_err_t nvs_flash_init(uint32_t baseSector, uint32_t sectorCount) extern "C" esp_err_t nvs_flash_init(uint32_t baseSector, uint32_t sectorCount)
{ {
Lock::init(); Lock::init();
Lock lock; Lock lock;
NVS_DEBUGV("%s %d %d\r\n", __func__, baseSector, sectorCount); NVS_DEBUGV("%s %d %d\r\n", __func__, baseSector, sectorCount);
s_nvs_handles.clear();
return s_nvs_storage.init(baseSector, sectorCount); return s_nvs_storage.init(baseSector, sectorCount);
} }
@ -254,12 +261,15 @@ static esp_err_t nvs_get_str_or_blob(nvs_handle handle, nvs::ItemType type, cons
return err; return err;
} }
if (length != nullptr && out_value == nullptr) { if (length == nullptr) {
return ESP_ERR_NVS_INVALID_LENGTH;
}
else if (out_value == nullptr) {
*length = dataSize; *length = dataSize;
return ESP_OK; return ESP_OK;
} }
else if (*length < dataSize) {
if (length == nullptr || *length < dataSize) { *length = dataSize;
return ESP_ERR_NVS_INVALID_LENGTH; return ESP_ERR_NVS_INVALID_LENGTH;
} }

View File

@ -17,7 +17,8 @@
#else #else
#include "crc.h" #include "crc.h"
#endif #endif
#include <cstdio>
#include <cstring>
namespace nvs namespace nvs
{ {
@ -98,15 +99,12 @@ esp_err_t Page::writeEntry(const Item& item)
return err; return err;
} }
if (mNextFreeEntry == 0) { if (mFirstUsedEntry == INVALID_ENTRY) {
mFirstUsedEntry = 0; mFirstUsedEntry = mNextFreeEntry;
} }
++mUsedEntryCount; ++mUsedEntryCount;
++mNextFreeEntry;
if (++mNextFreeEntry == ENTRY_COUNT) {
alterPageState(PageState::FULL);
}
return ESP_OK; return ESP_OK;
} }
@ -143,7 +141,7 @@ esp_err_t Page::writeItem(uint8_t nsIndex, ItemType datatype, const char* key, c
// primitive types should fit into one entry // primitive types should fit into one entry
assert(totalSize == ENTRY_SIZE || datatype == ItemType::BLOB || datatype == ItemType::SZ); assert(totalSize == ENTRY_SIZE || datatype == ItemType::BLOB || datatype == ItemType::SZ);
if (mNextFreeEntry + entriesCount > ENTRY_COUNT) { if (mNextFreeEntry == INVALID_ENTRY || mNextFreeEntry + entriesCount > ENTRY_COUNT) {
// page will not fit this amount of data // page will not fit this amount of data
return ESP_ERR_NVS_PAGE_FULL; return ESP_ERR_NVS_PAGE_FULL;
} }
@ -158,8 +156,8 @@ esp_err_t Page::writeItem(uint8_t nsIndex, ItemType datatype, const char* key, c
std::fill_n(reinterpret_cast<uint32_t*>(item.key), sizeof(item.key) / 4, 0xffffffff); std::fill_n(reinterpret_cast<uint32_t*>(item.key), sizeof(item.key) / 4, 0xffffffff);
std::fill_n(reinterpret_cast<uint32_t*>(item.data), sizeof(item.data) / 4, 0xffffffff); std::fill_n(reinterpret_cast<uint32_t*>(item.data), sizeof(item.data) / 4, 0xffffffff);
strlcpy(item.key, key, Item::MAX_KEY_LENGTH + 1); strncpy(item.key, key, sizeof(item.key) - 1);
item.key[sizeof(item.key) - 1] = 0;
if (datatype != ItemType::SZ && datatype != ItemType::BLOB) { if (datatype != ItemType::SZ && datatype != ItemType::BLOB) {
memcpy(item.data, data, dataSize); memcpy(item.data, data, dataSize);
@ -295,10 +293,14 @@ esp_err_t Page::eraseEntryAndSpan(size_t index)
} else { } else {
span = item.span; span = item.span;
for (ptrdiff_t i = index + span - 1; i >= static_cast<ptrdiff_t>(index); --i) { for (ptrdiff_t i = index + span - 1; i >= static_cast<ptrdiff_t>(index); --i) {
if (mEntryTable.get(i) == EntryState::WRITTEN) {
--mUsedEntryCount;
}
rc = alterEntryState(i, EntryState::ERASED); rc = alterEntryState(i, EntryState::ERASED);
if (rc != ESP_OK) { if (rc != ESP_OK) {
return rc; return rc;
} }
++mErasedEntryCount;
} }
} }
} }
@ -312,9 +314,10 @@ esp_err_t Page::eraseEntryAndSpan(size_t index)
if (index == mFirstUsedEntry) { if (index == mFirstUsedEntry) {
updateFirstUsedEntry(index, span); updateFirstUsedEntry(index, span);
} }
mErasedEntryCount += span; if (index + span > mNextFreeEntry) {
mUsedEntryCount -= span; mNextFreeEntry = index + span;
}
return ESP_OK; return ESP_OK;
} }
@ -323,7 +326,11 @@ void Page::updateFirstUsedEntry(size_t index, size_t span)
{ {
assert(index == mFirstUsedEntry); assert(index == mFirstUsedEntry);
mFirstUsedEntry = INVALID_ENTRY; mFirstUsedEntry = INVALID_ENTRY;
for (size_t i = index + span; i < mNextFreeEntry; ++i) { size_t end = mNextFreeEntry;
if (end > ENTRY_COUNT) {
end = ENTRY_COUNT;
}
for (size_t i = index + span; i < end; ++i) {
if (mEntryTable.get(i) == EntryState::WRITTEN) { if (mEntryTable.get(i) == EntryState::WRITTEN) {
mFirstUsedEntry = i; mFirstUsedEntry = i;
break; break;
@ -357,10 +364,6 @@ esp_err_t Page::moveItem(Page& other)
if (err != ESP_OK) { if (err != ESP_OK) {
return err; return err;
} }
err = eraseEntry(mFirstUsedEntry);
if (err != ESP_OK) {
return err;
}
size_t span = entry.span; size_t span = entry.span;
size_t end = mFirstUsedEntry + span; size_t end = mFirstUsedEntry + span;
@ -373,6 +376,8 @@ esp_err_t Page::moveItem(Page& other)
if (err != ESP_OK) { if (err != ESP_OK) {
return err; return err;
} }
}
for (size_t i = mFirstUsedEntry; i < end; ++i) {
err = eraseEntry(i); err = eraseEntry(i);
if (err != ESP_OK) { if (err != ESP_OK) {
return err; return err;
@ -427,30 +432,39 @@ esp_err_t Page::mLoadEntryTable()
// but before the entry state table was altered, the entry locacted via // but before the entry state table was altered, the entry locacted via
// entry state table may actually be half-written. // entry state table may actually be half-written.
// this is easy to check by reading EntryHeader (i.e. first word) // this is easy to check by reading EntryHeader (i.e. first word)
uint32_t entryAddress = mBaseAddress + ENTRY_DATA_OFFSET + if (mNextFreeEntry != INVALID_ENTRY) {
static_cast<uint32_t>(mNextFreeEntry) * ENTRY_SIZE; uint32_t entryAddress = getEntryAddress(mNextFreeEntry);
uint32_t header; uint32_t header;
auto rc = spi_flash_read(entryAddress, &header, sizeof(header)); auto rc = spi_flash_read(entryAddress, &header, sizeof(header));
if (rc != ESP_OK) { if (rc != ESP_OK) {
mState = PageState::INVALID;
return rc;
}
if (header != 0xffffffff) {
auto err = alterEntryState(mNextFreeEntry, EntryState::ERASED);
if (err != ESP_OK) {
mState = PageState::INVALID; mState = PageState::INVALID;
return err; return rc;
}
if (header != 0xffffffff) {
auto err = alterEntryState(mNextFreeEntry, EntryState::ERASED);
if (err != ESP_OK) {
mState = PageState::INVALID;
return err;
}
++mNextFreeEntry;
} }
++mNextFreeEntry;
} }
// check that all variable-length items are written or erased fully // check that all variable-length items are written or erased fully
for (size_t i = 0; i < mNextFreeEntry; ++i) { Item item;
size_t lastItemIndex = INVALID_ENTRY;
size_t end = mNextFreeEntry;
if (end > ENTRY_COUNT) {
end = ENTRY_COUNT;
}
for (size_t i = 0; i < end; ++i) {
if (mEntryTable.get(i) == EntryState::ERASED) { if (mEntryTable.get(i) == EntryState::ERASED) {
lastItemIndex = INVALID_ENTRY;
continue; continue;
} }
lastItemIndex = i;
Item item;
auto err = readEntry(i, item); auto err = readEntry(i, item);
if (err != ESP_OK) { if (err != ESP_OK) {
mState = PageState::INVALID; mState = PageState::INVALID;
@ -475,6 +489,7 @@ esp_err_t Page::mLoadEntryTable()
for (size_t j = i; j < i + span; ++j) { for (size_t j = i; j < i + span; ++j) {
if (mEntryTable.get(j) != EntryState::WRITTEN) { if (mEntryTable.get(j) != EntryState::WRITTEN) {
needErase = true; needErase = true;
lastItemIndex = INVALID_ENTRY;
break; break;
} }
} }
@ -483,7 +498,21 @@ esp_err_t Page::mLoadEntryTable()
} }
i += span - 1; i += span - 1;
} }
// check that last item is not duplicate
if (lastItemIndex != INVALID_ENTRY) {
size_t findItemIndex = 0;
Item dupItem;
if (findItem(item.nsIndex, item.datatype, item.key, findItemIndex, dupItem) == ESP_OK) {
if (findItemIndex < lastItemIndex) {
auto err = eraseEntryAndSpan(findItemIndex);
if (err != ESP_OK) {
mState = PageState::INVALID;
return err;
}
}
}
}
} }
return ESP_OK; return ESP_OK;
@ -536,7 +565,7 @@ esp_err_t Page::alterPageState(PageState state)
return ESP_OK; return ESP_OK;
} }
esp_err_t Page::readEntry(size_t index, Item& dst) esp_err_t Page::readEntry(size_t index, Item& dst) const
{ {
auto rc = spi_flash_read(getEntryAddress(index), reinterpret_cast<uint32_t*>(&dst), sizeof(dst)); auto rc = spi_flash_read(getEntryAddress(index), reinterpret_cast<uint32_t*>(&dst), sizeof(dst));
if (rc != ESP_OK) { if (rc != ESP_OK) {
@ -550,6 +579,10 @@ esp_err_t Page::findItem(uint8_t nsIndex, ItemType datatype, const char* key, si
if (mState == PageState::CORRUPT || mState == PageState::INVALID || mState == PageState::UNINITIALIZED) { if (mState == PageState::CORRUPT || mState == PageState::INVALID || mState == PageState::UNINITIALIZED) {
return ESP_ERR_NVS_NOT_FOUND; return ESP_ERR_NVS_NOT_FOUND;
} }
if (itemIndex >= ENTRY_COUNT) {
return ESP_ERR_NVS_NOT_FOUND;
}
CachedFindInfo findInfo(nsIndex, datatype, key); CachedFindInfo findInfo(nsIndex, datatype, key);
if (mFindInfo == findInfo) { if (mFindInfo == findInfo) {
@ -560,9 +593,14 @@ esp_err_t Page::findItem(uint8_t nsIndex, ItemType datatype, const char* key, si
if (itemIndex > mFirstUsedEntry && itemIndex < ENTRY_COUNT) { if (itemIndex > mFirstUsedEntry && itemIndex < ENTRY_COUNT) {
start = itemIndex; start = itemIndex;
} }
size_t end = mNextFreeEntry;
if (end > ENTRY_COUNT) {
end = ENTRY_COUNT;
}
size_t next; size_t next;
for (size_t i = start; i < mNextFreeEntry; i = next) { for (size_t i = start; i < end; i = next) {
next = i + 1; next = i + 1;
if (mEntryTable.get(i) != EntryState::WRITTEN) { if (mEntryTable.get(i) != EntryState::WRITTEN) {
continue; continue;
@ -658,7 +696,7 @@ void Page::invalidateCache()
mFindInfo = CachedFindInfo(); mFindInfo = CachedFindInfo();
} }
void Page::debugDump() void Page::debugDump() const
{ {
printf("state=%x addr=%x seq=%d\nfirstUsed=%d nextFree=%d used=%d erased=%d\n", mState, mBaseAddress, mSeqNumber, static_cast<int>(mFirstUsedEntry), static_cast<int>(mNextFreeEntry), mUsedEntryCount, mErasedEntryCount); printf("state=%x addr=%x seq=%d\nfirstUsed=%d nextFree=%d used=%d erased=%d\n", mState, mBaseAddress, mSeqNumber, static_cast<int>(mFirstUsedEntry), static_cast<int>(mNextFreeEntry), mUsedEntryCount, mErasedEntryCount);
size_t skip = 0; size_t skip = 0;

View File

@ -40,7 +40,7 @@ public:
bool operator==(const CachedFindInfo& other) const bool operator==(const CachedFindInfo& other) const
{ {
return mKeyPtr == other.mKeyPtr && mType == other.mType && mNsIndex == other.mNsIndex; return mKeyPtr != nullptr && mKeyPtr == other.mKeyPtr && mType == other.mType && mNsIndex == other.mNsIndex;
} }
void setItemIndex(uint32_t index) void setItemIndex(uint32_t index)
@ -162,7 +162,7 @@ public:
void invalidateCache(); void invalidateCache();
void debugDump(); void debugDump() const;
protected: protected:
@ -195,7 +195,7 @@ protected:
esp_err_t alterPageState(PageState state); esp_err_t alterPageState(PageState state);
esp_err_t readEntry(size_t index, Item& dst); esp_err_t readEntry(size_t index, Item& dst) const;
esp_err_t writeEntry(const Item& item); esp_err_t writeEntry(const Item& item);
@ -210,7 +210,7 @@ protected:
return static_cast<uint8_t>(type) & 0x0f; return static_cast<uint8_t>(type) & 0x0f;
} }
uint32_t getEntryAddress(size_t entry) uint32_t getEntryAddress(size_t entry) const
{ {
assert(entry < ENTRY_COUNT); assert(entry < ENTRY_COUNT);
return mBaseAddress + ENTRY_DATA_OFFSET + static_cast<uint32_t>(entry) * ENTRY_SIZE; return mBaseAddress + ENTRY_DATA_OFFSET + static_cast<uint32_t>(entry) * ENTRY_SIZE;

View File

@ -53,6 +53,58 @@ esp_err_t PageManager::load(uint32_t baseSector, uint32_t sectorCount)
assert(mPageList.back().getSeqNumber(lastSeqNo) == ESP_OK); assert(mPageList.back().getSeqNumber(lastSeqNo) == ESP_OK);
mSeqNumber = lastSeqNo + 1; mSeqNumber = lastSeqNo + 1;
} }
// if power went out after a new item for the given key was written,
// but before the old one was erased, we end up with a duplicate item
Page& lastPage = back();
size_t lastItemIndex = SIZE_MAX;
Item item;
size_t itemIndex = 0;
while (lastPage.findItem(Page::NS_ANY, ItemType::ANY, nullptr, itemIndex, item) == ESP_OK) {
itemIndex += item.span;
lastItemIndex = itemIndex;
}
if (lastItemIndex != SIZE_MAX) {
auto last = PageManager::TPageListIterator(&lastPage);
for (auto it = begin(); it != last; ++it) {
if (it->eraseItem(item.nsIndex, item.datatype, item.key) == ESP_OK) {
break;
}
}
}
// check if power went out while page was being freed
for (auto it = begin(); it!= end(); ++it) {
if (it->state() == Page::PageState::FREEING) {
Page* newPage = &mPageList.back();
if(newPage->state() != Page::PageState::ACTIVE) {
auto err = activatePage();
if (err != ESP_OK) {
return err;
}
newPage = &mPageList.back();
}
while (true) {
auto err = it->moveItem(*newPage);
if (err == ESP_ERR_NVS_NOT_FOUND) {
break;
} else if (err != ESP_OK) {
return err;
}
}
auto err = it->erase();
if (err != ESP_OK) {
return err;
}
Page* p = static_cast<Page*>(it);
mPageList.erase(it);
mFreePageList.push_back(p);
break;
}
}
return ESP_OK; return ESP_OK;
} }
@ -91,6 +143,7 @@ esp_err_t PageManager::requestNewPage()
Page* newPage = &mPageList.back(); Page* newPage = &mPageList.back();
Page* erasedPage = maxErasedItemsPageIt; Page* erasedPage = maxErasedItemsPageIt;
size_t usedEntries = erasedPage->getUsedEntryCount();
err = erasedPage->markFreeing(); err = erasedPage->markFreeing();
if (err != ESP_OK) { if (err != ESP_OK) {
return err; return err;
@ -108,6 +161,8 @@ esp_err_t PageManager::requestNewPage()
if (err != ESP_OK) { if (err != ESP_OK) {
return err; return err;
} }
assert(usedEntries == newPage->getUsedEntryCount());
mPageList.erase(maxErasedItemsPageIt); mPageList.erase(maxErasedItemsPageIt);
mFreePageList.push_back(erasedPage); mFreePageList.push_back(erasedPage);

View File

@ -13,6 +13,11 @@
// limitations under the License. // limitations under the License.
#include "nvs_storage.hpp" #include "nvs_storage.hpp"
#ifndef ESP_PLATFORM
#include <map>
#include <sstream>
#endif
namespace nvs namespace nvs
{ {
@ -39,6 +44,7 @@ esp_err_t Storage::init(uint32_t baseSector, uint32_t sectorCount)
return err; return err;
} }
// load namespaces list
clearNamespaces(); clearNamespaces();
std::fill_n(mNamespaceUsage.data(), mNamespaceUsage.byteSize() / 4, 0); std::fill_n(mNamespaceUsage.data(), mNamespaceUsage.byteSize() / 4, 0);
for (auto it = mPageManager.begin(); it != mPageManager.end(); ++it) { for (auto it = mPageManager.begin(); it != mPageManager.end(); ++it) {
@ -51,11 +57,15 @@ esp_err_t Storage::init(uint32_t baseSector, uint32_t sectorCount)
item.getValue(entry->mIndex); item.getValue(entry->mIndex);
mNamespaces.push_back(entry); mNamespaces.push_back(entry);
mNamespaceUsage.set(entry->mIndex, true); mNamespaceUsage.set(entry->mIndex, true);
itemIndex += item.span;
} }
} }
mNamespaceUsage.set(0, true); mNamespaceUsage.set(0, true);
mNamespaceUsage.set(255, true); mNamespaceUsage.set(255, true);
mState = StorageState::ACTIVE; mState = StorageState::ACTIVE;
#ifndef ESP_PLATFORM
debugCheck();
#endif
return ESP_OK; return ESP_OK;
} }
@ -75,29 +85,46 @@ esp_err_t Storage::writeItem(uint8_t nsIndex, ItemType datatype, const char* key
Page& page = getCurrentPage(); Page& page = getCurrentPage();
err = page.writeItem(nsIndex, datatype, key, data, dataSize); err = page.writeItem(nsIndex, datatype, key, data, dataSize);
if (err == ESP_ERR_NVS_PAGE_FULL) { if (err == ESP_ERR_NVS_PAGE_FULL) {
page.markFull(); if (page.state() != Page::PageState::FULL) {
err = page.markFull();
if (err != ESP_OK) {
return err;
}
}
err = mPageManager.requestNewPage(); err = mPageManager.requestNewPage();
if (err != ESP_OK) { if (err != ESP_OK) {
return err; return err;
} }
err = getCurrentPage().writeItem(nsIndex, datatype, key, data, dataSize); err = getCurrentPage().writeItem(nsIndex, datatype, key, data, dataSize);
if (err == ESP_ERR_NVS_PAGE_FULL) {
return ESP_ERR_NVS_NOT_ENOUGH_SPACE;
}
if (err != ESP_OK) { if (err != ESP_OK) {
return err; return err;
} }
} }
else if (err != ESP_OK) {
return err;
}
if (findPage) { if (findPage) {
if (findPage->state() == Page::PageState::UNINITIALIZED) { if (findPage->state() == Page::PageState::UNINITIALIZED ||
findPage->state() == Page::PageState::INVALID) {
auto err = findItem(nsIndex, datatype, key, findPage, item); auto err = findItem(nsIndex, datatype, key, findPage, item);
assert(err == ESP_OK); assert(err == ESP_OK);
} }
err = findPage->eraseItem(nsIndex, datatype, key); err = findPage->eraseItem(nsIndex, datatype, key);
if (err == ESP_ERR_FLASH_OP_FAIL) {
return ESP_ERR_NVS_REMOVE_FAILED;
}
if (err != ESP_OK) { if (err != ESP_OK) {
return err; return err;
} }
} }
#ifndef ESP_PLATFORM
debugCheck();
#endif
return ESP_OK; return ESP_OK;
} }
@ -134,7 +161,8 @@ esp_err_t Storage::createOrOpenNamespace(const char* nsName, bool canCreate, uin
NamespaceEntry* entry = new NamespaceEntry; NamespaceEntry* entry = new NamespaceEntry;
entry->mIndex = ns; entry->mIndex = ns;
strlcpy(entry->mName, nsName, sizeof(entry->mName)); strncpy(entry->mName, nsName, sizeof(entry->mName) - 1);
entry->mName[sizeof(entry->mName) - 1] = 0;
mNamespaces.push_back(entry); mNamespaces.push_back(entry);
} else { } else {
@ -192,4 +220,35 @@ esp_err_t Storage::getItemDataSize(uint8_t nsIndex, ItemType datatype, const cha
return ESP_OK; return ESP_OK;
} }
} void Storage::debugDump()
{
for (auto p = mPageManager.begin(); p != mPageManager.end(); ++p) {
p->debugDump();
}
}
#ifndef ESP_PLATFORM
void Storage::debugCheck()
{
std::map<std::string, Page*> keys;
for (auto p = mPageManager.begin(); p != mPageManager.end(); ++p) {
size_t itemIndex = 0;
Item item;
while (p->findItem(Page::NS_ANY, ItemType::ANY, nullptr, itemIndex, item) == ESP_OK) {
std::stringstream keyrepr;
keyrepr << static_cast<unsigned>(item.nsIndex) << "_" << static_cast<unsigned>(item.datatype) << "_" << item.key;
std::string keystr = keyrepr.str();
if (keys.find(keystr) != std::end(keys)) {
printf("Duplicate key: %s\n", keystr.c_str());
debugDump();
assert(0);
}
keys.insert(std::make_pair(keystr, static_cast<Page*>(p)));
itemIndex += item.span;
}
}
}
#endif //ESP_PLATFORM
}

View File

@ -74,6 +74,9 @@ public:
{ {
return eraseItem(nsIndex, itemTypeOf<T>(), key); return eraseItem(nsIndex, itemTypeOf<T>(), key);
} }
void debugDump();
void debugCheck();
protected: protected:

View File

@ -1,5 +1,5 @@
TEST_PROGRAM=test_nvs TEST_PROGRAM=test_nvs
all: test all: $(TEST_PROGRAM)
SOURCE_FILES = \ SOURCE_FILES = \
$(addprefix ../src/, \ $(addprefix ../src/, \
@ -14,19 +14,22 @@ SOURCE_FILES = \
test_spi_flash_emulation.cpp \ test_spi_flash_emulation.cpp \
test_intrusive_list.cpp \ test_intrusive_list.cpp \
test_nvs.cpp \ test_nvs.cpp \
crc.cpp \ crc.cpp \
main.cpp main.cpp
CPPFLAGS += -I../include -I../src -I./ -I../../esp32/include -I ../../spi_flash/include CPPFLAGS += -I../include -I../src -I./ -I../../esp32/include -I ../../spi_flash/include -fprofile-arcs -ftest-coverage
CFLAGS += -fprofile-arcs -ftest-coverage
CXXFLAGS += -std=c++11 -Wall -Werror CXXFLAGS += -std=c++11 -Wall -Werror
LDFLAGS += -lstdc++ LDFLAGS += -lstdc++ -Wall -fprofile-arcs -ftest-coverage
OBJ_FILES = $(SOURCE_FILES:.cpp=.o) OBJ_FILES = $(SOURCE_FILES:.cpp=.o)
COVERAGE_FILES = $(OBJ_FILES:.o=.gc*)
$(OBJ_FILES): %.o: %.cpp $(OBJ_FILES): %.o: %.cpp
$(TEST_PROGRAM): $(OBJ_FILES) $(TEST_PROGRAM): $(OBJ_FILES)
gcc $(LDFLAGS) -o $(TEST_PROGRAM) $(OBJ_FILES) g++ $(LDFLAGS) -o $(TEST_PROGRAM) $(OBJ_FILES)
$(OUTPUT_DIR): $(OUTPUT_DIR):
mkdir -p $(OUTPUT_DIR) mkdir -p $(OUTPUT_DIR)
@ -34,7 +37,23 @@ $(OUTPUT_DIR):
test: $(TEST_PROGRAM) test: $(TEST_PROGRAM)
./$(TEST_PROGRAM) ./$(TEST_PROGRAM)
long-test: $(TEST_PROGRAM)
./$(TEST_PROGRAM) [list],[enumtable],[spi_flash_emu],[nvs],[long]
$(COVERAGE_FILES): $(TEST_PROGRAM) long-test
coverage.info: $(COVERAGE_FILES)
find ../src/ -name "*.gcno" -exec gcov -r -pb {} +
lcov --capture --directory ../src --no-external --output-file coverage.info
coverage_report: coverage.info
genhtml coverage.info --output-directory coverage_report
@echo "Coverage report is in coverage_report/index.html"
clean: clean:
rm -f $(OBJ_FILES) $(TEST_PROGRAM) rm -f $(OBJ_FILES) $(TEST_PROGRAM)
rm -f $(COVERAGE_FILES) *.gcov
rm -rf coverage_report/
rm -f coverage.info
.PHONY: clean all test .PHONY: clean all test

View File

@ -73,6 +73,10 @@ public:
dstAddr + size > mData.size() * 4) { dstAddr + size > mData.size() * 4) {
return false; return false;
} }
if (mFailCountdown != SIZE_MAX && mFailCountdown-- == 0) {
return false;
}
for (size_t i = 0; i < size / 4; ++i) { for (size_t i = 0; i < size / 4; ++i) {
uint32_t sv = src[i]; uint32_t sv = src[i];
@ -103,6 +107,10 @@ public:
WARN("invalid flash operation detected: erase sector=" << sectorNumber); WARN("invalid flash operation detected: erase sector=" << sectorNumber);
return false; return false;
} }
if (mFailCountdown != SIZE_MAX && mFailCountdown-- == 0) {
return false;
}
std::fill_n(begin(mData) + offset, SPI_FLASH_SEC_SIZE / 4, 0xffffffff); std::fill_n(begin(mData) + offset, SPI_FLASH_SEC_SIZE / 4, 0xffffffff);
@ -173,6 +181,10 @@ public:
mLowerSectorBound = lowerSector; mLowerSectorBound = lowerSector;
mUpperSectorBound = upperSector; mUpperSectorBound = upperSector;
} }
void failAfter(uint32_t count) {
mFailCountdown = count;
}
protected: protected:
static size_t getReadOpTime(uint32_t bytes); static size_t getReadOpTime(uint32_t bytes);
@ -190,6 +202,8 @@ protected:
mutable size_t mTotalTime = 0; mutable size_t mTotalTime = 0;
size_t mLowerSectorBound = 0; size_t mLowerSectorBound = 0;
size_t mUpperSectorBound = 0; size_t mUpperSectorBound = 0;
size_t mFailCountdown = SIZE_MAX;
}; };

View File

@ -19,7 +19,8 @@
struct TestNode : public intrusive_list_node<TestNode> { struct TestNode : public intrusive_list_node<TestNode> {
TestNode(const char* name_ = "", int num_ = 0) : num(num_) TestNode(const char* name_ = "", int num_ = 0) : num(num_)
{ {
strlcpy(name, name_, sizeof(name)); strncpy(name, name_, sizeof(name) - 1);
name[sizeof(name) - 1] = 0;
} }
char name[32]; char name[32];
int num; int num;

View File

@ -62,7 +62,7 @@ TEST_CASE("crc32 behaves as expected", "[nvs]")
CHECK(crc32_1 != item2.calculateCrc32()); CHECK(crc32_1 != item2.calculateCrc32());
item2 = item1; item2 = item1;
strlcpy(item2.key, "foo", Item::MAX_KEY_LENGTH); strncpy(item2.key, "foo", Item::MAX_KEY_LENGTH);
CHECK(crc32_1 != item2.calculateCrc32()); CHECK(crc32_1 != item2.calculateCrc32());
} }
@ -153,9 +153,7 @@ TEST_CASE("when page is full, adding an element fails", "[nvs]")
snprintf(name, sizeof(name), "i%ld", i); snprintf(name, sizeof(name), "i%ld", i);
CHECK(page.writeItem(1, name, i) == ESP_OK); CHECK(page.writeItem(1, name, i) == ESP_OK);
} }
CHECK(page.state() == Page::PageState::FULL);
CHECK(page.writeItem(1, "foo", 64UL) == ESP_ERR_NVS_PAGE_FULL); CHECK(page.writeItem(1, "foo", 64UL) == ESP_ERR_NVS_PAGE_FULL);
CHECK(page.state() == Page::PageState::FULL);
} }
TEST_CASE("page maintains its seq number") TEST_CASE("page maintains its seq number")
@ -527,162 +525,189 @@ TEST_CASE("nvs api tests, starting with random data in flash", "[nvs][.][long]")
} }
} }
template<typename TGen> extern "C" void nvs_dump();
esp_err_t doRandomThings(nvs_handle handle, TGen gen, size_t count) {
class RandomTest {
const char* keys[] = {"foo", "bar", "longkey_0123456", "another key", "param1", "param2", "param3", "param4", "param5"};
const ItemType types[] = {ItemType::I32, ItemType::I32, ItemType::U64, ItemType::U64, ItemType::SZ, ItemType::SZ, ItemType::SZ, ItemType::SZ, ItemType::SZ};
static const size_t nKeys = 9;
int32_t v1 = 0, v2 = 0; int32_t v1 = 0, v2 = 0;
uint64_t v3 = 0, v4 = 0; uint64_t v3 = 0, v4 = 0;
const size_t strBufLen = 1024; static const size_t strBufLen = 1024;
char v5[strBufLen], v6[strBufLen], v7[strBufLen], v8[strBufLen], v9[strBufLen]; char v5[strBufLen], v6[strBufLen], v7[strBufLen], v8[strBufLen], v9[strBufLen];
void* values[] = {&v1, &v2, &v3, &v4, &v5, &v6, &v7, &v8, &v9};
const size_t nKeys = sizeof(keys) / sizeof(keys[0]);
static_assert(nKeys == sizeof(types) / sizeof(types[0]), "");
static_assert(nKeys == sizeof(values) / sizeof(values[0]), "");
bool written[nKeys]; bool written[nKeys];
std::fill_n(written, nKeys, false);
auto generateRandomString = [](char* dst, size_t size) { public:
size_t len = 0; RandomTest()
}; {
std::fill_n(written, nKeys, false);
auto randomRead = [&](size_t index) -> esp_err_t {
switch (types[index]) {
case ItemType::I32:
{
int32_t val;
auto err = nvs_get_i32(handle, keys[index], &val);
if (err == ESP_ERR_FLASH_OP_FAIL) {
return err;
}
if (!written[index]) {
REQUIRE(err == ESP_ERR_NVS_NOT_FOUND);
}
else {
REQUIRE(val == *reinterpret_cast<int32_t*>(values[index]));
}
break;
}
case ItemType::U64:
{
uint64_t val;
auto err = nvs_get_u64(handle, keys[index], &val);
if (err == ESP_ERR_FLASH_OP_FAIL) {
return err;
}
if (!written[index]) {
REQUIRE(err == ESP_ERR_NVS_NOT_FOUND);
}
else {
REQUIRE(val == *reinterpret_cast<uint64_t*>(values[index]));
}
break;
}
case ItemType::SZ:
{
char buf[strBufLen];
size_t len = strBufLen;
auto err = nvs_get_str(handle, keys[index], buf, &len);
if (err == ESP_ERR_FLASH_OP_FAIL) {
return err;
}
if (!written[index]) {
REQUIRE(err == ESP_ERR_NVS_NOT_FOUND);
}
else {
REQUIRE(strncmp(buf, reinterpret_cast<const char*>(values[index]), strBufLen));
}
break;
}
default:
assert(0);
}
return ESP_OK;
};
auto randomWrite = [&](size_t index) -> esp_err_t {
switch (types[index]) {
case ItemType::I32:
{
int32_t val = static_cast<int32_t>(gen());
*reinterpret_cast<int32_t*>(values[index]) = val;
auto err = nvs_set_i32(handle, keys[index], val);
if (err == ESP_ERR_FLASH_OP_FAIL) {
return err;
}
REQUIRE(err == ESP_OK);
written[index] = true;
break;
}
case ItemType::U64:
{
uint64_t val = static_cast<uint64_t>(gen());
*reinterpret_cast<uint64_t*>(values[index]) = val;
auto err = nvs_set_u64(handle, keys[index], val);
if (err == ESP_ERR_FLASH_OP_FAIL) {
return err;
}
REQUIRE(err == ESP_OK);
written[index] = true;
break;
}
case ItemType::SZ:
{
char buf[strBufLen];
size_t len = strBufLen;
size_t strLen = gen() % (strBufLen - 1);
std::generate_n(buf, strLen, [&]() -> char {
const char c = static_cast<char>(gen() % 127);
return (c < 32) ? 32 : c;
});
auto err = nvs_set_str(handle, keys[index], buf);
if (err == ESP_ERR_FLASH_OP_FAIL) {
return err;
}
REQUIRE(err == ESP_OK);
written[index] = true;
break;
}
default:
assert(0);
}
return ESP_OK;
};
for (size_t i = 0; i < count; ++i) {
size_t index = gen() % nKeys;
switch (gen() % 3) {
case 0: // read, 1/3
if (randomRead(index) == ESP_ERR_FLASH_OP_FAIL) {
return ESP_ERR_FLASH_OP_FAIL;
}
break;
default: // write, 2/3
if (randomWrite(index) == ESP_ERR_FLASH_OP_FAIL) {
return ESP_ERR_FLASH_OP_FAIL;
}
break;
}
} }
return ESP_OK;
} template<typename TGen>
esp_err_t doRandomThings(nvs_handle handle, TGen gen, size_t& count) {
const char* keys[] = {"foo", "bar", "longkey_0123456", "another key", "param1", "param2", "param3", "param4", "param5"};
const ItemType types[] = {ItemType::I32, ItemType::I32, ItemType::U64, ItemType::U64, ItemType::SZ, ItemType::SZ, ItemType::SZ, ItemType::SZ, ItemType::SZ};
void* values[] = {&v1, &v2, &v3, &v4, &v5, &v6, &v7, &v8, &v9};
const size_t nKeys = sizeof(keys) / sizeof(keys[0]);
static_assert(nKeys == sizeof(types) / sizeof(types[0]), "");
static_assert(nKeys == sizeof(values) / sizeof(values[0]), "");
auto randomRead = [&](size_t index) -> esp_err_t {
switch (types[index]) {
case ItemType::I32:
{
int32_t val;
auto err = nvs_get_i32(handle, keys[index], &val);
if (err == ESP_ERR_FLASH_OP_FAIL) {
return err;
}
if (!written[index]) {
REQUIRE(err == ESP_ERR_NVS_NOT_FOUND);
}
else {
REQUIRE(err == ESP_OK);
REQUIRE(val == *reinterpret_cast<int32_t*>(values[index]));
}
break;
}
case ItemType::U64:
{
uint64_t val;
auto err = nvs_get_u64(handle, keys[index], &val);
if (err == ESP_ERR_FLASH_OP_FAIL) {
return err;
}
if (!written[index]) {
REQUIRE(err == ESP_ERR_NVS_NOT_FOUND);
}
else {
REQUIRE(err == ESP_OK);
REQUIRE(val == *reinterpret_cast<uint64_t*>(values[index]));
}
break;
}
case ItemType::SZ:
{
char buf[strBufLen];
size_t len = strBufLen;
auto err = nvs_get_str(handle, keys[index], buf, &len);
if (err == ESP_ERR_FLASH_OP_FAIL) {
return err;
}
if (!written[index]) {
REQUIRE(err == ESP_ERR_NVS_NOT_FOUND);
}
else {
REQUIRE(err == ESP_OK);
REQUIRE(strncmp(buf, reinterpret_cast<const char*>(values[index]), strBufLen) == 0);
}
break;
}
default:
assert(0);
}
return ESP_OK;
};
auto randomWrite = [&](size_t index) -> esp_err_t {
switch (types[index]) {
case ItemType::I32:
{
int32_t val = static_cast<int32_t>(gen());
auto err = nvs_set_i32(handle, keys[index], val);
if (err == ESP_ERR_FLASH_OP_FAIL) {
return err;
}
if (err == ESP_ERR_NVS_REMOVE_FAILED) {
written[index] = true;
*reinterpret_cast<int32_t*>(values[index]) = val;
return ESP_ERR_FLASH_OP_FAIL;
}
REQUIRE(err == ESP_OK);
written[index] = true;
*reinterpret_cast<int32_t*>(values[index]) = val;
break;
}
case ItemType::U64:
{
uint64_t val = static_cast<uint64_t>(gen());
auto err = nvs_set_u64(handle, keys[index], val);
if (err == ESP_ERR_FLASH_OP_FAIL) {
return err;
}
if (err == ESP_ERR_NVS_REMOVE_FAILED) {
written[index] = true;
*reinterpret_cast<uint64_t*>(values[index]) = val;
return ESP_ERR_FLASH_OP_FAIL;
}
REQUIRE(err == ESP_OK);
written[index] = true;
*reinterpret_cast<uint64_t*>(values[index]) = val;
break;
}
case ItemType::SZ:
{
char buf[strBufLen];
size_t len = strBufLen;
size_t strLen = gen() % (strBufLen - 1);
std::generate_n(buf, strLen, [&]() -> char {
const char c = static_cast<char>(gen() % 127);
return (c < 32) ? 32 : c;
});
buf[strLen] = 0;
auto err = nvs_set_str(handle, keys[index], buf);
if (err == ESP_ERR_FLASH_OP_FAIL) {
return err;
}
if (err == ESP_ERR_NVS_REMOVE_FAILED) {
written[index] = true;
strncpy(reinterpret_cast<char*>(values[index]), buf, strBufLen);
return ESP_ERR_FLASH_OP_FAIL;
}
REQUIRE(err == ESP_OK);
written[index] = true;
strncpy(reinterpret_cast<char*>(values[index]), buf, strBufLen);
break;
}
default:
assert(0);
}
return ESP_OK;
};
for (; count != 0; --count) {
size_t index = gen() % nKeys;
switch (gen() % 3) {
case 0: // read, 1/3
if (randomRead(index) == ESP_ERR_FLASH_OP_FAIL) {
return ESP_ERR_FLASH_OP_FAIL;
}
break;
default: // write, 2/3
if (randomWrite(index) == ESP_ERR_FLASH_OP_FAIL) {
return ESP_ERR_FLASH_OP_FAIL;
}
break;
}
}
return ESP_OK;
}
};
TEST_CASE("monkey test", "[nvs][monkey]") TEST_CASE("monkey test", "[nvs][monkey]")
{ {
@ -693,6 +718,7 @@ TEST_CASE("monkey test", "[nvs][monkey]")
SpiFlashEmulator emu(10); SpiFlashEmulator emu(10);
emu.randomize(seed); emu.randomize(seed);
emu.clearStats();
const uint32_t NVS_FLASH_SECTOR = 6; const uint32_t NVS_FLASH_SECTOR = 6;
const uint32_t NVS_FLASH_SECTOR_COUNT_MIN = 3; const uint32_t NVS_FLASH_SECTOR_COUNT_MIN = 3;
@ -702,9 +728,66 @@ TEST_CASE("monkey test", "[nvs][monkey]")
nvs_handle handle; nvs_handle handle;
TEST_ESP_OK(nvs_open("namespace1", NVS_READWRITE, &handle)); TEST_ESP_OK(nvs_open("namespace1", NVS_READWRITE, &handle));
RandomTest test;
size_t count = 1000;
CHECK(test.doRandomThings(handle, gen, count) == ESP_OK);
s_perf << "Monkey test: nErase=" << emu.getEraseOps() << " nWrite=" << emu.getWriteOps() << std::endl;
}
CHECK(doRandomThings(handle, gen, 10000) == ESP_OK); TEST_CASE("test recovery from sudden poweroff", "[.][long][nvs][recovery][monkey]")
{
std::random_device rd;
std::mt19937 gen(rd());
uint32_t seed = 3;
gen.seed(seed);
const size_t iter_count = 2000;
SpiFlashEmulator emu(10);
const uint32_t NVS_FLASH_SECTOR = 6;
const uint32_t NVS_FLASH_SECTOR_COUNT_MIN = 3;
emu.setBounds(NVS_FLASH_SECTOR, NVS_FLASH_SECTOR + NVS_FLASH_SECTOR_COUNT_MIN);
size_t totalOps = 0;
int lastPercent = -1;
for (uint32_t errDelay = 4; ; ++errDelay) {
INFO(errDelay);
emu.randomize(seed);
emu.clearStats();
emu.failAfter(errDelay);
RandomTest test;
if (totalOps != 0) {
int percent = errDelay * 100 / totalOps;
if (percent != lastPercent) {
printf("%d/%d (%d%%)\r\n", errDelay, static_cast<int>(totalOps), percent);
lastPercent = percent;
}
}
TEST_ESP_OK(nvs_flash_init(NVS_FLASH_SECTOR, NVS_FLASH_SECTOR_COUNT_MIN));
nvs_handle handle;
TEST_ESP_OK(nvs_open("namespace1", NVS_READWRITE, &handle));
size_t count = iter_count;
if(test.doRandomThings(handle, gen, count) != ESP_ERR_FLASH_OP_FAIL) {
nvs_close(handle);
break;
}
nvs_close(handle);
TEST_ESP_OK(nvs_flash_init(NVS_FLASH_SECTOR, NVS_FLASH_SECTOR_COUNT_MIN));
TEST_ESP_OK(nvs_open("namespace1", NVS_READWRITE, &handle));
auto res = test.doRandomThings(handle, gen, count);
if (res != ESP_OK) {
nvs_dump();
CHECK(0);
}
nvs_close(handle);
totalOps = emu.getEraseOps() + emu.getWriteOps();
}
} }
TEST_CASE("dump all performance data", "[nvs]") TEST_CASE("dump all performance data", "[nvs]")