🚀 GC(ガベージコレクション)のテスト設計
SSDの寿命と性能を決定づける「ガベージコレクション(GC)」。 しかし、裏を返せばGCとは「ユーザーのデータを読み出し、別の場所に書き込み、元のデータを物理的に消去する」という、一歩間違えれば致命的なデータロストを引き起こす最も危険な処理でもあります。
もし、データの引っ越し中にNANDの読み出しエラーが起きたら? 新しい場所への書き込みが失敗したのに、元のブロックを「消去(Erase)」してしまったら? ユーザーのデータは宇宙のチリとなり、製品はリコール(全量回収)の憂き目に遭います。
今回は、「ESTR 組込みソフトウェア開発における品質向上の勧め[テスト編]」に準拠し、前回のGC実装に対し、モック(ダミー関数)を用いてハードウェアエラーを意図的に注入し、「いかなる異常時でもデータを消去しない(フェイルセーフ)」ことを証明するUnity単体テストコードを紹介します!
💡 組み込みテスト設計のポイント
ESTR事例集が提唱する「アルゴリズムの妥当性」と「異常発生時のフェイルソフト(安全側への倒れ込み)」を、GC特有の要件に適用します。
1. 境界値分析とGreedyアルゴリズムの証明(Algorithm Verification)
GC対象(Victim)を選定する際、最も「ゴミが多い(Validページが少ない)」ブロックが正確に選ばれるかをテストします。すべてのブロックが「OPEN(書き込み中)」で対象外のケースや、Validページが「0(全てゴミ)」のブロックを発見した際に探索を打ち切る(最適化)ロジックが正しく働いているかを同値分割で網羅します。
2. データフローとアトミック性の検証(Data Flow Testing)
データの退避が「NANDからのRead → 新ブロックへのWrite → L2Pの更新」という絶対に間違えてはならない順序(アトミックなトランザクション)で行われ、全てが完了して初めて元のブロックがEraseされることを、コールカウントを用いて証明します。
3. フォールトインジェクション(異常注入)とフェイルセーフ
ESTRが最も重視する検証です。データの退避中、NANDフラッシュのハードウェア異常(ReadエラーやWriteエラー)をモックでシミュレートします。 この時、FWがパニックを起こさず、データの移動を即座に中断し、「元のブロックの消去(Erase)を絶対に実行しない(データを死守する)」というフェイルセーフ機構が完璧に動作することを証明します。
💻 テストコード
#include "unity.h" #include <stdint.h> #include <stdbool.h> /* テスト対象のCファイルを直接インクルード(ホワイトボックステスト用) */ #include "ftl_gc.c" /* --- モック用制御・状態記録変数 --- */ static uint32_t g_mock_valid_page_target = 0U; /* このページだけValidとする */ static bool g_mock_read_success = true; static uint32_t g_call_read_nand = 0U; static bool g_mock_write_success = true; static uint32_t g_call_write_nand = 0U; static uint32_t g_call_update_l2p = 0U; static bool g_mock_erase_success = true; static uint32_t g_call_erase_block = 0U; /* --- 外部HAL/FW関数のダミー実装 (モック) --- */ bool FTL_IsPageValid(uint32_t pba_page) { /* テスト用: 指定した1ページだけを有効(Valid)データとして扱う */ return (pba_page == g_mock_valid_page_target); } bool HAL_ReadNandPage(uint32_t pba_page, uint8_t* p_buffer) { g_call_read_nand++; return g_mock_read_success; } uint32_t FTL_WriteToOpenBlock(const uint8_t* p_buffer) { g_call_write_nand++; return g_mock_write_success ? 9999U : FTL_INVALID_BLOCK_ID; /* 成功時は適当なPBAを返す */ } uint32_t FTL_GetLbaFromPba(uint32_t pba_page) { return pba_page * 10U; /* ダミーLBA */ } void FTL_UpdateL2pMapping(uint32_t lba, uint32_t new_pba_page) { g_call_update_l2p++; } bool HAL_EraseBlock(uint32_t pba_block) { g_call_erase_block++; return g_mock_erase_success; } /* --- テストのセットアップ --- */ void setUp(void) { /* モック変数の初期化 */ g_mock_valid_page_target = 0U; g_mock_read_success = true; g_mock_write_success = true; g_mock_erase_success = true; g_call_read_nand = 0U; g_call_write_nand = 0U; g_call_update_l2p = 0U; g_call_erase_block = 0U; /* ブロックメタデータの初期化 */ for (uint32_t i = 0U; i < FTL_TOTAL_BLOCKS; i++) { s_block_metadata[i].state = BLOCK_STATE_FREE; s_block_metadata[i].valid_page_count = 0U; } } void tearDown(void) {} /* ========================================================= * GC_SelectVictimBlock のテストケース * ========================================================= */ /* 1. 正常系(アルゴリズム検証): 最もValidページが少ない(ゴミが多い)ブロックを選定する */ void test_GC_SelectVictimBlock_ShouldReturnBlockWithMinValidPages(void) { /* ブロック10: 100ページ有効 (GC候補) */ s_block_metadata[10].state = BLOCK_STATE_CLOSED; s_block_metadata[10].valid_page_count = 100U; /* ブロック20: 5ページ有効 (最適候補: 引っ越しコストが最も低い) */ s_block_metadata[20].state = BLOCK_STATE_CLOSED; s_block_metadata[20].valid_page_count = 5U; /* ブロック30: 2ページ有効だが書き込み中(OPEN)のため除外されるべき */ s_block_metadata[30].state = BLOCK_STATE_OPEN; s_block_metadata[30].valid_page_count = 2U; uint32_t victim = GC_SelectVictimBlock(); TEST_ASSERT_EQUAL_UINT32_MESSAGE(20U, victim, "CLOSED状態で最もValidページが少ないブロック(20)が選ばれること"); } /* ========================================================= * GC_ExecuteSingleBlock のテストケース (ESTR準拠) * ========================================================= */ /* 2. 正常系(データフロー): データの退避と消去(Erase)のアトミックな完了 */ void test_GC_ExecuteSingleBlock_Success_ShouldMoveDataAndErase(void) { uint32_t target_block = 5U; s_block_metadata[target_block].state = BLOCK_STATE_CLOSED; s_block_metadata[target_block].valid_page_count = 1U; /* ブロック5の先頭ページだけをValidに設定 */ g_mock_valid_page_target = target_block * FTL_PAGES_PER_BLOCK; bool result = GC_ExecuteSingleBlock(); TEST_ASSERT_TRUE_MESSAGE(result, "正常にデータ退避と消去が完了した場合はtrueを返すこと"); /* アトミックなデータ移動(トランザクション)の証明 */ TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_read_nand, "NANDからの読み出しが行われること"); TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_write_nand, "新ブロックへの書き込みが行われること"); TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_update_l2p, "書き込み完了後にL2Pが更新されること"); /* 消去とメタデータ更新の証明 */ TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_erase_block, "退避が完了した古いブロックが消去(Erase)されること"); TEST_ASSERT_EQUAL_MESSAGE(BLOCK_STATE_FREE, s_block_metadata[target_block].state, "ブロックが空き(FREE)状態としてシステムに返却されること"); } /* 3. 異常系(フォールトインジェクション): 退避中のReadエラーに対するフェイルセーフ(データ保護) */ void test_GC_ExecuteSingleBlock_ReadError_ShouldAbortAndNotErase(void) { uint32_t target_block = 5U; s_block_metadata[target_block].state = BLOCK_STATE_CLOSED; s_block_metadata[target_block].valid_page_count = 1U; g_mock_valid_page_target = target_block * FTL_PAGES_PER_BLOCK; /* 【異常注入】NANDからのデータ読み出し(Read)がハードウェアエラーで失敗する */ g_mock_read_success = false; bool result = GC_ExecuteSingleBlock(); TEST_ASSERT_FALSE_MESSAGE(result, "エラー発生時はGC失敗(false)を返すこと"); /* 【ESTR フェイルセーフの証明】 */ TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_read_nand, "読み出しは試行されるが..."); TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, g_call_write_nand, "エラー時は新しい場所へ書き込まないこと(キメラデータ防止)"); TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, g_call_update_l2p, "エラー時はL2Pを更新しないこと(ポインタロスト防止)"); /* 【最重要検証】データロストの防止 */ TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, g_call_erase_block, "【フェイルセーフ】退避に失敗した場合は、絶対に元のブロックを消去(Erase)してはならない"); TEST_ASSERT_EQUAL_MESSAGE(BLOCK_STATE_CLOSED, s_block_metadata[target_block].state, "ブロック状態はCLOSEDのままで維持され、上位層のデータ復旧に委ねること"); }
🎯 組み込みエンジニアとして得られる知見
組み込みエンジニアとしての評価の分かれ目は「最悪の事態(ハードウェアエラー)を想定し、それをテストで証明できるか」にあります。
- フェイルセーフ(データ死守)のテストによる保証
テストケース3(
ReadError_ShouldAbortAndNotErase)は、ストレージ開発の真髄です。NANDフラッシュは劣化によって必ず「読み出しエラー(UNC)」を起こします。もしGC中にこのエラーを引き当てた際、そのまま処理を進めて元のブロックをErase(消去)してしまえば、データは永遠に失われます。 「g_call_erase_blockが0Uであること」をアサーション(TEST_ASSERT)に組み込むことで、「異常時には絶対にデータを消さない(フェイルセーフ)」という設計思想を、コードとして確固たる証拠にしている点がQA・評価部門から信頼を得られます。 - 「Greedyアルゴリズム」のコーナーケース検証
アルゴリズム検証(テストケース1)において、「
BLOCK_STATE_OPEN(書き込み中のブロック)」をテストデータに含め、それが正しく除外されることを確認しています。GCは「書き込みが終わった(CLOSED)ブロック」のみを対象にしなければならず、ここをミスすると「今まさにホストから送られてきたデータ」をGCが横取りして破壊してしまうため、このドメイン知識に基づく境界値テストは高く評価されます。