blob: 2e5c484e21a0b5f47970305573e3627f04356bd9 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <lib/ftl/ndm-driver.h>
#include <optional>
#include <vector>
#include <zxtest/zxtest.h>
#include "ftl.h"
#include "ndm/ndmp.h"
namespace {
constexpr uint32_t kNumBlocks = 30;
constexpr uint32_t kPagesPerBlock = 16;
constexpr uint32_t kPageSize = 4096;
constexpr uint32_t kOobSize = 16;
constexpr uint32_t kBlockSize = kPageSize * kPagesPerBlock;
constexpr uint32_t kControlPage0 = (kNumBlocks - 1) * kPagesPerBlock;
constexpr uint32_t kControlPage1 = (kNumBlocks - 2) * kPagesPerBlock;
constexpr ftl::VolumeOptions kDefaultOptions = {kNumBlocks, 2, kBlockSize, kPageSize, kOobSize, 0};
class NdmRamDriver final : public ftl::NdmBaseDriver {
public:
NdmRamDriver(const ftl::VolumeOptions options = kDefaultOptions) : options_(options) {}
~NdmRamDriver() final {}
const uint8_t* data(uint32_t page_num) const { return &volume_[page_num * kPageSize]; }
NDM ndm() { return GetNdmForTest(); }
void format_using_v2(bool value) { format_using_v2_ = value; }
// Goes through the normal logic to create a volume with user data info.
const char* CreateVolume(std::optional<ftl::LoggerProxy> logger = std::nullopt) {
return CreateNdmVolumeWithLogger(nullptr, options_, true, logger);
}
// NdmDriver interface:
const char* Init() final;
const char* Attach(const ftl::Volume* ftl_volume) final;
bool Detach() final;
int NandRead(uint32_t start_page, uint32_t page_count, void* page_buffer, void* oob_buffer) final;
int NandWrite(uint32_t start_page, uint32_t page_count, const void* page_buffer,
const void* oob_buffer) final;
int NandErase(uint32_t page_num) final;
int IsBadBlock(uint32_t page_num) final { return ftl::kFalse; }
bool IsEmptyPage(uint32_t page_num, const uint8_t* data, const uint8_t* spare) final {
return IsEmptyPageImpl(data, kPageSize, spare, kOobSize);
}
private:
std::vector<uint8_t> volume_;
ftl::VolumeOptions options_;
bool format_using_v2_ = true;
};
const char* NdmRamDriver::Init() {
size_t volume_size = (kPageSize + kOobSize) * kPagesPerBlock * kNumBlocks;
volume_.resize(volume_size);
memset(volume_.data(), 0xff, volume_size);
return nullptr;
}
const char* NdmRamDriver::Attach(const ftl::Volume* ftl_volume) {
if (!GetNdmForTest()) {
IsNdmDataPresent(options_, format_using_v2_);
}
return GetNdmForTest() ? nullptr : "Failed to add device";
}
bool NdmRamDriver::Detach() { return RemoveNdmVolume(); }
int NdmRamDriver::NandRead(uint32_t start_page, uint32_t page_count, void* page_buffer,
void* oob_buffer) {
uint8_t* data = reinterpret_cast<uint8_t*>(page_buffer);
uint8_t* spare = reinterpret_cast<uint8_t*>(oob_buffer);
if (data) {
memcpy(data, &volume_[start_page * kPageSize], page_count * kPageSize);
}
if (spare) {
uint32_t oob_offset = kPagesPerBlock * kNumBlocks * kPageSize;
memcpy(spare, &volume_[oob_offset + start_page * kOobSize], page_count * kOobSize);
}
return ftl::kNdmOk;
}
int NdmRamDriver::NandWrite(uint32_t start_page, uint32_t page_count, const void* page_buffer,
const void* oob_buffer) {
const uint8_t* data = reinterpret_cast<const uint8_t*>(page_buffer);
const uint8_t* spare = reinterpret_cast<const uint8_t*>(oob_buffer);
ZX_ASSERT(data);
ZX_ASSERT(spare);
uint32_t oob_offset = kPagesPerBlock * kNumBlocks * kPageSize;
memcpy(&volume_[start_page * kPageSize], data, page_count * kPageSize);
memcpy(&volume_[oob_offset + start_page * kOobSize], spare, page_count * kOobSize);
return ftl::kNdmOk;
}
int NdmRamDriver::NandErase(uint32_t page_num) {
ZX_ASSERT(page_num % kPagesPerBlock == 0);
uint32_t oob_offset = kPagesPerBlock * kNumBlocks * kPageSize;
memset(&volume_[page_num * kPageSize], 0xFF, kBlockSize);
memset(&volume_[oob_offset + page_num * kOobSize], 0xFF, kPagesPerBlock * kOobSize);
return ftl::kNdmOk;
}
class NdmTest : public zxtest::Test {
public:
void SetUp() override {
ASSERT_TRUE(ftl::InitModules());
ASSERT_NULL(ndm_driver_.Init());
ASSERT_NULL(ndm_driver_.Attach(nullptr));
}
protected:
NdmRamDriver ndm_driver_;
};
struct HeaderV1 {
uint16_t current_location;
uint16_t last_location;
int32_t sequence_num;
uint32_t crc;
int32_t num_blocks;
int32_t block_size;
int32_t control_block0;
int32_t control_block1;
int32_t free_virt_block;
int32_t free_control_block;
int32_t transfer_to_block;
};
struct HeaderV2 {
uint16_t major_version;
uint16_t minor_version;
HeaderV1 v1;
};
class NdmTestOldFormat : public NdmTest {
public:
void SetUp() override {
ASSERT_TRUE(ftl::InitModules());
ASSERT_NULL(ndm_driver_.Init());
ndm_driver_.format_using_v2(false);
ASSERT_NULL(ndm_driver_.Attach(nullptr));
}
};
TEST_F(NdmTestOldFormat, WritesVersion1) {
auto header = reinterpret_cast<const HeaderV1*>(ndm_driver_.data(kControlPage0));
EXPECT_EQ(1, header->current_location);
EXPECT_EQ(1, header->last_location);
EXPECT_EQ(0, header->sequence_num);
EXPECT_EQ(kNumBlocks, header->num_blocks);
EXPECT_EQ(kPageSize * kPagesPerBlock, header->block_size);
EXPECT_EQ(kNumBlocks - 1, header->control_block0);
EXPECT_EQ(kNumBlocks - 2, header->control_block1);
EXPECT_EQ(kNumBlocks - 4, header->free_virt_block);
EXPECT_EQ(kNumBlocks - 3, header->free_control_block);
EXPECT_EQ(-1, header->transfer_to_block);
}
TEST_F(NdmTest, WritesVersion2) {
auto header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0));
EXPECT_EQ(2, header->major_version);
EXPECT_EQ(0, header->minor_version);
EXPECT_EQ(1, header->v1.current_location);
EXPECT_EQ(1, header->v1.last_location);
EXPECT_EQ(0, header->v1.sequence_num);
EXPECT_EQ(kNumBlocks, header->v1.num_blocks);
EXPECT_EQ(kPageSize * kPagesPerBlock, header->v1.block_size);
EXPECT_EQ(kNumBlocks - 1, header->v1.control_block0);
EXPECT_EQ(kNumBlocks - 2, header->v1.control_block1);
EXPECT_EQ(kNumBlocks - 4, header->v1.free_virt_block);
EXPECT_EQ(kNumBlocks - 3, header->v1.free_control_block);
EXPECT_EQ(-1, header->v1.transfer_to_block);
}
TEST_F(NdmTest, OnlyOneControlBlock) {
auto header = reinterpret_cast<const HeaderV1*>(ndm_driver_.data(kControlPage0 + 1));
EXPECT_EQ(0xffff, header->current_location);
header = reinterpret_cast<const HeaderV1*>(ndm_driver_.data(kControlPage1));
EXPECT_EQ(0xffff, header->current_location);
}
TEST_F(NdmTestOldFormat, NoVersion2) {
NDMPartition partition = {};
partition.num_blocks = ndmGetNumVBlocks(ndm_driver_.ndm());
ASSERT_EQ(0, ndmWritePartition(ndm_driver_.ndm(), &partition, 0, "foo"));
EXPECT_NOT_NULL(ndmGetPartition(ndm_driver_.ndm(), 0));
EXPECT_NULL(ndmGetPartitionInfo(ndm_driver_.ndm()));
}
TEST_F(NdmTest, UsesVersion2) {
NDMPartitionInfo partition = {};
uint32_t partition_size = ndmGetNumVBlocks(ndm_driver_.ndm());
partition.basic_data.num_blocks = partition_size;
strcpy(partition.basic_data.name, "foo");
ASSERT_EQ(0, ndmWritePartitionInfo(ndm_driver_.ndm(), &partition));
EXPECT_NOT_NULL(ndmGetPartition(ndm_driver_.ndm(), 0));
const NDMPartitionInfo* info = ndmGetPartitionInfo(ndm_driver_.ndm());
ASSERT_NOT_NULL(info);
EXPECT_EQ(0, info->basic_data.first_block);
EXPECT_EQ(partition_size, info->basic_data.num_blocks);
EXPECT_EQ(0, info->user_data.data_size);
EXPECT_STR_EQ("foo", info->basic_data.name);
}
TEST_F(NdmTest, SavesVersion2) {
NDMPartitionInfo partition = {};
partition.basic_data.num_blocks = ndmGetNumVBlocks(ndm_driver_.ndm());
ASSERT_EQ(0, ndmWritePartitionInfo(ndm_driver_.ndm(), &partition));
ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
auto header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 1));
EXPECT_EQ(2, header->major_version);
EXPECT_EQ(0, header->minor_version);
EXPECT_EQ(1, header->v1.current_location);
EXPECT_EQ(1, header->v1.last_location);
EXPECT_EQ(1, header->v1.sequence_num);
EXPECT_EQ(kNumBlocks, header->v1.num_blocks);
EXPECT_EQ(kPageSize * kPagesPerBlock, header->v1.block_size);
EXPECT_EQ(kNumBlocks - 1, header->v1.control_block0);
EXPECT_EQ(kNumBlocks - 2, header->v1.control_block1);
EXPECT_EQ(kNumBlocks - 4, header->v1.free_virt_block);
EXPECT_EQ(kNumBlocks - 3, header->v1.free_control_block);
EXPECT_EQ(-1, header->v1.transfer_to_block);
}
TEST_F(NdmTest, OnlyOneV2ControlBlock) {
NDMPartitionInfo partition = {};
partition.basic_data.num_blocks = ndmGetNumVBlocks(ndm_driver_.ndm());
ASSERT_EQ(0, ndmWritePartitionInfo(ndm_driver_.ndm(), &partition));
ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
auto header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 2));
EXPECT_EQ(0xffff, header->major_version);
header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage1));
EXPECT_EQ(0xffff, header->major_version);
}
TEST_F(NdmTest, SavesUpdatedPartitionData) {
NDMPartitionInfo partition = {};
partition.basic_data.num_blocks = ndmGetNumVBlocks(ndm_driver_.ndm());
ASSERT_EQ(0, ndmWritePartitionInfo(ndm_driver_.ndm(), &partition));
// Write three new control blocks.
ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
auto header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 1));
EXPECT_EQ(2, header->major_version);
EXPECT_EQ(1, header->v1.sequence_num);
header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 2));
EXPECT_EQ(2, header->major_version);
EXPECT_EQ(2, header->v1.sequence_num);
header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 3));
EXPECT_EQ(2, header->major_version);
EXPECT_EQ(3, header->v1.sequence_num);
header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 4));
EXPECT_EQ(0xffff, header->major_version);
header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage1));
EXPECT_EQ(0xffff, header->major_version);
}
union PartitionInfo {
NDMPartitionInfo ndm;
struct {
NDMPartition basic_data;
uint32_t data_size;
uint32_t data;
} exploded;
};
static_assert(sizeof(NDMPartition) + sizeof(uint32_t) == sizeof(NDMPartitionInfo));
static_assert(sizeof(NDMPartitionInfo) + sizeof(uint32_t) == sizeof(PartitionInfo));
// Tests that the user portion of the partition info can grow.
TEST_F(NdmTest, UpdatesUserData) {
NDMPartitionInfo partition = {};
partition.basic_data.num_blocks = ndmGetNumVBlocks(ndm_driver_.ndm());
ASSERT_EQ(0, ndmWritePartitionInfo(ndm_driver_.ndm(), &partition));
ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
// Reinitialize NDM.
EXPECT_TRUE(ndm_driver_.Detach());
ASSERT_NULL(ndm_driver_.Attach(nullptr));
// Redefine the partition.
PartitionInfo new_info = {};
new_info.exploded.basic_data = partition.basic_data;
new_info.exploded.data_size = sizeof(new_info.exploded.data);
new_info.exploded.data = 42;
ASSERT_EQ(0, ndmWritePartitionInfo(ndm_driver_.ndm(), &new_info.ndm));
ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
// Read the latest version from disk.
EXPECT_TRUE(ndm_driver_.Detach());
ASSERT_NULL(ndm_driver_.Attach(nullptr));
const NDMPartitionInfo* info = ndmGetPartitionInfo(ndm_driver_.ndm());
ASSERT_NOT_NULL(info);
ASSERT_EQ(sizeof(new_info.exploded.data), info->user_data.data_size);
auto actual_info = reinterpret_cast<const PartitionInfo*>(info);
EXPECT_EQ(42, actual_info->exploded.data);
// Verify the expected disk layout.
auto header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 1));
EXPECT_EQ(2, header->major_version);
EXPECT_EQ(1, header->v1.sequence_num);
header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage1));
EXPECT_EQ(2, header->major_version);
EXPECT_EQ(2, header->v1.sequence_num);
header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 2));
EXPECT_EQ(0xffff, header->major_version);
header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage1 + 1));
EXPECT_EQ(0xffff, header->major_version);
}
TEST_F(NdmTest, BaseDriverSavesConfig) {
ASSERT_NULL(ndm_driver_.CreateVolume());
const NDMPartitionInfo* info = ndmGetPartitionInfo(ndm_driver_.ndm());
ASSERT_NOT_NULL(info);
ASSERT_GE(info->user_data.data_size, 96); // Size of the first version of the data.
const ftl::VolumeOptions* options = ndm_driver_.GetSavedOptions();
ASSERT_NOT_NULL(options);
ASSERT_BYTES_EQ(&kDefaultOptions, options, sizeof(*options));
}
class NdmReadOnlyTest : public zxtest::Test {
public:
void SetUp() override {
ASSERT_TRUE(ftl::InitModules());
ftl::VolumeOptions options = kDefaultOptions;
options.flags |= ftl::kReadOnlyInit;
ndm_driver_.reset(new NdmRamDriver(options));
ASSERT_NULL(ndm_driver_->Init());
buffer_ = std::vector<uint8_t>(kPageSize, 0xff);
}
protected:
std::unique_ptr<NdmRamDriver> ndm_driver_;
std::vector<uint8_t> buffer_;
};
// An NDM control block version 1, stored on page 29.
constexpr uint32_t kControl29V1[] = {
0x00010001, 0x00000000, 0x4efa26dd, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c, 0x0000001a,
0x0000001b, 0xffffffff, 0x00000000, 0x0000001e, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff};
constexpr uint32_t kControlOob[] = {0x4d444eff, 0x31304154, 0xffffffff, 0x00ffffff};
TEST_F(NdmReadOnlyTest, Version1Only) {
memcpy(buffer_.data(), kControl29V1, sizeof(kControl29V1));
ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
ASSERT_NULL(ndm_driver_->CreateVolume());
}
// An NDM control block version 2.0, stored on page 29.
constexpr uint32_t kControl29V2[] = {
0x00000002, 0x00010001, 0x00000000, 0x061cc64a, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c,
0x0000001a, 0x0000001b, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0x0000001e, 0xffffffff};
TEST_F(NdmReadOnlyTest, Version2Only) {
memcpy(buffer_.data(), kControl29V2, sizeof(kControl29V2));
ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
ASSERT_NULL(ndm_driver_->CreateVolume());
}
// An NDM control block version 2.0, stored on page 28, with partition data.
constexpr uint32_t kControl28V2[] = {
0x00000002, 0x00010001, 0x00000001, 0x41220f07, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c,
0x0000001a, 0x0000001b, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000001, 0x0000001e, 0xffffffff,
0xffffffff, 0x00000000, 0x0000001a, 0x006c7466, 0x00000000, 0x00000000, 0x00000000, 0x00000060,
0x00000001, 0x00000004, 0x00000006, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x0000001e, 0x00000002, 0x00010000, 0x00001000, 0x00000010, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000};
TEST_F(NdmReadOnlyTest, UpgradedVersion2) {
memcpy(buffer_.data(), kControl29V1, sizeof(kControl29V1));
ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
memcpy(buffer_.data(), kControl28V2, sizeof(kControl28V2));
ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage1, 1, buffer_.data(), kControlOob));
ASSERT_NULL(ndm_driver_->CreateVolume());
}
// An NDM control block version 1, stored on page 29, with one factory bad block and
// a second bad block in the process of being relocated.
constexpr uint32_t kControlBlockTransferV1[] = {
0x00010001, 0x00000001, 0xcd0deda6, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c, 0xffffffff,
0xffffffff, 0x0000001b, 0x00000003, 0x0000000d, 0x00000102, 0x00000000, 0x00001e00, 0x00000300,
0x00001b00, 0xffffff00, 0xffffffff, 0x000000ff, 0x00001a00, 0x6c746600, 0x00000000, 0x00000000,
0x00000000, 0xffffff00, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff};
TEST_F(NdmReadOnlyTest, InTransferV1) {
memcpy(buffer_.data(), kControlBlockTransferV1, sizeof(kControlBlockTransferV1));
ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
ASSERT_NOT_NULL(ndm_driver_->CreateVolume());
ASSERT_EQ(NDM_BAD_BLK_RECOV, GetFsErrCode());
}
// An NDM control block version 1, stored on page 29, with one factory bad block and
// one translated bad block.
constexpr uint32_t kControlBlockBadBlocksV1[] = {
0x00010001, 0x00000002, 0x64342dc5, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c, 0xffffffff,
0xffffffff, 0xffffffff, 0x00000001, 0x00000000, 0x0000001e, 0x00000003, 0x0000001b, 0xffffffff,
0xffffffff, 0x00000000, 0x0000001a, 0x006c7466, 0x00000000, 0x00000000, 0x00000000, 0xffffffff};
TEST_F(NdmReadOnlyTest, BadBlocksV1) {
memcpy(buffer_.data(), kControlBlockBadBlocksV1, sizeof(kControlBlockBadBlocksV1));
ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
ASSERT_NULL(ndm_driver_->CreateVolume());
EXPECT_EQ(2, ndm_driver_->ndm()->num_bad_blks);
}
// An NDM control block version 2.0, stored on page 29, with one factory bad block and
// a second bad block in the process of being relocated.
constexpr uint32_t kControlBlockTransferV2[] = {
0x00000002, 0x00010001, 0x00000001, 0xdc1fd63c, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c,
0xffffffff, 0xffffffff, 0x0000001b, 0x00000003, 0x0000000d, 0x00000001, 0x00000000, 0x0000001e,
0x00000003, 0x0000001b, 0xffffffff, 0xffffffff, 0x00000000, 0x0000001a, 0x006c7466, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff};
TEST_F(NdmReadOnlyTest, InTransferV2) {
memcpy(buffer_.data(), kControlBlockTransferV2, sizeof(kControlBlockTransferV2));
ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
ASSERT_NOT_NULL(ndm_driver_->CreateVolume());
ASSERT_EQ(NDM_BAD_BLK_RECOV, GetFsErrCode());
}
// An NDM control block version 2.0, stored on page 29, with one factory bad block and
// one translated bad block.
constexpr uint32_t kControlBlockBadBlocksV2[] = {
0x00000002, 0x00010001, 0x00000002, 0x01148752, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c,
0xffffffff, 0xffffffff, 0xffffffff, 0x00000003, 0x0000000d, 0x00000001, 0x00000000, 0x0000001e,
0x00000003, 0x0000001b, 0xffffffff, 0xffffffff, 0x00000000, 0x0000001a, 0x006c7466, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff};
TEST_F(NdmReadOnlyTest, BadBlocksV2) {
static bool logger_called = false;
logger_called = false;
class LoggerHelper {
public:
static void Log(const char* _, ...) __PRINTFLIKE(1, 2) { logger_called = true; }
};
ftl::LoggerProxy logger;
logger.trace = &LoggerHelper::Log;
logger.debug = &LoggerHelper::Log;
logger.info = &LoggerHelper::Log;
logger.warn = &LoggerHelper::Log;
logger.error = &LoggerHelper::Log;
memcpy(buffer_.data(), kControlBlockBadBlocksV2, sizeof(kControlBlockBadBlocksV2));
ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
ASSERT_NULL(ndm_driver_->CreateVolume(logger));
EXPECT_EQ(2, ndm_driver_->ndm()->num_bad_blks);
EXPECT_TRUE(logger_called);
}
} // namespace