diff --git a/test-refactor/README.md b/test-refactor/README.md
index cd37861f9..8e3fdc0d6 100644
--- a/test-refactor/README.md
+++ b/test-refactor/README.md
@@ -80,12 +80,13 @@ Translated tests:
| `wh_test_dma.c::whTest_Dma` | `misc/wh_test_dma.c::whTest_Dma` | Misc | |
| `wh_test_comm.c::whTest_Comm` | `misc/wh_test_comm.c::whTest_Comm` | Misc | Sequential mem variant only; pthread mem/tcp/shmem variants remain in the legacy harness |
| `wh_test_keystore_reqsize.c::whTest_KeystoreReqSize` | `misc/wh_test_keystore_reqsize.c::whTest_KeystoreReqSize` | Misc | |
-| `wh_test_cert.c::whTest_CertRamSim` | `server/wh_test_cert.c::whTest_CertVerify` | Server | remove ramsim coupling and migrate to server group |
-| `wh_test_crypto.c::whTest_Crypto` | `client-server/wh_test_crypto.c::{whTest_CryptoSha256, whTest_CryptoAes, whTest_CryptoEcc256}` | Client | Subset only; remaining cases listed below |
+| `wh_test_cert.c::whTest_CertRamSim` | `server/wh_test_cert.c::whTest_CertVerify` | Server | remove ramsim coupling and migrate to server group. Legacy ran FLASH and FLASH_LOG backends; the port runs the plain flash backend only -- FLASH_LOG re-run pending (see Known coverage gaps) |
+| `wh_test_crypto.c::whTest_Crypto` | `client-server/wh_test_crypto.c::{whTest_CryptoSha256, whTest_CryptoAes, whTest_CryptoEcc256}` | Client | Subset only; remaining cases listed below. Legacy ran FLASH and FLASH_LOG backends; the port runs the plain flash backend only -- FLASH_LOG re-run pending (see Known coverage gaps) |
| `wh_test_clientserver.c` (echo and server-info paths) | `client-server/wh_test_echo.c::whTest_Echo`, `client-server/wh_test_server_info.c::whTest_ServerInfo` | Client | pthread test ported, sequential test dropped |
| `wh_test_wolfcrypt_test.c::whTest_WolfCryptTest` | `client-server/wh_test_wolfcrypt.c::whTest_WolfCryptTest` | Client | |
| `wh_test_flash_ramsim.c::whTest_Flash_RamSim` | `posix/wh_test_flash_ramsim.c::{whTest_FlashWriteLock, whTest_FlashEraseProgramVerify, whTest_FlashUnitOps}` | POSIX port-specific (`whTestGroup_RunOne`) | remove ramsim coupling and migrate to server group |
-| `wh_test_nvm_flash.c::whTest_NvmFlash` | `posix/wh_test_nvm_flash.c::whTest_NvmAddOverwriteDestroy` | POSIX port-specific (`whTestGroup_RunOne`) | remove ramsim coupling and migrate to server group |
+| `wh_test_nvm_flash.c::{whTest_NvmFlash, whTest_NvmFlash_Recovery}` | `posix/wh_test_nvm_flash.c::{whTest_NvmAddOverwriteDestroy, whTest_NvmFlashLog, whTest_NvmRecovery}` | POSIX port-specific (`whTestGroup_RunOne`) | remove ramsim coupling and migrate to server group; flash-log backend exercised by `whTest_NvmFlashLog` (skipped unless `WOLFHSM_CFG_SERVER_NVM_FLASH_LOG`) |
+| `wh_test_flash_fault_inject.c` | `posix/wh_test_flash_fault_inject.c` | helper (no test) | fault-injection flash wrapper used by the recovery test |
| `wh_test_posix_threadsafe_stress.c::whTest_ThreadSafeStress` | called directly from `posix/wh_test_posix_main.c` | POSIX port-specific (direct call) | |
Not yet migrated (still live in `wolfHSM/test/`):
@@ -105,9 +106,14 @@ Not yet migrated (still live in `wolfHSM/test/`):
| `wh_test_auth.c::whTest_AuthMEM`, `whTest_AuthTCP` | |
| `wh_test_server_img_mgr.c::whTest_ServerImgMgr` | |
| `wh_test_nvmflags.c::whTest_NvmFlags` | |
-| `wh_test_flash_fault_inject.c` | |
| `wh_test_check_struct_padding.c` | |
+### Shared helpers pulled from `test/`
+- `wh_test_common.c::whTest_NvmCfgBackend` is compiled into the POSIX port build to select an NVM backend (flash or flash-log) over a ramsim flash. Used by `whTest_NvmFlashLog` today; the not-yet-migrated cert, image-manager, auth, and log tests rely on it too, so it is wired in ahead of those migrations.
+
+### Known coverage gaps
+- FLASH_LOG backend for server/client-group tests. In `test/`, cert (`whTest_CertRamSim`), crypto (`wh_ClientServer_MemThreadTest`), image-manager (`whTest_ServerImgMgr`), and client/server were each run against both the plain flash and flash-log NVM backends. The refactored server/client-group tests consume a single server context (`wh_test_posix_server.c` hard-codes `WH_NVM_FLASH_CB`), so only the plain flash backend is exercised. Restoring parity means selecting the server backend via `whTest_NvmCfgBackend` and running the server + client groups once per backend (flash, then flash-log) from `wh_test_posix_main.c`. Tracked as a follow-up; the port-specific `whTest_NvmFlashLog` already covers the flash-log NVM object lifecycle directly.
+
### Other improvements
- Add callback from `wh_Server_HandleRequestMessage` to allow sleep and avoid a busy loop
- Add client-only harness to feed invalid server inputs from the test bench with the goal of expanding coverage.
diff --git a/test-refactor/posix/Makefile b/test-refactor/posix/Makefile
index f17a9cf2a..7742b8f83 100644
--- a/test-refactor/posix/Makefile
+++ b/test-refactor/posix/Makefile
@@ -182,6 +182,11 @@ SRC_C += $(wildcard $(PROJECT_DIR)/*.c)
# an empty TU when the gate isn't satisfied.
SRC_C += $(TEST_DIR)/wh_test_posix_threadsafe_stress.c
+# Shared NVM backend selector pulled from test/; provides
+# whTest_NvmCfgBackend for tests that exercise multiple NVM
+# backends (flash and flash-log).
+SRC_C += $(TEST_DIR)/wh_test_common.c
+
## Build rules
diff --git a/test-refactor/posix/wh_test_flash_fault_inject.c b/test-refactor/posix/wh_test_flash_fault_inject.c
new file mode 100644
index 000000000..47312aa8c
--- /dev/null
+++ b/test-refactor/posix/wh_test_flash_fault_inject.c
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of wolfHSM.
+ *
+ * wolfHSM is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfHSM is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with wolfHSM. If not, see .
+ */
+/*
+ * test-refactor/wh_test_flash_fault_inject.c
+ *
+ * Flash fault-injection wrapper. Forwards every flash op to a real
+ * callback but forces an abort on the Nth Program call. Used by the
+ * NVM recovery test (wh_test_nvm_flash.c) to fail a write mid-object.
+ */
+
+/* Pick up compile-time configuration */
+#include "wolfhsm/wh_settings.h"
+
+#include
+#include /* For NULL */
+
+#include
+#include
+
+#include "wolfhsm/wh_error.h"
+#include "wh_test_flash_fault_inject.h"
+
+int whFlashFaultInject_Init(void* context, const void* config)
+{
+ whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context;
+ const whFlashFaultInjectCfg* cfg = (const whFlashFaultInjectCfg*)config;
+
+ if (ctx == NULL || cfg == NULL || cfg->realCb == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->realCb = cfg->realCb;
+ ctx->realCtx = cfg->realCtx;
+
+ if (cfg->realCb->Init != NULL)
+ return cfg->realCb->Init(cfg->realCtx, cfg->realCfg);
+
+ return WH_ERROR_OK;
+}
+
+int whFlashFaultInject_Cleanup(void* context)
+{
+ whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context;
+
+ if (ctx == NULL || ctx->realCb == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+ if (ctx->realCb->Cleanup != NULL)
+ return ctx->realCb->Cleanup(ctx->realCtx);
+
+ return WH_ERROR_OK;
+}
+
+int whFlashFaultInject_Program(void* context, uint32_t offset, uint32_t size,
+ const uint8_t* data)
+{
+ whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context;
+
+ if ((ctx == NULL) || (ctx->realCb == NULL))
+ return WH_ERROR_BADARGS;
+ /* Check if we need to simulate a failure */
+ if (ctx->failAfterPrograms > 0) {
+ ctx->failAfterPrograms--;
+ if (ctx->failAfterPrograms == 0)
+ return WH_ERROR_ABORTED;
+ }
+
+ if (ctx->realCb->Program != NULL)
+ return ctx->realCb->Program(ctx->realCtx, offset, size, data);
+
+ return WH_ERROR_OK;
+}
+
+int whFlashFaultInject_Read(void* context, uint32_t offset, uint32_t size,
+ uint8_t* data)
+{
+ whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context;
+
+ if ((ctx == NULL) || (ctx->realCb == NULL))
+ return WH_ERROR_BADARGS;
+
+ if (ctx->realCb->Read != NULL)
+ return ctx->realCb->Read(ctx->realCtx, offset, size, data);
+
+ return WH_ERROR_OK;
+}
+
+int whFlashFaultInject_Erase(void* context, uint32_t offset, uint32_t size)
+{
+ whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context;
+
+ if ((ctx == NULL) || (ctx->realCb == NULL))
+ return WH_ERROR_BADARGS;
+
+ if (ctx->realCb->Erase != NULL)
+ return ctx->realCb->Erase(ctx->realCtx, offset, size);
+
+ return WH_ERROR_OK;
+}
+
+int whFlashFaultInject_Verify(void* context, uint32_t offset, uint32_t size,
+ const uint8_t* data)
+{
+ whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context;
+
+ if ((ctx == NULL) || (ctx->realCb == NULL))
+ return WH_ERROR_BADARGS;
+
+ if (ctx->realCb->Verify != NULL)
+ return ctx->realCb->Verify(ctx->realCtx, offset, size, data);
+
+ return WH_ERROR_OK;
+}
+
+int whFlashFaultInject_BlankCheck(void* context, uint32_t offset, uint32_t size)
+{
+ whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context;
+
+ if ((ctx == NULL) || (ctx->realCb == NULL))
+ return WH_ERROR_BADARGS;
+
+ if (ctx->realCb->BlankCheck != NULL)
+ return ctx->realCb->BlankCheck(ctx->realCtx, offset, size);
+
+ return WH_ERROR_OK;
+}
+
+uint32_t whFlashFaultInject_PartitionSize(void* context)
+{
+ whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context;
+
+ if ((ctx == NULL) || (ctx->realCb == NULL))
+ return WH_ERROR_BADARGS;
+
+ if (ctx->realCb->PartitionSize != NULL)
+ return ctx->realCb->PartitionSize(ctx->realCtx);
+
+ return WH_ERROR_OK;
+}
+
+int whFlashFaultInject_WriteLock(void* context, uint32_t offset, uint32_t size)
+{
+ whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context;
+
+ if ((ctx == NULL) || (ctx->realCb == NULL))
+ return WH_ERROR_BADARGS;
+
+ if (ctx->realCb->WriteLock != NULL)
+ return ctx->realCb->WriteLock(ctx->realCtx, offset, size);
+
+ return WH_ERROR_OK;
+}
+
+int whFlashFaultInject_WriteUnlock(void* context, uint32_t offset,
+ uint32_t size)
+{
+ whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context;
+
+ if ((ctx == NULL) || (ctx->realCb == NULL))
+ return WH_ERROR_BADARGS;
+
+ if (ctx->realCb->WriteUnlock != NULL)
+ return ctx->realCb->WriteUnlock(ctx->realCtx, offset, size);
+
+ return WH_ERROR_OK;
+}
diff --git a/test-refactor/posix/wh_test_flash_fault_inject.h b/test-refactor/posix/wh_test_flash_fault_inject.h
new file mode 100644
index 000000000..afbdf22b5
--- /dev/null
+++ b/test-refactor/posix/wh_test_flash_fault_inject.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of wolfHSM.
+ *
+ * wolfHSM is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfHSM is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with wolfHSM. If not, see .
+ */
+/*
+ * test-refactor/wh_test_flash_fault_inject.h
+ *
+ * Flash fault-injection wrapper. Wraps a real flash callback and
+ * forces a failure on the Nth Program call, used to drive the NVM
+ * recovery test on a host-sim flash backend.
+ */
+#ifndef WH_FLASH_FAULTINJECT_H_
+#define WH_FLASH_FAULTINJECT_H_
+
+/* Pick up compile-time configuration */
+#include "wolfhsm/wh_settings.h"
+
+#include
+
+#include "wolfhsm/wh_flash.h"
+
+typedef struct {
+ const whFlashCb* realCb;
+ void* realCtx;
+ int failAfterPrograms;
+} whFlashFaultInjectCtx;
+
+typedef struct {
+ const whFlashCb* realCb;
+ void* realCtx;
+ void* realCfg;
+} whFlashFaultInjectCfg;
+
+int whFlashFaultInject_Init(void* context, const void* config);
+int whFlashFaultInject_Cleanup(void* context);
+int whFlashFaultInject_Program(void* context, uint32_t offset, uint32_t size,
+ const uint8_t* data);
+int whFlashFaultInject_Read(void* context, uint32_t offset, uint32_t size,
+ uint8_t* data);
+int whFlashFaultInject_Erase(void* context, uint32_t offset, uint32_t size);
+int whFlashFaultInject_Verify(void* context, uint32_t offset, uint32_t size,
+ const uint8_t* data);
+int whFlashFaultInject_BlankCheck(void* context, uint32_t offset,
+ uint32_t size);
+uint32_t whFlashFaultInject_PartitionSize(void* context);
+int whFlashFaultInject_WriteLock(void* context, uint32_t offset, uint32_t size);
+int whFlashFaultInject_WriteUnlock(void* context, uint32_t offset,
+ uint32_t size);
+
+/* clang-format off */
+#define WH_FLASH_FAULTINJECT_CB \
+ { \
+ .Init = whFlashFaultInject_Init, \
+ .Cleanup = whFlashFaultInject_Cleanup, \
+ .PartitionSize = whFlashFaultInject_PartitionSize, \
+ .WriteLock = whFlashFaultInject_WriteLock, \
+ .WriteUnlock = whFlashFaultInject_WriteUnlock, \
+ .Read = whFlashFaultInject_Read, \
+ .Program = whFlashFaultInject_Program, \
+ .Erase = whFlashFaultInject_Erase, \
+ .Verify = whFlashFaultInject_Verify, \
+ .BlankCheck = whFlashFaultInject_BlankCheck, \
+ }
+/* clang-format on */
+
+#endif /* !WH_FLASH_FAULTINJECT_H_ */
diff --git a/test-refactor/posix/wh_test_nvm_flash.c b/test-refactor/posix/wh_test_nvm_flash.c
index 957f43e32..5415dad12 100644
--- a/test-refactor/posix/wh_test_nvm_flash.c
+++ b/test-refactor/posix/wh_test_nvm_flash.c
@@ -38,6 +38,7 @@
#include "wh_test_common.h"
#include "wh_test_list.h"
+#include "wh_test_flash_fault_inject.h"
#define NVM_FLASH_SIZE (1024 * 1024)
#define NVM_FLASH_SECTOR_SZ (4096)
@@ -55,9 +56,9 @@ typedef struct {
whFlashRamsimCfg flashCfg;
whFlashCb flashCb;
- whNvmFlashContext nvmFlashCtx;
- whNvmFlashConfig nvmFlashCfg;
- whNvmCb nvmCb;
+ /* NVM backend selected per-test by whTest_NvmCfgBackend */
+ whTestNvmBackendUnion nvmSetup;
+ whNvmConfig nvmCfg;
} whTestNvmFlashCtx;
static whTestNvmFlashCtx _ctx;
@@ -71,7 +72,6 @@ static void _setup(void)
{
whTestNvmFlashCtx* c = &_ctx;
const whFlashCb initFlashCb[1] = {WH_FLASH_RAMSIM_CB};
- whNvmCb initNvmCb[1] = {WH_NVM_FLASH_CB};
memset(c, 0, sizeof(*c));
@@ -81,12 +81,19 @@ static void _setup(void)
c->flashCfg.pageSize = NVM_FLASH_PAGE_SZ;
c->flashCfg.erasedByte = 0;
c->flashCfg.memory = c->memory;
+}
- c->nvmFlashCfg.cb = &c->flashCb;
- c->nvmFlashCfg.context = &c->flashCtx;
- c->nvmFlashCfg.config = &c->flashCfg;
- c->nvmCb = initNvmCb[0];
+/*
+ * Wire the requested NVM backend over the ramsim flash configured
+ * by _setup, leaving _ctx.nvmCfg ready to Init. Delegates to the
+ * shared backend selector so flash and flash-log tests stay in
+ * sync with the rest of the suite.
+ */
+static int _selectNvm(whTestNvmBackendType type)
+{
+ return whTest_NvmCfgBackend(type, &_ctx.nvmSetup, &_ctx.nvmCfg,
+ &_ctx.flashCfg, &_ctx.flashCtx, &_ctx.flashCb);
}
@@ -221,16 +228,14 @@ static int _addAndCheck(const whNvmCb* cb, void* context,
/*
- * Add objects, overwrite, reclaim, destroy, verify
- * data integrity throughout.
+ * Backend-agnostic object lifecycle: init the NVM backend from
+ * cfg, add three objects, overwrite, reclaim, destroy, and verify
+ * data integrity throughout, then clean up. Shared by the plain
+ * flash and flash-log backend tests.
*/
-int whTest_NvmAddOverwriteDestroy(void* ctx)
+static int _addOverwriteDestroy(const whNvmCb* cb, void* context,
+ const void* cfg)
{
- whTestNvmFlashCtx* c = &_ctx;
- const whNvmCb* cb = &c->nvmCb;
-
- (void)ctx;
- _setup();
uint8_t data1[] = "Data1";
uint8_t data2[] = "Data2";
uint8_t data3[] = "Data3";
@@ -246,57 +251,206 @@ int whTest_NvmAddOverwriteDestroy(void* ctx)
uint8_t readBuf[256];
size_t i;
- WH_TEST_RETURN_ON_FAIL(
- cb->Init(&c->nvmFlashCtx, &c->nvmFlashCfg));
+ WH_TEST_RETURN_ON_FAIL(cb->Init(context, cfg));
/* Add 3 objects */
WH_TEST_RETURN_ON_FAIL(
- _addAndCheck(cb, &c->nvmFlashCtx,
- &meta1, sizeof(data1), data1));
+ _addAndCheck(cb, context, &meta1, sizeof(data1), data1));
WH_TEST_RETURN_ON_FAIL(
- _addAndCheck(cb, &c->nvmFlashCtx,
- &meta2, sizeof(data2), data2));
+ _addAndCheck(cb, context, &meta2, sizeof(data2), data2));
WH_TEST_RETURN_ON_FAIL(
- _addAndCheck(cb, &c->nvmFlashCtx,
- &meta3, sizeof(data3), data3));
+ _addAndCheck(cb, context, &meta3, sizeof(data3), data3));
/* Overwrite objects */
WH_TEST_RETURN_ON_FAIL(
- _addAndCheck(cb, &c->nvmFlashCtx,
- &meta1, sizeof(update1), update1));
+ _addAndCheck(cb, context, &meta1, sizeof(update1), update1));
WH_TEST_RETURN_ON_FAIL(
- _addAndCheck(cb, &c->nvmFlashCtx,
- &meta2, sizeof(update2), update2));
+ _addAndCheck(cb, context, &meta2, sizeof(update2), update2));
/* Reclaim space */
- WH_TEST_RETURN_ON_FAIL(
- cb->DestroyObjects(&c->nvmFlashCtx, 0, NULL));
+ WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 0, NULL));
/* Verify all objects survived reclaim */
for (i = 0; i < sizeof(ids) / sizeof(ids[0]); i++) {
memset(&readMeta, 0, sizeof(readMeta));
- WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(
- &c->nvmFlashCtx, ids[i], &readMeta));
- WH_TEST_RETURN_ON_FAIL(cb->Read(
- &c->nvmFlashCtx, ids[i], 0,
- readMeta.len, readBuf));
+ WH_TEST_RETURN_ON_FAIL(
+ cb->GetMetadata(context, ids[i], &readMeta));
+ WH_TEST_RETURN_ON_FAIL(
+ cb->Read(context, ids[i], 0, readMeta.len, readBuf));
}
/* Destroy first object, verify it's gone */
- WH_TEST_RETURN_ON_FAIL(
- cb->DestroyObjects(&c->nvmFlashCtx, 1, ids));
+ WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 1, ids));
WH_TEST_ASSERT_RETURN(
- WH_ERROR_NOTFOUND == cb->Read(
- &c->nvmFlashCtx, ids[0], 0,
+ WH_ERROR_NOTFOUND == cb->Read(context, ids[0], 0,
sizeof(readBuf), readBuf));
/* Destroy remaining */
- WH_TEST_RETURN_ON_FAIL(
- cb->DestroyObjects(&c->nvmFlashCtx,
- sizeof(ids) / sizeof(ids[0]), ids));
+ WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context,
+ sizeof(ids) / sizeof(ids[0]), ids));
- WH_TEST_RETURN_ON_FAIL(
- cb->Cleanup(&c->nvmFlashCtx));
+ WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context));
+
+ return 0;
+}
+
+
+/*
+ * Object lifecycle against the plain NVM flash backend.
+ */
+int whTest_NvmAddOverwriteDestroy(void* ctx)
+{
+ (void)ctx;
+ _setup();
+ WH_TEST_RETURN_ON_FAIL(_selectNvm(WH_NVM_TEST_BACKEND_FLASH));
+ return _addOverwriteDestroy(_ctx.nvmCfg.cb, _ctx.nvmCfg.context,
+ _ctx.nvmCfg.config);
+}
+
+
+/*
+ * Same lifecycle against the NVM flash-log backend, which layers a
+ * journaled log over the same ramsim flash. Skipped when the log
+ * backend isn't built.
+ */
+int whTest_NvmFlashLog(void* ctx)
+{
+ (void)ctx;
+#if defined(WOLFHSM_CFG_SERVER_NVM_FLASH_LOG)
+ _setup();
+ WH_TEST_RETURN_ON_FAIL(_selectNvm(WH_NVM_TEST_BACKEND_FLASH_LOG));
+ return _addOverwriteDestroy(_ctx.nvmCfg.cb, _ctx.nvmCfg.context,
+ _ctx.nvmCfg.config);
+#else
+ return WH_TEST_SKIPPED;
+#endif
+}
+
+
+/* ---- NVM recovery ---- */
+
+/*
+ * Two flash images for the recovery test: one live backing store
+ * and one snapshot replayed as init data to model a reboot over a
+ * dirty flash. File-static to keep 2 MB off the stack.
+ */
+static uint8_t _recoveryMemory[NVM_FLASH_SIZE];
+static uint8_t _recoveryBackup[NVM_FLASH_SIZE];
+
+
+/*
+ * Simulate a failure (eg power loss) during AddObject(), then reinit
+ * and confirm the half-written object is not found.
+ */
+static int _simulateFailureAndRecover(
+ int failAfter, int* dataSize, uint32_t* bytesAvalBefore,
+ whNvmId* objsAvailBefore, uint32_t* bytesReclBefore, whNvmId* objsReclBefore,
+ uint32_t* bytesAvalAfter, whNvmId* objsAvailAfter, uint32_t* bytesReclAfter,
+ whNvmId* objsReclAfter)
+{
+ unsigned char data[] = "This is test data for recovery test";
+ whNvmMetadata meta = {.id = 42, .label = "RecoveryTest"};
+ const whFlashCb flashCb[1] = {WH_FLASH_RAMSIM_CB};
+ whFlashRamsimCtx flashCtx[1] = {0};
+ whFlashRamsimCfg flashCfg[1] = {{
+ .size = NVM_FLASH_SIZE,
+ .sectorSize = NVM_FLASH_SECTOR_SZ,
+ .pageSize = NVM_FLASH_PAGE_SZ,
+ .erasedByte = (uint8_t)0,
+ .memory = _recoveryMemory,
+ }};
+ const whFlashCb flashFaultInjCb[1] = {WH_FLASH_FAULTINJECT_CB};
+ whFlashFaultInjectCtx faultInjCtx[1] = {0};
+ whFlashFaultInjectCfg faultInjCfg[1] = {{
+ .realCb = flashCb,
+ .realCtx = flashCtx,
+ .realCfg = flashCfg,
+ }};
+ const whNvmCb cb[1] = {WH_NVM_FLASH_CB};
+ whNvmFlashContext context[1] = {0};
+ whNvmFlashConfig cfg = {
+ .cb = flashFaultInjCb,
+ .context = faultInjCtx,
+ .config = faultInjCfg,
+ };
+ whNvmMetadata checkMeta = {0};
+ int ret = 0;
+
+ WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg));
+ WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, bytesAvalBefore,
+ objsAvailBefore, bytesReclBefore,
+ objsReclBefore));
+ faultInjCtx->failAfterPrograms = failAfter;
+ ret = cb->AddObject(context, (whNvmMetadata*)&meta, (whNvmSize)sizeof(data),
+ data);
+ WH_TEST_ASSERT_RETURN(ret == WH_ERROR_ABORTED);
+
+ /* Save the memory state for recovery testing */
+ memcpy(_recoveryBackup, _recoveryMemory, NVM_FLASH_SIZE);
+
+ WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context));
+ /* clean-up the memory */
+ memset(_recoveryMemory, 0, NVM_FLASH_SIZE);
+
+ /* Reinit the NVM stack with the backup data from the failure */
+ flashCfg->initData = _recoveryBackup;
+ WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg));
+ WH_TEST_ASSERT_RETURN(cb->GetMetadata(context, meta.id, &checkMeta) ==
+ WH_ERROR_NOTFOUND);
+
+ /* Return available and reclaimable stats after recovery */
+ WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, bytesAvalAfter,
+ objsAvailAfter, bytesReclAfter,
+ objsReclAfter));
+ WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context));
+ *dataSize = sizeof(data);
+ return 0;
+}
+
+
+/*
+ * Recover from a program failure at two points: writing the object
+ * start (the metadata/start record only) and writing the object
+ * count (after the data is on flash). Each scenario checks the
+ * partial object is reclaimed and the live counts are consistent.
+ */
+int whTest_NvmRecovery(void* ctx)
+{
+ int test_data_len;
+ uint32_t bytesBefore, bytesAfter;
+ whNvmId objsBefore, objsAfter;
+ uint32_t bytesReclBefore, bytesReclAfter;
+ whNvmId objsReclBefore, objsReclAfter;
+
+ (void)ctx;
+
+ WH_TEST_PRINT("--simulate failure when writing object start\n");
+ WH_TEST_RETURN_ON_FAIL(_simulateFailureAndRecover(
+ 2 /* program epoch, metadata and fail */, &test_data_len, &bytesBefore,
+ &objsBefore, &bytesReclBefore, &objsReclBefore, &bytesAfter, &objsAfter,
+ &bytesReclAfter, &objsReclAfter));
+ /* object should be marked as reclaimable */
+ WH_TEST_ASSERT_RETURN(objsReclAfter == objsReclBefore + 1);
+ /* data should not be marked as reclaimable */
+ WH_TEST_ASSERT_RETURN(bytesAfter == bytesBefore);
+ WH_TEST_ASSERT_RETURN(bytesReclAfter == bytesReclBefore);
+ /* available object should be decremented */
+ WH_TEST_ASSERT_RETURN(objsAfter == objsBefore - 1);
+
+ WH_TEST_PRINT("--simulate failure when writing object count\n");
+ WH_TEST_RETURN_ON_FAIL(_simulateFailureAndRecover(
+ 4 /* program epoch, metadata, start, data and fail */, &test_data_len,
+ &bytesBefore, &objsBefore, &bytesReclBefore, &objsReclBefore,
+ &bytesAfter, &objsAfter, &bytesReclAfter, &objsReclAfter));
+ /* object should be marked as reclaimable */
+ WH_TEST_ASSERT_RETURN(objsReclAfter == objsReclBefore + 1);
+ /* data should be marked as reclaimable by test_data_len rounded up to
+ * WHFU_BYTES_PER_UNIT */
+ WH_TEST_ASSERT_RETURN(bytesAfter <= bytesBefore - test_data_len);
+ WH_TEST_ASSERT_RETURN(bytesReclAfter >= bytesReclBefore);
+ WH_TEST_ASSERT_RETURN(bytesReclAfter == bytesBefore - bytesAfter);
+ /* available object should be decremented */
+ WH_TEST_ASSERT_RETURN(objsAfter == objsBefore - 1);
return 0;
}
diff --git a/test-refactor/posix/wh_test_posix_main.c b/test-refactor/posix/wh_test_posix_main.c
index 3d8c6083e..38e10eb1b 100644
--- a/test-refactor/posix/wh_test_posix_main.c
+++ b/test-refactor/posix/wh_test_posix_main.c
@@ -64,13 +64,17 @@
* RAM-based flash simulator, which is a host-sim component;
* the nvm_flash test wires the NVM stack to that simulator
* with a 1 MB buffer that's not realistic on embedded targets.
- * Both run from the POSIX port directly until the NVM test is
- * reworked to take a port-supplied flash fixture (then it can
+ * The recovery test layers a fault-injection wrapper over the
+ * same simulator to abort a write mid-object and check cleanup.
+ * All run from the POSIX port directly until the NVM tests are
+ * reworked to take a port-supplied flash fixture (then they can
* lift back into whTestGroup_Server). */
int whTest_FlashWriteLock(void* ctx);
int whTest_FlashEraseProgramVerify(void* ctx);
int whTest_FlashUnitOps(void* ctx);
int whTest_NvmAddOverwriteDestroy(void* ctx);
+int whTest_NvmFlashLog(void* ctx);
+int whTest_NvmRecovery(void* ctx);
/*
* Port-owned contexts. The thread functions fill these in and
@@ -264,6 +268,16 @@ int main(void)
if (rc != 0 && rc != WH_TEST_SKIPPED && miscRc == 0) {
miscRc = rc;
}
+ rc = whTestGroup_RunOne("whTest_NvmFlashLog",
+ whTest_NvmFlashLog, NULL);
+ if (rc != 0 && rc != WH_TEST_SKIPPED && miscRc == 0) {
+ miscRc = rc;
+ }
+ rc = whTestGroup_RunOne("whTest_NvmRecovery",
+ whTest_NvmRecovery, NULL);
+ if (rc != 0 && rc != WH_TEST_SKIPPED && miscRc == 0) {
+ miscRc = rc;
+ }
}
rc = pthread_create(&sthread, NULL, _serverThread, NULL);