🚀 SSDの文鎮化をテストで防げ!「L2Pマッピング更新」の完全網羅テスト設計(Unity編)
組み込みエンジニアの皆さん、SSDのファームウェアにおいて「データが壊れる」瞬間とはいつでしょうか?
それはNANDフラッシュの寿命が尽きた時ではありません。「新しいデータをNANDへ書き込んでいる最中に電源が落ち、管理テーブル(L2P)だけが先に更新されていた時」です。
これを防ぐための「Write-then-Update(トランザクションのアトミック性)」が正しく実装されているかを証明するのが、今回紹介する単体テストです。
Unity(C言語向けテストフレームワーク)とモックを駆使し、「NANDの書き込み失敗(Program Fail)」というハードウェアの最悪の挙動を意図的に引き起こして、ファームウェアの堅牢性を100%証明します!
💡 テスト設計のポイント(面接で語れるテストの真髄)
このテスト群では、単にコードの網羅率(カバレッジ)を上げるだけでなく、ストレージ特有の「エッジケース」を狙い撃ちにします。
-
アトミック性の完全証明(異常系)
モック関数HAL_NandProgramPageにエラー(false)を返させます。この「書き込み失敗」の直後、DRAM上のL2Pテーブルが絶対に新しいPBAへ書き換わっておらず、古いPBAを維持していること(ロールバック保護)を検証します。 -
不良ブロック隔離(GBB登録)の検証(異常系)
NANDへの書き込みに失敗した物理ブロックは物理的に壊れている可能性が高いため、そのブロックが直ちにFTL_MarkBlockAsGbbで隔離(不良登録)されるかを確認します。 -
上書き時の「旧PBA無効化」検証(正常系)
初めての書き込み(Unmapped)と、2回目以降の上書き(Overwrite)で分岐をテストします。上書き成功時のみ、古いデータが格納されている旧PBAに対して確実にFTL_InvalidatePbaが呼ばれ、ガベージコレクション(GC)の対象となることを証明します。 -
防御的プログラミングの検証(異常系)
NULLポインタや、仕様上限を超えるLBA(Out of bounds)が入力された場合、メモリ破壊を起こす前に即座にリジェクトされることを検証します。
💻 実装コード
#include "unity.h"
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
/* テスト対象のCファイルを直接インクルード(ホワイトボックステスト用) */
#include "ftl_l2p_update.c"
/* --- モック用制御・状態記録変数 --- */
static bool g_mock_allocate_success = true;
static uint32_t g_mock_next_free_pba = 0U;
static bool g_mock_program_success = true;
static uint32_t g_call_program = 0U;
static uint32_t g_call_invalidate = 0U;
static uint32_t g_last_invalidated_pba = FTL_UNMAPPED_PBA;
static uint32_t g_call_gbb = 0U;
static uint32_t g_last_gbb_pba = FTL_UNMAPPED_PBA;
/* --- 外部HAL/FW関数のダミー実装 (モック) --- */
bool FTL_AllocateFreePba(uint32_t* p_out_pba) {
if (g_mock_allocate_success) {
*p_out_pba = g_mock_next_free_pba;
g_mock_next_free_pba++;
return true;
}
return false;
}
bool HAL_NandProgramPage(uint32_t pba, const uint8_t* p_data, const NAND_OobMetadata_t* p_oob) {
g_call_program++;
return g_mock_program_success;
}
void FTL_InvalidatePba(uint32_t pba) {
g_call_invalidate++;
g_last_invalidated_pba = pba;
}
void FTL_MarkBlockAsGbb(uint32_t pba) {
g_call_gbb++;
g_last_gbb_pba = pba;
}
/* --- テストのセットアップ --- */
void setUp(void) {
g_mock_allocate_success = true;
g_mock_next_free_pba = 1000U; /* テスト用の開始PBA */
g_mock_program_success = true;
g_call_program = 0U;
g_call_invalidate = 0U;
g_last_invalidated_pba = FTL_UNMAPPED_PBA;
g_call_gbb = 0U;
g_last_gbb_pba = FTL_UNMAPPED_PBA;
/* L2Pテーブルを未マッピング状態で初期化 */
for (uint32_t i = 0U; i < FTL_MAX_SUPPORTED_LBA; i++) {
s_l2p_table[i] = FTL_UNMAPPED_PBA;
}
}
void tearDown(void) {}
/* =========================================================
* FTL_WriteAndMap のテストケース
* ========================================================= */
/* 1. 異常系: 引数エラー(NULLポインタと境界値) */
void test_FTL_WriteAndMap_InvalidArgs_ShouldReturnFalse(void) {
uint8_t data[4096] = {0};
TEST_ASSERT_FALSE_MESSAGE(FTL_WriteAndMap(0U, NULL), "データポインタがNULLの場合はfalseを返すこと");
TEST_ASSERT_FALSE_MESSAGE(FTL_WriteAndMap(FTL_MAX_SUPPORTED_LBA, data), "上限以上のLBAを指定した場合はfalseを返すこと");
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, g_call_program, "無効な要求ではNANDアクセスを行わないこと");
}
/* 2. 異常系: 空き物理ブロックが枯渇している場合のアロケーション失敗 */
void test_FTL_WriteAndMap_AllocationFails_ShouldReturnFalse(void) {
uint8_t data[4096] = {0};
g_mock_allocate_success = false; /* SSDフル等の理由でPBA取得失敗をシミュレート */
bool result = FTL_WriteAndMap(10U, data);
TEST_ASSERT_FALSE_MESSAGE(result, "PBA確保失敗時はfalseを返すこと");
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, g_call_program, "NAND Programは呼ばれないこと");
}
/* 3. 正常系: 未マッピング領域への新規書き込み */
void test_FTL_WriteAndMap_FirstWrite_ShouldMapSuccessfully(void) {
uint8_t data[4096] = {0xAA};
uint32_t target_lba = 100U;
uint32_t expected_pba = 1000U; /* モックが返す最初のPBA */
bool result = FTL_WriteAndMap(target_lba, data);
TEST_ASSERT_TRUE_MESSAGE(result, "書き込み成功時はtrueを返すこと");
TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_program, "NAND Programが1回呼ばれること");
TEST_ASSERT_EQUAL_UINT32_MESSAGE(expected_pba, s_l2p_table[target_lba], "L2Pテーブルが新しいPBAへ更新されていること");
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, g_call_invalidate, "初回書き込みのため、Invalidate(旧PBAの破棄)は呼ばれないこと");
}
/* 4. 正常系: 既存領域への上書き(Overwrite)とガベージコレクション準備 */
void test_FTL_WriteAndMap_Overwrite_ShouldMapAndInvalidateOldPba(void) {
uint8_t data[4096] = {0};
uint32_t target_lba = 200U;
/* 事前準備: すでにPBA 500 にマッピングされている状態を作る */
s_l2p_table[target_lba] = 500U;
uint32_t expected_new_pba = 1000U;
bool result = FTL_WriteAndMap(target_lba, data);
TEST_ASSERT_TRUE_MESSAGE(result, "上書き成功時はtrueを返すこと");
TEST_ASSERT_EQUAL_UINT32_MESSAGE(expected_new_pba, s_l2p_table[target_lba], "L2Pテーブルが新しいPBAへ更新されていること");
/* ガベージコレクション用連携の検証 */
TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_invalidate, "上書き時は、旧PBAのInvalidate関数が必ず呼ばれること");
TEST_ASSERT_EQUAL_UINT32_MESSAGE(500U, g_last_invalidated_pba, "Invalidate対象が正しく「上書き前の旧PBA」であること");
}
/* 5. 異常系(最重要): NAND書き込み失敗時のアトミック性証明と不良ブロック隔離 */
void test_FTL_WriteAndMap_ProgramFails_ShouldNotUpdateL2pAndMarkGbb(void) {
uint8_t data[4096] = {0};
uint32_t target_lba = 300U;
/* 事前準備: すでにPBA 600 にマッピングされている状態 */
s_l2p_table[target_lba] = 600U;
/* NANDへのハードウェア書き込みが失敗(Program Fail)する最悪のシナリオ */
g_mock_program_success = false;
uint32_t expected_failed_pba = 1000U;
bool result = FTL_WriteAndMap(target_lba, data);
TEST_ASSERT_FALSE_MESSAGE(result, "NAND書き込み失敗時はfalseを返すこと(フェイルセーフ)");
/* 【トランザクション保護の証明】 */
TEST_ASSERT_EQUAL_UINT32_MESSAGE(600U, s_l2p_table[target_lba], "【最重要】書き込み失敗時は絶対にL2Pを更新せず、古い正常なデータ(PBA 600)を死守すること");
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, g_call_invalidate, "エラー時は旧データのInvalidateを行わないこと(旧データを保護)");
/* 【異常ブロックの隔離】 */
TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_gbb, "書き込みに失敗したブロックは後発不良(GBB)登録関数が呼ばれること");
TEST_ASSERT_EQUAL_UINT32_MESSAGE(expected_failed_pba, g_last_gbb_pba, "GBB登録対象が、書き込みに失敗した新しいPBAであること");
}