| // 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 <endian.h> |
| #include <lib/fake_ddk/fake_ddk.h> |
| #include <lib/fit/function.h> |
| #include <lib/scsi/scsilib.h> |
| #include <lib/scsi/scsilib_controller.h> |
| #include <sys/types.h> |
| #include <zircon/listnode.h> |
| |
| #include <map> |
| |
| #include <ddk/binding.h> |
| #include <fbl/auto_lock.h> |
| #include <fbl/condition_variable.h> |
| #include <zxtest/zxtest.h> |
| |
| namespace { |
| |
| // Binder captures a scsi::Disk when device_add() is invoked inside the DDK. |
| class Binder : public fake_ddk::Bind { |
| public: |
| zx_status_t DeviceAdd(zx_driver_t* drv, zx_device_t* parent, device_add_args_t* args, |
| zx_device_t** out) { |
| device_ = reinterpret_cast<scsi::Disk*>(args->ctx); |
| return Base::DeviceAdd(drv, parent, args, out); |
| } |
| |
| scsi::Disk* device() const { return device_; } |
| |
| private: |
| using Base = fake_ddk::Bind; |
| scsi::Disk* device_; |
| }; |
| |
| // ScsiController for test; allows us to set expectations and fakes command responses. |
| class ScsiControllerForTest : public scsi::Controller { |
| public: |
| using IOCallbackType = |
| fit::function<zx_status_t(uint8_t, uint16_t, struct iovec, struct iovec, struct iovec)>; |
| |
| ~ScsiControllerForTest() { ASSERT_EQ(times_, 0); } |
| |
| // Init the state required for testing async IOs. |
| zx_status_t AsyncIoInit() { |
| { |
| fbl::AutoLock lock(&lock_); |
| list_initialize(&queued_ios_); |
| worker_thread_exit_ = false; |
| } |
| auto cb = [](void* arg) -> int { |
| return static_cast<ScsiControllerForTest*>(arg)->WorkerThread(); |
| }; |
| if (thrd_create_with_name(&worker_thread_, cb, this, "scsi-test-controller") != thrd_success) { |
| printf("%s: Failed to create worker thread\n", __FILE__); |
| return ZX_ERR_INTERNAL; |
| } |
| return ZX_OK; |
| } |
| |
| // De-Init the state required for testing async IOs. |
| void AsyncIoRelease() { |
| { |
| fbl::AutoLock lock(&lock_); |
| worker_thread_exit_ = true; |
| cv_.Signal(); |
| } |
| thrd_join(worker_thread_, nullptr); |
| list_node_t* node; |
| list_node_t* temp_node; |
| fbl::AutoLock lock(&lock_); |
| list_for_every_safe(&queued_ios_, node, temp_node) { |
| auto* io = containerof(node, struct queued_io, node); |
| list_delete(node); |
| free(io); |
| } |
| } |
| |
| zx_status_t ExecuteCommandAsync(uint8_t target, uint16_t lun, struct iovec cdb, |
| struct iovec data_out, struct iovec data_in, |
| void (*cb)(void*, zx_status_t), void* cookie) override { |
| // In the caller, enqueue the request for the worker thread, |
| // poke the worker thread and return. The worker thread, on |
| // waking up, will do the actual IO and call the callback. |
| auto* io = reinterpret_cast<struct queued_io*>(new queued_io); |
| io->target = target; |
| io->lun = lun; |
| // The cbd is allocated on the stack in the scsilib's BlockImplQueue. |
| // So make a copy of that locally, and point to that instead |
| memcpy(reinterpret_cast<void*>(&io->cdbptr), cdb.iov_base, cdb.iov_len); |
| io->cdb.iov_base = &io->cdbptr; |
| io->cdb.iov_len = cdb.iov_len; |
| io->data_out = data_out; |
| io->data_in = data_in; |
| io->cb = cb; |
| io->cookie = cookie; |
| fbl::AutoLock lock(&lock_); |
| list_add_tail(&queued_ios_, &io->node); |
| cv_.Signal(); |
| return ZX_OK; |
| } |
| |
| zx_status_t ExecuteCommandSync(uint8_t target, uint16_t lun, struct iovec cdb, |
| struct iovec data_out, struct iovec data_in) override { |
| EXPECT_TRUE(do_io_); |
| EXPECT_GT(times_, 0); |
| |
| if (!do_io_ || times_ == 0) { |
| return ZX_ERR_INTERNAL; |
| } |
| |
| auto status = do_io_(target, lun, cdb, data_out, data_in); |
| if (--times_ == 0) { |
| decltype(do_io_) empty; |
| do_io_.swap(empty); |
| } |
| return status; |
| } |
| |
| void ExpectCall(IOCallbackType do_io, int times) { |
| do_io_.swap(do_io); |
| times_ = times; |
| } |
| |
| private: |
| IOCallbackType do_io_; |
| int times_ = 0; |
| |
| int WorkerThread() { |
| fbl::AutoLock lock(&lock_); |
| while (true) { |
| if (worker_thread_exit_ == true) |
| return ZX_OK; |
| // While non-empty, remove requests and execute them |
| list_node_t* node; |
| list_node_t* temp_node; |
| list_for_every_safe(&queued_ios_, node, temp_node) { |
| auto* io = containerof(node, struct queued_io, node); |
| list_delete(node); |
| zx_status_t status; |
| status = ExecuteCommandSync(io->target, io->lun, io->cdb, io->data_out, io->data_in); |
| io->cb(io->cookie, status); |
| delete io; |
| } |
| cv_.Wait(&lock_); |
| } |
| return ZX_OK; |
| } |
| |
| struct queued_io { |
| list_node_t node; |
| uint8_t target; |
| uint16_t lun; |
| // Deep copy of the CDB. |
| union { |
| scsi::Read16CDB readcdb; |
| scsi::Write16CDB writecdb; |
| } cdbptr; |
| struct iovec cdb; |
| struct iovec data_out; |
| struct iovec data_in; |
| void (*cb)(void*, zx_status_t); |
| void* cookie; |
| }; |
| |
| // These are the state for testing Async IOs. |
| // The test enqueues Async IOs and pokes the worker thread, which |
| // does the IO, and calls back. |
| fbl::Mutex lock_; |
| fbl::ConditionVariable cv_; |
| thrd_t worker_thread_; |
| bool worker_thread_exit_ __TA_GUARDED(lock_); |
| list_node_t queued_ios_ __TA_GUARDED(lock_); |
| }; |
| |
| class ScsilibDiskTest : public zxtest::Test { |
| public: |
| static constexpr uint32_t kBlockSize = 512; |
| static constexpr uint64_t kFakeBlocks = 128000ul; |
| |
| using DiskBlock = unsigned char[kBlockSize]; |
| |
| void SetupDefaultCreateExpectations() { |
| controller_.ExpectCall( |
| [this](uint8_t target, uint16_t lun, struct iovec cdb, struct iovec data_out, |
| struct iovec data_in) -> auto { |
| switch (default_seq_) { |
| case 0: { |
| scsi::InquiryCDB decoded_cdb = {}; |
| memcpy(&decoded_cdb, cdb.iov_base, cdb.iov_len); |
| EXPECT_EQ(decoded_cdb.opcode, scsi::Opcode::INQUIRY); |
| break; |
| } |
| case 1: { |
| scsi::ReadCapacity16CDB decoded_cdb = {}; |
| memcpy(&decoded_cdb, cdb.iov_base, cdb.iov_len); |
| scsi::ReadCapacity16ParameterData response = {}; |
| response.returned_logical_block_address = htobe64(kFakeBlocks - 1); |
| response.block_length_in_bytes = htobe32(kBlockSize); |
| memcpy(data_in.iov_base, reinterpret_cast<char*>(&response), sizeof(response)); |
| break; |
| } |
| } |
| default_seq_++; |
| |
| return ZX_OK; |
| }, |
| /*times=*/2); |
| } |
| |
| ScsiControllerForTest controller_; |
| int default_seq_ = 0; |
| }; |
| |
| // Test that we can create a disk when the underlying controller successfully executes CDBs. |
| TEST_F(ScsilibDiskTest, TestCreateDestroy) { |
| static constexpr uint8_t kTarget = 5; |
| static constexpr uint16_t kLun = 1; |
| static constexpr int kTransferSize = 32 * 1024; |
| |
| int seq = 0; |
| controller_.ExpectCall( |
| [&seq](uint8_t target, uint16_t lun, struct iovec cdb, struct iovec data_out, |
| struct iovec data_in) -> auto { |
| EXPECT_EQ(target, kTarget); |
| EXPECT_EQ(lun, kLun); |
| |
| if (seq == 0) { |
| // INQUIRY is expected first. |
| EXPECT_EQ(cdb.iov_len, 6); |
| scsi::InquiryCDB decoded_cdb = {}; |
| memcpy(reinterpret_cast<scsi::InquiryCDB*>(&decoded_cdb), cdb.iov_base, cdb.iov_len); |
| EXPECT_EQ(decoded_cdb.opcode, scsi::Opcode::INQUIRY); |
| } else if (seq == 1) { |
| // Then READ CAPACITY (16). |
| EXPECT_EQ(cdb.iov_len, 16); |
| scsi::ReadCapacity16CDB decoded_cdb = {}; |
| memcpy(reinterpret_cast<scsi::ReadCapacity16CDB*>(&decoded_cdb), cdb.iov_base, |
| cdb.iov_len); |
| EXPECT_EQ(decoded_cdb.opcode, scsi::Opcode::READ_CAPACITY_16); |
| EXPECT_EQ(decoded_cdb.service_action, 0x10); |
| |
| scsi::ReadCapacity16ParameterData response = {}; |
| response.returned_logical_block_address = htobe64(kFakeBlocks - 1); |
| response.block_length_in_bytes = htobe32(kBlockSize); |
| memcpy(data_in.iov_base, reinterpret_cast<char*>(&response), sizeof(response)); |
| } |
| seq++; |
| |
| return ZX_OK; |
| }, |
| /*times=*/2); |
| |
| Binder bind; |
| EXPECT_EQ(scsi::Disk::Create(&controller_, fake_ddk::kFakeParent, kTarget, kLun, kTransferSize), |
| ZX_OK); |
| EXPECT_EQ(bind.device()->DdkGetSize(), kFakeBlocks * kBlockSize); |
| |
| bind.device()->DdkAsyncRemove(); |
| EXPECT_OK(bind.WaitUntilRemove()); |
| bind.device()->DdkRelease(); |
| EXPECT_TRUE(bind.Ok()); |
| } |
| |
| // Test creating a disk and executing read commands. |
| TEST_F(ScsilibDiskTest, TestCreateReadDestroy) { |
| static constexpr uint8_t kTarget = 5; |
| static constexpr uint16_t kLun = 1; |
| static constexpr int kTransferSize = 32 * 1024; |
| |
| SetupDefaultCreateExpectations(); |
| |
| Binder bind; |
| EXPECT_EQ(scsi::Disk::Create(&controller_, fake_ddk::kFakeParent, kTarget, kLun, kTransferSize), |
| ZX_OK); |
| |
| // To test SCSI Read functionality, create a fake "disk" backing store in memory and service |
| // reads from it. Fill block 1 with a test pattern of 0x01. |
| std::map<uint64_t, DiskBlock> blocks; |
| DiskBlock& test_block_1 = blocks[1]; |
| memset(test_block_1, 0x01, sizeof(DiskBlock)); |
| |
| controller_.ExpectCall( |
| [&blocks](uint8_t target, uint16_t lun, struct iovec cdb, struct iovec data_out, |
| struct iovec data_in) -> auto { |
| EXPECT_EQ(cdb.iov_len, 16); |
| scsi::Read16CDB decoded_cdb = {}; |
| memcpy(&decoded_cdb, cdb.iov_base, cdb.iov_len); |
| EXPECT_EQ(decoded_cdb.opcode, scsi::Opcode::READ_16); |
| |
| // Support reading one block. |
| EXPECT_EQ(be32toh(decoded_cdb.transfer_length), 1); |
| uint64_t block_to_read = be64toh(decoded_cdb.logical_block_address); |
| const DiskBlock& data_to_return = blocks.at(block_to_read); |
| memcpy(data_in.iov_base, data_to_return, sizeof(DiskBlock)); |
| |
| return ZX_OK; |
| }, |
| /*times=*/1); |
| |
| // Issue a read to block 1 that should work. |
| struct IoWait { |
| fbl::Mutex lock_; |
| fbl::ConditionVariable cv_; |
| }; |
| IoWait iowait_; |
| block_op_t read = {}; |
| block_impl_queue_callback done = [](void* ctx, zx_status_t status, block_op_t* op) { |
| IoWait* iowait_ = reinterpret_cast<struct IoWait*>(ctx); |
| |
| fbl::AutoLock lock(&iowait_->lock_); |
| iowait_->cv_.Signal(); |
| }; |
| read.command = BLOCK_OP_READ; |
| read.rw.length = 1; // Read one block |
| read.rw.offset_dev = 1; // Read logical block 1 |
| read.rw.offset_vmo = 0; |
| EXPECT_OK(zx_vmo_create(PAGE_SIZE, 0, &read.rw.vmo)); |
| controller_.AsyncIoInit(); |
| { |
| fbl::AutoLock lock(&iowait_.lock_); |
| bind.device()->BlockImplQueue(&read, done, &iowait_); // NOTE: Assumes asynchronous controller |
| iowait_.cv_.Wait(&iowait_.lock_); |
| } |
| // Make sure the contents of the VMO we read into match the expected test pattern |
| DiskBlock check_buffer = {}; |
| EXPECT_OK(zx_vmo_read(read.rw.vmo, check_buffer, 0, sizeof(DiskBlock))); |
| for (uint i = 0; i < sizeof(DiskBlock); i++) { |
| EXPECT_EQ(check_buffer[i], 0x01); |
| } |
| controller_.AsyncIoRelease(); |
| bind.device()->DdkAsyncRemove(); |
| EXPECT_OK(bind.WaitUntilRemove()); |
| bind.device()->DdkRelease(); |
| EXPECT_TRUE(bind.Ok()); |
| } |
| |
| } // namespace |