| // 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) |