[nand][test] Add unit tests for core nand driver.
Tested: `runtests -t nand-unittest`
Change-Id: Ie9f7f216488bb8d48dc0a27054cba8d9b0dbf698
diff --git a/system/dev/nand/nand/nand.cpp b/system/dev/nand/nand/nand.cpp
index 8c7dd92..c98aa02 100644
--- a/system/dev/nand/nand/nand.cpp
+++ b/system/dev/nand/nand/nand.cpp
@@ -286,6 +286,10 @@
}
void NandDevice::DdkRelease() {
+ delete this;
+}
+
+NandDevice::~NandDevice() {
// Signal the worker thread and wait for it to terminate.
worker_event_.signal(0, kNandShutdown);
thrd_join(worker_thread_, nullptr);
@@ -300,28 +304,20 @@
}
lock_.Release();
}
-
- delete this;
}
// static
zx_status_t NandDevice::Create(void* ctx, zx_device_t* parent) {
zxlogf(ERROR, "NandDevice::Create: Starting...!\n");
- zx::event worker_event;
- zx_status_t status = zx::event::create(0, &worker_event);
- if (status != ZX_OK) {
- zxlogf(ERROR, "nand: failed to create event, retcode = %d\n", status);
- return status;
- }
-
fbl::AllocChecker ac;
- fbl::unique_ptr<NandDevice> dev(new (&ac) NandDevice(parent, std::move(worker_event)));
+ fbl::unique_ptr<NandDevice> dev(new (&ac) NandDevice(parent));
if (!ac.check()) {
zxlogf(ERROR, "nand: no memory to allocate nand device!\n");
return ZX_ERR_NO_MEMORY;
}
+ zx_status_t status;
if ((status = dev->Init()) != ZX_OK) {
return status;
}
@@ -351,6 +347,12 @@
num_nand_pages_ = nand_info_.num_blocks * nand_info_.pages_per_block;
+ status = zx::event::create(0, &worker_event_);
+ if (status != ZX_OK) {
+ zxlogf(ERROR, "nand: failed to create event, retcode = %d\n", status);
+ return status;
+ }
+
int rc = thrd_create_with_name(
&worker_thread_,
[](void* arg) { return static_cast<NandDevice*>(arg)->WorkerThread(); },
@@ -372,18 +374,22 @@
return DdkAdd("nand", 0, props, fbl::count_of(props));
}
+#ifndef TEST
static zx_driver_ops_t nand_driver_ops = []() {
zx_driver_ops_t ops = {};
ops.version = DRIVER_OPS_VERSION;
ops.bind = NandDevice::Create;
return ops;
}();
+#endif
} // namespace nand
+#ifndef TEST
// The formatter does not play nice with these macros.
// clang-format off
ZIRCON_DRIVER_BEGIN(nand, nand::nand_driver_ops, "zircon", "0.1", 1)
BI_MATCH_IF(EQ, BIND_PROTOCOL, ZX_PROTOCOL_RAW_NAND),
ZIRCON_DRIVER_END(nand)
- // clang-format on
+// clang-format on
+#endif
diff --git a/system/dev/nand/nand/nand.h b/system/dev/nand/nand/nand.h
index 35bf811..4d821c2 100644
--- a/system/dev/nand/nand/nand.h
+++ b/system/dev/nand/nand/nand.h
@@ -27,11 +27,13 @@
class NandDevice : public DeviceType,
public ddk::NandProtocol<NandDevice, ddk::base_protocol> {
public:
- explicit NandDevice(zx_device_t* parent, zx::event event)
- : DeviceType(parent), raw_nand_(parent), worker_event_(std::move(event)) {}
+ explicit NandDevice(zx_device_t* parent)
+ : DeviceType(parent), raw_nand_(parent) {}
DISALLOW_COPY_ASSIGN_AND_MOVE(NandDevice);
+ ~NandDevice();
+
static zx_status_t Create(void* ctx, zx_device_t* parent);
zx_status_t Bind();
zx_status_t Init();
diff --git a/system/dev/nand/nand/rules.mk b/system/dev/nand/nand/rules.mk
index 73b564e..8ba2a1c 100644
--- a/system/dev/nand/nand/rules.mk
+++ b/system/dev/nand/nand/rules.mk
@@ -24,9 +24,52 @@
system/ulib/zxcpp \
MODULE_LIBS := \
+ system/ulib/c \
system/ulib/driver \
+ system/ulib/zircon \
+
+MODULE_FIDL_LIBS := \
+ system/fidl/fuchsia-hardware-nand \
+
+MODULE_BANJO_LIBS := \
+ system/banjo/ddk-protocol-nand \
+ system/banjo/ddk-protocol-rawnand \
+
+include make/module.mk
+
+# Unit tests.
+
+MODULE := $(LOCAL_DIR).test
+
+MODULE_TYPE := usertest
+
+MODULE_NAME := nand-unittest
+
+TEST_DIR := $(LOCAL_DIR)/test
+
+MODULE_SRCS := \
+ $(LOCAL_DIR)/nand.cpp \
+ $(TEST_DIR)/main.cpp\
+ $(TEST_DIR)/nand-test.cpp\
+
+MODULE_COMPILEFLAGS := \
+ -I$(LOCAL_DIR) \
+ -DTEST \
+
+MODULE_STATIC_LIBS := \
+ system/dev/lib/fake_ddk \
+ system/ulib/ddk \
+ system/ulib/ddktl \
+ system/ulib/fbl \
+ system/ulib/fzl \
+ system/ulib/sync \
+ system/ulib/zx \
+ system/ulib/zxcpp \
+
+MODULE_LIBS := \
system/ulib/c \
system/ulib/fdio \
+ system/ulib/unittest \
system/ulib/zircon \
MODULE_FIDL_LIBS := \
diff --git a/system/dev/nand/nand/test/main.cpp b/system/dev/nand/nand/test/main.cpp
new file mode 100644
index 0000000..faff464
--- /dev/null
+++ b/system/dev/nand/nand/test/main.cpp
@@ -0,0 +1,9 @@
+// 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 <unittest/unittest.h>
+
+int main(int argc, char** argv) {
+ return unittest_run_all_tests(argc, argv) ? 0 : -1;
+}
diff --git a/system/dev/nand/nand/test/nand-test.cpp b/system/dev/nand/nand/test/nand-test.cpp
new file mode 100644
index 0000000..dca0665
--- /dev/null
+++ b/system/dev/nand/nand/test/nand-test.cpp
@@ -0,0 +1,467 @@
+// 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 "nand.h"
+
+#include <memory>
+#include <utility>
+
+#include <lib/fake_ddk/fake_ddk.h>
+#include <lib/fzl/owned-vmo-mapper.h>
+#include <lib/sync/completion.h>
+#include <unittest/unittest.h>
+
+namespace {
+
+constexpr uint32_t kPageSize = 1024;
+constexpr uint32_t kOobSize = 8;
+constexpr uint32_t kNumPages = 20;
+constexpr uint32_t kNumBlocks = 10;
+constexpr uint32_t kEccBits = 10;
+constexpr uint32_t kNumOobSize = 8;
+
+constexpr uint8_t kMagic = 'd';
+constexpr uint8_t kOobMagic = 'o';
+
+fuchsia_hardware_nand_Info kInfo = {kPageSize, kNumPages, kNumBlocks, kEccBits, kNumOobSize, 0, {}};
+
+enum class OperationType {
+ kRead,
+ kWrite,
+ kErase,
+};
+
+struct LastOperation {
+ OperationType type;
+ uint32_t nandpage;
+};
+
+// Fake for the raw nand protocol.
+class FakeRawNand : public ddk::RawNandProtocol<FakeRawNand> {
+public:
+ FakeRawNand()
+ : proto_({&raw_nand_protocol_ops_, this}) {}
+
+ const raw_nand_protocol_t* proto() const { return &proto_; }
+
+ void set_result(zx_status_t result) { result_ = result; }
+ void set_ecc_bits(uint32_t ecc_bits) { ecc_bits_ = ecc_bits; }
+
+ // Raw nand protocol:
+ zx_status_t RawNandGetNandInfo(fuchsia_hardware_nand_Info* out_info) {
+ *out_info = info_;
+ return result_;
+ }
+
+ zx_status_t RawNandReadPageHwecc(uint32_t nandpage, void* out_data_buffer, size_t data_size,
+ size_t* out_data_actual, void* out_oob_buffer, size_t oob_size,
+ size_t* out_oob_actual, uint32_t* out_ecc_correct) {
+ if (nandpage > info_.pages_per_block * info_.num_blocks) {
+ result_ = ZX_ERR_IO;
+ }
+ static_cast<uint8_t*>(out_data_buffer)[0] = 'd';
+ static_cast<uint8_t*>(out_oob_buffer)[0] = 'o';
+ *out_ecc_correct = ecc_bits_;
+
+ last_op_.type = OperationType::kRead;
+ last_op_.nandpage = nandpage;
+
+ return result_;
+ }
+
+ zx_status_t RawNandWritePageHwecc(const void* data_buffer, size_t data_size,
+ const void* oob_buffer, size_t oob_size, uint32_t nandpage) {
+ if (nandpage > info_.pages_per_block * info_.num_blocks) {
+ result_ = ZX_ERR_IO;
+ }
+
+ uint8_t byte = static_cast<const uint8_t*>(data_buffer)[0];
+ if (byte != 'd') {
+ result_ = ZX_ERR_IO;
+ }
+
+ byte = static_cast<const uint8_t*>(oob_buffer)[0];
+ if (byte != 'o') {
+ result_ = ZX_ERR_IO;
+ }
+
+ last_op_.type = OperationType::kWrite;
+ last_op_.nandpage = nandpage;
+
+ return result_;
+ }
+
+ zx_status_t RawNandEraseBlock(uint32_t nandpage) {
+ last_op_.type = OperationType::kErase;
+ last_op_.nandpage = nandpage;
+ return result_;
+ }
+
+ const LastOperation& last_op() { return last_op_; }
+
+private:
+ raw_nand_protocol_t proto_;
+ fuchsia_hardware_nand_Info info_ = kInfo;
+ zx_status_t result_ = ZX_OK;
+ uint32_t ecc_bits_ = 0;
+
+ LastOperation last_op_ = {};
+};
+
+class NandTester {
+public:
+ NandTester() {
+ fbl::Array<fake_ddk::ProtocolEntry> protocols(new fake_ddk::ProtocolEntry[1], 1);
+ protocols[0] = {ZX_PROTOCOL_RAW_NAND,
+ *reinterpret_cast<const fake_ddk::Protocol*>(raw_nand_.proto())};
+ ddk_.SetProtocols(std::move(protocols));
+ ddk_.SetSize(kPageSize * kNumPages * kNumBlocks);
+ }
+
+ fake_ddk::Bind& ddk() { return ddk_; }
+ FakeRawNand& raw_nand() { return raw_nand_; }
+
+private:
+ fake_ddk::Bind ddk_;
+ FakeRawNand raw_nand_;
+};
+
+bool TrivialLifetimeTest() {
+ BEGIN_TEST;
+ NandTester tester;
+ nand::NandDevice device(fake_ddk::kFakeParent);
+ ASSERT_EQ(ZX_OK, device.Init());
+ END_TEST;
+}
+
+bool DdkLifetimeTest() {
+ BEGIN_TEST;
+
+ NandTester tester;
+ nand::NandDevice* device(new nand::NandDevice(fake_ddk::kFakeParent));
+
+ ASSERT_EQ(ZX_OK, device->Init());
+ ASSERT_EQ(ZX_OK, device->Bind());
+ device->DdkUnbind();
+ EXPECT_TRUE(tester.ddk().Ok());
+
+ // This should delete the object, which means this test should not leak.
+ device->DdkRelease();
+ END_TEST;
+}
+
+bool GetSizeTest() {
+ BEGIN_TEST;
+ NandTester tester;
+ nand::NandDevice device(fake_ddk::kFakeParent);
+ ASSERT_EQ(ZX_OK, device.Init());
+ EXPECT_EQ(kPageSize * kNumPages * kNumBlocks, device.DdkGetSize());
+ END_TEST;
+}
+
+bool QueryTest() {
+ BEGIN_TEST;
+ NandTester tester;
+ nand::NandDevice device(fake_ddk::kFakeParent);
+ ASSERT_EQ(ZX_OK, device.Init());
+
+ fuchsia_hardware_nand_Info info;
+ size_t operation_size;
+ device.NandQuery(&info, &operation_size);
+
+ ASSERT_EQ(0, memcmp(&info, &kInfo, sizeof(info)));
+ ASSERT_GT(operation_size, sizeof(nand_operation_t));
+ END_TEST;
+}
+
+class NandDeviceTest;
+
+// Wrapper for a nand_operation_t.
+class Operation {
+public:
+ explicit Operation(size_t op_size, NandDeviceTest* test)
+ : op_size_(op_size), test_(test) {}
+ ~Operation() {}
+
+ // Accessors for the memory represented by the operation's vmo.
+ size_t buffer_size() const { return buffer_size_; }
+ void* buffer() const { return data_mapper_.start(); }
+
+ size_t oob_buffer_size() const { return buffer_size_; }
+ void* oob_buffer() const { return oob_mapper_.start(); }
+
+ // Creates a vmo and sets the handle on the nand_operation_t.
+ bool SetVmo();
+
+ nand_operation_t* GetOperation();
+
+ void OnCompletion(zx_status_t status) {
+ status_ = status;
+ completed_ = true;
+ }
+
+ bool completed() const { return completed_; }
+ zx_status_t status() const { return status_; }
+ NandDeviceTest* test() const { return test_; }
+
+ DISALLOW_COPY_ASSIGN_AND_MOVE(Operation);
+
+private:
+ zx_handle_t GetDataVmo();
+ zx_handle_t GetOobVmo();
+
+ fzl::OwnedVmoMapper data_mapper_;
+ fzl::OwnedVmoMapper oob_mapper_;
+ size_t op_size_;
+ NandDeviceTest* test_;
+ zx_status_t status_ = ZX_ERR_ACCESS_DENIED;
+ bool completed_ = false;
+ static constexpr size_t buffer_size_ = kPageSize * kNumPages;
+ static constexpr size_t oob_buffer_size_ = kOobSize * kNumPages;
+ std::unique_ptr<char[]> raw_buffer_;
+};
+
+bool Operation::SetVmo() {
+ nand_operation_t* operation = GetOperation();
+ if (!operation) {
+ return false;
+ }
+ operation->rw.data_vmo = GetDataVmo();
+ operation->rw.oob_vmo = GetOobVmo();
+ return operation->rw.data_vmo != ZX_HANDLE_INVALID &&
+ operation->rw.oob_vmo != ZX_HANDLE_INVALID;
+}
+
+nand_operation_t* Operation::GetOperation() {
+ if (!raw_buffer_) {
+ raw_buffer_.reset(new char[op_size_]);
+ memset(raw_buffer_.get(), 0, op_size_);
+ }
+ return reinterpret_cast<nand_operation_t*>(raw_buffer_.get());
+}
+
+zx_handle_t Operation::GetDataVmo() {
+ if (data_mapper_.start()) {
+ return data_mapper_.vmo().get();
+ }
+
+ if (data_mapper_.CreateAndMap(buffer_size_, "") != ZX_OK) {
+ return ZX_HANDLE_INVALID;
+ }
+
+ return data_mapper_.vmo().get();
+}
+
+zx_handle_t Operation::GetOobVmo() {
+ if (oob_mapper_.start()) {
+ return oob_mapper_.vmo().get();
+ }
+
+ if (oob_mapper_.CreateAndMap(oob_buffer_size_, "") != ZX_OK) {
+ return ZX_HANDLE_INVALID;
+ }
+
+ return oob_mapper_.vmo().get();
+}
+
+// Provides control primitives for tests that issue IO requests to the device.
+class NandDeviceTest {
+public:
+ NandDeviceTest();
+ ~NandDeviceTest() {}
+
+ nand::NandDevice* device() { return device_.get(); }
+ FakeRawNand& raw_nand() { return tester_.raw_nand(); }
+
+ size_t op_size() const { return op_size_; }
+
+ static void CompletionCb(void* cookie, zx_status_t status, nand_operation_t* op) {
+ Operation* operation = reinterpret_cast<Operation*>(cookie);
+
+ operation->OnCompletion(status);
+ operation->test()->num_completed_++;
+ sync_completion_signal(&operation->test()->event_);
+ }
+
+ bool Wait() {
+ zx_status_t status = sync_completion_wait(&event_, ZX_SEC(5));
+ sync_completion_reset(&event_);
+ return status == ZX_OK;
+ }
+
+ bool WaitFor(int desired) {
+ while (num_completed_ < desired) {
+ if (!Wait()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ DISALLOW_COPY_ASSIGN_AND_MOVE(NandDeviceTest);
+
+private:
+ sync_completion_t event_;
+ int num_completed_ = 0;
+ NandTester tester_;
+ std::unique_ptr<nand::NandDevice> device_;
+ size_t op_size_;
+};
+
+NandDeviceTest::NandDeviceTest() {
+ device_ = std::make_unique<nand::NandDevice>(fake_ddk::kFakeParent);
+
+ fuchsia_hardware_nand_Info info;
+ device_->NandQuery(&info, &op_size_);
+
+ if (device_->Init() != ZX_OK) {
+ device_.reset();
+ }
+}
+
+// Tests trivial attempts to queue one operation.
+bool QueueOneTest() {
+ BEGIN_TEST;
+ NandDeviceTest test;
+ nand::NandDevice* device = test.device();
+ ASSERT_TRUE(device);
+
+ Operation operation(test.op_size(), &test);
+
+ nand_operation_t* op = operation.GetOperation();
+ ASSERT_TRUE(op);
+
+ op->rw.command = NAND_OP_READ;
+ device->NandQueue(op, &NandDeviceTest::CompletionCb, &operation);
+
+ ASSERT_TRUE(test.Wait());
+ ASSERT_EQ(ZX_ERR_OUT_OF_RANGE, operation.status());
+
+ op->rw.length = 1;
+ device->NandQueue(op, &NandDeviceTest::CompletionCb, &operation);
+ ASSERT_TRUE(test.Wait());
+ ASSERT_EQ(ZX_ERR_BAD_HANDLE, operation.status());
+
+ op->rw.offset_nand = kNumPages * kNumBlocks;
+ device->NandQueue(op, &NandDeviceTest::CompletionCb, &operation);
+ ASSERT_TRUE(test.Wait());
+ ASSERT_EQ(ZX_ERR_OUT_OF_RANGE, operation.status());
+
+ ASSERT_TRUE(operation.SetVmo());
+
+ op->rw.offset_nand = (kNumPages * kNumBlocks) - 1;
+ device->NandQueue(op, &NandDeviceTest::CompletionCb, &operation);
+ ASSERT_TRUE(test.Wait());
+ ASSERT_EQ(ZX_OK, operation.status());
+ END_TEST;
+}
+
+bool ReadWriteTest() {
+ BEGIN_TEST;
+ NandDeviceTest test;
+ nand::NandDevice* device = test.device();
+ FakeRawNand& raw_nand = test.raw_nand();
+ ASSERT_TRUE(device);
+
+ Operation operation(test.op_size(), &test);
+ ASSERT_TRUE(operation.SetVmo());
+
+ nand_operation_t* op = operation.GetOperation();
+ ASSERT_TRUE(op);
+
+ op->rw.command = NAND_OP_READ;
+ op->rw.length = 2;
+ op->rw.offset_nand = 3;
+ ASSERT_TRUE(operation.SetVmo());
+ device->NandQueue(op, &NandDeviceTest::CompletionCb, &operation);
+
+ ASSERT_TRUE(test.Wait());
+ ASSERT_EQ(ZX_OK, operation.status());
+
+ EXPECT_EQ(raw_nand.last_op().type, OperationType::kRead);
+ EXPECT_EQ(raw_nand.last_op().nandpage, 4);
+
+ op->rw.command = NAND_OP_WRITE;
+ op->rw.length = 4;
+ op->rw.offset_nand = 5;
+ memset(operation.buffer(), kMagic, kPageSize * 5);
+ memset(operation.oob_buffer(), kOobMagic, kOobSize * 5);
+ device->NandQueue(op, &NandDeviceTest::CompletionCb, &operation);
+
+ ASSERT_TRUE(test.Wait());
+ ASSERT_EQ(ZX_OK, operation.status());
+
+ EXPECT_EQ(raw_nand.last_op().type, OperationType::kWrite);
+ EXPECT_EQ(raw_nand.last_op().nandpage, 8);
+
+ END_TEST;
+}
+
+bool EraseTest() {
+ BEGIN_TEST;
+ NandDeviceTest test;
+ nand::NandDevice* device = test.device();
+ ASSERT_TRUE(device);
+
+ Operation operation(test.op_size(), &test);
+ nand_operation_t* op = operation.GetOperation();
+ ASSERT_TRUE(op);
+
+ op->erase.command = NAND_OP_ERASE;
+ op->erase.num_blocks = 1;
+ op->erase.first_block = 5;
+ device->NandQueue(op, &NandDeviceTest::CompletionCb, &operation);
+
+ ASSERT_TRUE(test.Wait());
+ ASSERT_EQ(ZX_OK, operation.status());
+
+ EXPECT_EQ(test.raw_nand().last_op().type, OperationType::kErase);
+ EXPECT_EQ(test.raw_nand().last_op().nandpage, 5 * kNumPages);
+
+ END_TEST;
+}
+
+// Tests serialization of multiple operations.
+bool QueueMultipleTest() {
+ BEGIN_TEST;
+ NandDeviceTest test;
+ nand::NandDevice* device = test.device();
+ ASSERT_TRUE(device);
+
+ std::unique_ptr<Operation> operations[10];
+ for (int i = 0; i < 10; i++) {
+ operations[i].reset(new Operation(test.op_size(), &test));
+ Operation& operation = *(operations[i].get());
+ nand_operation_t* op = operation.GetOperation();
+ ASSERT_TRUE(op);
+
+ op->rw.command = NAND_OP_READ;
+ op->rw.length = 1;
+ op->rw.offset_nand = i;
+ ASSERT_TRUE(operation.SetVmo());
+ device->NandQueue(op, &NandDeviceTest::CompletionCb, &operation);
+ }
+
+ ASSERT_TRUE(test.WaitFor(10));
+
+ for (const auto& operation : operations) {
+ ASSERT_EQ(ZX_OK, operation->status());
+ ASSERT_TRUE(operation->completed());
+ }
+
+ END_TEST;
+}
+
+} // namespace
+
+BEGIN_TEST_CASE(NandDeviceTests)
+RUN_TEST_SMALL(TrivialLifetimeTest)
+RUN_TEST_SMALL(DdkLifetimeTest)
+RUN_TEST_SMALL(GetSizeTest)
+RUN_TEST_SMALL(QueryTest)
+RUN_TEST_SMALL(QueueOneTest)
+RUN_TEST_SMALL(ReadWriteTest)
+RUN_TEST_SMALL(EraseTest)
+RUN_TEST_SMALL(QueueMultipleTest)
+END_TEST_CASE(NandDeviceTests)