diff --git a/scst/src/dev_handlers/kunit/vdisk/scst_vdisk_fixture.c b/scst/src/dev_handlers/kunit/vdisk/scst_vdisk_fixture.c new file mode 100644 index 000000000..541a48257 --- /dev/null +++ b/scst/src/dev_handlers/kunit/vdisk/scst_vdisk_fixture.c @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "scst_vdisk_fixture.h" + +static ssize_t vdisk_test_iter(struct kiocb *iocb, struct iov_iter *iter, bool write) +{ + struct vdisk_test_file *file = iocb->ki_filp->private_data; + struct vdisk_test_fixture *f = file->fixture; + struct kunit *test = kunit_get_current_test(); + unsigned int n = f->io_count++; + size_t length = iov_iter_count(iter); + ssize_t ret; + + /* Bound retries even if the production short-write loop regresses. */ + if (n >= ARRAY_SIZE(f->io_offset)) { + KUNIT_FAIL(test, "unexpected backend retry"); + return -EIO; + } + f->io_offset[n] = iocb->ki_pos; + f->io_length[n] = length; + f->io_flags[n] = iocb->ki_flags; + f->read_count += !write; + ret = n < f->result_count ? f->results[n] : length; + if (ret == -EIOCBQUEUED) { + /* No work is scheduled: the test owns delivery of this completion. */ + f->pending = iocb; + return ret; + } + if (ret < 0) + return ret; + if (ret > length || ret > sizeof(f->written) - f->written_len) { + KUNIT_FAIL(test, "invalid fake I/O size"); + return -EIO; + } + if (write) { + KUNIT_EXPECT_EQ(test, copy_from_iter(f->written + f->written_len, ret, iter), + (size_t)ret); + f->written_len += ret; + } else { + KUNIT_EXPECT_EQ(test, iov_iter_zero(ret, iter), (size_t)ret); + } + iocb->ki_pos += ret; + return ret; +} + +static ssize_t vdisk_test_write_iter(struct kiocb *iocb, struct iov_iter *iter) +{ + return vdisk_test_iter(iocb, iter, true); +} + +static ssize_t vdisk_test_read_iter(struct kiocb *iocb, struct iov_iter *iter) +{ + return vdisk_test_iter(iocb, iter, false); +} + +static int vdisk_test_record_sync(struct file *file, loff_t start, loff_t end, + enum vdisk_test_sync_op op) +{ + struct vdisk_test_file *backend = file->private_data; + struct vdisk_test_fixture *f = backend->fixture; + unsigned int n = f->flush_count++; + + if (n >= ARRAY_SIZE(f->flushes)) { + KUNIT_FAIL(kunit_get_current_test(), "unexpected extra flush"); + return -EIO; + } + f->flushes[n] = (struct vdisk_test_flush) { + .file = file, .start = start, .end = end, .op = op, + }; + return backend->sync_result; +} + +static int vdisk_test_fsync(struct file *file, loff_t start, loff_t end, int datasync) +{ + /* Both fsync and fdatasync satisfy the persistence requirement. */ + return vdisk_test_record_sync(file, start, end, VDISK_TEST_FSYNC); +} + +static int vdisk_test_writeback(struct address_space *mapping, loff_t start, loff_t end) +{ + struct vdisk_test_file *backend = container_of(mapping, struct vdisk_test_file, mapping); + + return vdisk_test_record_sync(&backend->file, start, end, VDISK_TEST_WRITEBACK); +} + +static const struct file_operations vdisk_test_fops = { + .write_iter = vdisk_test_write_iter, + .read_iter = vdisk_test_read_iter, + .fsync = vdisk_test_fsync, +}; + +static void vdisk_test_file_init(struct vdisk_test_file *file, struct vdisk_test_fixture *f) +{ + file->fixture = f; + file->file.f_op = &vdisk_test_fops; + file->file.f_inode = &file->inode; + file->file.f_mapping = &file->mapping; + file->file.f_mode = FMODE_READ | FMODE_WRITE; + file->file.private_data = file; + file->mapping.host = &file->inode; + /* A synthetic special inode avoids taking a real superblock freeze lock. */ + file->inode.i_mode = S_IFCHR; + file->inode.i_size = 16384; +} + +static int vdisk_test_block_flush(struct block_device *bdev, gfp_t gfp_mask, bool report_error, + struct scst_cmd *cmd, bool async) +{ + struct kunit *test = kunit_get_current_test(); + struct vdisk_test_fixture *f = test->priv; + + KUNIT_EXPECT_PTR_EQ(test, bdev, &f->bdev); + if (f->vdev.dif_fd) { + KUNIT_EXPECT_EQ(test, f->flush_count, 1U); + KUNIT_EXPECT_PTR_EQ(test, f->flushes[0].file, f->vdev.dif_fd); + } + if (f->sysfs_sync) { + KUNIT_EXPECT_FALSE(test, report_error); + KUNIT_EXPECT_FALSE(test, async); + KUNIT_EXPECT_PTR_EQ(test, cmd, NULL); + } + f->block_flush_count++; + return f->block_flush_result; +} + +static int vdisk_test_no_dif(struct scst_cmd *cmd) +{ + return 0; +} + +static void vdisk_test_done(struct scst_cmd *cmd, int state, enum scst_exec_context context) +{ + struct vdisk_test_fixture *f = container_of(cmd, struct vdisk_test_fixture, cmd); + + f->completions++; + f->completion_state = state; + f->completed_before_callback = cmd->completed; +} + +int vdisk_test_init(struct kunit *test) +{ + struct vdisk_test_fixture *f; + unsigned int i, j; + + test->priv = NULL; + f = kunit_kzalloc(test, sizeof(*f), GFP_KERNEL); + if (!f) + return -ENOMEM; + test->priv = f; + f->dev.dh_priv = &f->vdev; + f->dev.block_size = 512; + f->dev.block_shift = 9; + f->dev.dev_dif_fn = vdisk_test_no_dif; + f->vdev.dev = &f->dev; + f->vdev.dev_active = true; + f->vdev.file_size = 16384; + vdisk_test_file_init(&f->data, f); + vdisk_test_file_init(&f->dif, f); + f->vdev.fd = &f->data.file; + f->cmd.dev = &f->dev; + f->cmd.sess = &f->sess; + f->cmd.cmd_gfp_mask = GFP_KERNEL; + f->cmd.data_direction = SCST_DATA_WRITE; + f->cmd.data_len = sizeof(f->written); + f->cmd.bufflen = sizeof(f->written); + f->cmd.resp_data_len = sizeof(f->written); + f->cmd.sense = f->sense; + f->cmd.sense_buflen = sizeof(f->sense); + f->cmd.scst_cmd_done = vdisk_test_done; + f->params.cmd = &f->cmd; + f->params.loff = 8192; + f->params.fua = true; + atomic_set(&f->sess.sess_cmd_count, 1); + f->sess.initiator_name = "kunit"; + f->sess.init_phase = SCST_SESS_IPH_READY; + sg_init_table(f->sg, ARRAY_SIZE(f->sg)); + for (i = 0; i < ARRAY_SIZE(f->sg); i++) { + f->input[i] = kunit_kmalloc(test, 512, GFP_KERNEL); + if (!f->input[i]) + return -ENOMEM; + for (j = 0; j < 512; j++) + f->input[i][j] = (j * 17 + i * 73) & 0xff; + sg_set_buf(&f->sg[i], f->input[i], 512); + } + f->cmd.sg = f->sg; + f->cmd.sg_cnt = ARRAY_SIZE(f->sg); + kunit_activate_static_stub(test, vdisk_blockio_flush, vdisk_test_block_flush); + kunit_activate_static_stub(test, vdisk_writeback_range, vdisk_test_writeback); + return 0; +} + +void vdisk_test_exit(struct kunit *test) +{ + struct vdisk_test_fixture *f = test->priv; + + if (!f) + return; + /* A failed assertion must not leave a synthetic completion outstanding. */ + if (f->pending) + f->pending->ki_complete(f->pending, -EIO); + vdisk_on_free_cmd_params(&f->params); + test->priv = NULL; +} + +void vdisk_test_expect_flush(struct kunit *test, unsigned int n, struct file *file, + loff_t start, loff_t end) +{ + struct vdisk_test_fixture *f = test->priv; + + KUNIT_ASSERT_LT(test, n, f->flush_count); + KUNIT_ASSERT_LT(test, n, (unsigned int)ARRAY_SIZE(f->flushes)); + KUNIT_EXPECT_PTR_EQ(test, f->flushes[n].file, file); + KUNIT_EXPECT_EQ(test, f->flushes[n].start, start); + KUNIT_EXPECT_EQ(test, f->flushes[n].end, end); + KUNIT_EXPECT_EQ_MSG(test, f->flushes[n].op, VDISK_TEST_FSYNC, + "writeback alone does not provide filesystem synchronization"); +} diff --git a/scst/src/dev_handlers/kunit/vdisk/scst_vdisk_fixture.h b/scst/src/dev_handlers/kunit/vdisk/scst_vdisk_fixture.h new file mode 100644 index 000000000..0518f40ee --- /dev/null +++ b/scst/src/dev_handlers/kunit/vdisk/scst_vdisk_fixture.h @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef SCST_VDISK_FIXTURE_H +#define SCST_VDISK_FIXTURE_H + +#include "../../../kunit/scst_kunit.h" +#include "../../scst_vdisk_priv.h" +#include "../../scst_vdisk_test.h" + +struct vdisk_test_fixture; + +struct vdisk_test_file { + struct file file; + struct inode inode; + struct address_space mapping; + struct vdisk_test_fixture *fixture; + int sync_result; +}; + +enum vdisk_test_sync_op { + VDISK_TEST_WRITEBACK, + VDISK_TEST_FSYNC, +}; + +struct vdisk_test_flush { + struct file *file; + loff_t start; + loff_t end; + enum vdisk_test_sync_op op; +}; + +struct vdisk_test_fixture { + struct scst_cmd cmd; + struct scst_device dev; + struct scst_session sess; + struct scst_vdisk_dev vdev; + struct vdisk_cmd_params params; + struct vdisk_test_file data; + struct vdisk_test_file dif; + struct block_device bdev; + struct scatterlist sg[2]; + u8 sense[SCST_SENSE_BUFFERSIZE]; + u8 *input[2]; + u8 written[1024]; + size_t written_len; + ssize_t results[4]; + unsigned int result_count; + unsigned int io_count; + loff_t io_offset[4]; + size_t io_length[4]; + unsigned int io_flags[4]; + unsigned int read_count; + struct kiocb *pending; + unsigned int completions; + int completion_state; + bool completed_before_callback; + struct vdisk_test_flush flushes[2]; + unsigned int flush_count; + unsigned int block_flush_count; + int block_flush_result; + bool sysfs_sync; +}; + +int vdisk_test_init(struct kunit *test); +void vdisk_test_exit(struct kunit *test); +void vdisk_test_expect_flush(struct kunit *test, unsigned int n, struct file *file, + loff_t start, loff_t end); + +#endif /* SCST_VDISK_FIXTURE_H */ diff --git a/scst/src/dev_handlers/kunit/vdisk/suites/scst_vdisk_io_test.c b/scst/src/dev_handlers/kunit/vdisk/suites/scst_vdisk_io_test.c new file mode 100644 index 000000000..cc1d075d3 --- /dev/null +++ b/scst/src/dev_handlers/kunit/vdisk/suites/scst_vdisk_io_test.c @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "../scst_vdisk_fixture.h" + +struct vdisk_write_case { + const char *name; + loff_t start; + ssize_t results[3]; + unsigned int result_count; + unsigned int calls; + size_t written; + bool no_fua; + bool direct; + bool wt; + bool nv_cache; + bool thin; + bool dif_fallback; + int flush_error; + u8 status; +}; + +static const struct vdisk_write_case vdisk_write_cases[] = { + { .name = "fua_at_zero", .calls = 1, .written = 1024 }, + { .name = "fua_at_nonzero_offset", .start = 8192, .calls = 1, .written = 1024 }, + { .name = "short_before_sg_boundary", .start = 8192, .results = { 256 }, + .result_count = 1, .calls = 2, .written = 1024 }, + { .name = "short_at_sg_boundary", .start = 8192, .results = { 512 }, + .result_count = 1, .calls = 2, .written = 1024 }, + { .name = "short_after_sg_boundary", .start = 8192, .results = { 768 }, + .result_count = 1, .calls = 2, .written = 1024 }, + { .name = "two_short_writes", .start = 8192, .results = { 128, 512 }, + .result_count = 2, .calls = 3, .written = 1024 }, + { .name = "write_eagain", .start = 8192, .results = { -EAGAIN }, + .result_count = 1, .calls = 1, .status = SAM_STAT_BUSY }, + { .name = "thin_out_of_space", .start = 8192, .results = { -ENOSPC }, + .result_count = 1, .calls = 1, .thin = true, .status = SAM_STAT_CHECK_CONDITION }, + { .name = "write_eio", .start = 8192, .results = { -EIO }, + .result_count = 1, .calls = 1, .status = SAM_STAT_CHECK_CONDITION }, + { .name = "eio_after_short_write", .start = 8192, .results = { 256, -EIO }, + .result_count = 2, .calls = 2, .written = 256, .status = SAM_STAT_CHECK_CONDITION }, + { .name = "ordinary_write", .start = 8192, .calls = 1, .written = 1024, .no_fua = true }, + { .name = "flush_eio", .start = 8192, .calls = 1, .written = 1024, + .flush_error = -EIO, .status = SAM_STAT_CHECK_CONDITION }, + { .name = "flush_enomem", .start = 8192, .calls = 1, .written = 1024, + .flush_error = -ENOMEM, .status = SAM_STAT_BUSY }, + { .name = "direct_flag_still_flushes", .start = 8192, .calls = 1, .written = 1024, + .direct = true }, + { .name = "write_through", .start = 8192, .calls = 1, .written = 1024, .wt = true }, + { .name = "nonvolatile_cache", .start = 8192, .calls = 1, .written = 1024, + .nv_cache = true }, + { .name = "direct_async_dif_fallback", .start = 8192, .calls = 1, .written = 1024, + .direct = true, .dif_fallback = true }, +}; + +static void vdisk_write_desc(const struct vdisk_write_case *p, char *desc) +{ + snprintf(desc, KUNIT_PARAM_DESC_SIZE, "%s", p->name); +} + +KUNIT_ARRAY_PARAM(vdisk_write, vdisk_write_cases, vdisk_write_desc); + +static void vdisk_write_test(struct kunit *test) +{ + const struct vdisk_write_case *p = test->param_value; + struct vdisk_test_fixture *f = test->priv; + loff_t offset = p->start; + size_t remaining = 1024; + unsigned int i; + + f->params.loff = p->start; + f->params.fua = !p->no_fua; + f->vdev.o_direct_flag = p->direct; + f->vdev.wt_flag = p->wt; + f->vdev.nv_cache = p->nv_cache; + f->vdev.thin_provisioned = p->thin; + f->vdev.async = p->dif_fallback; + f->dev.dev_dif_mode = p->dif_fallback ? SCST_DIF_MODE_TGT : SCST_DIF_MODE_NONE; + f->data.sync_result = p->flush_error; + memcpy(f->results, p->results, sizeof(p->results)); + f->result_count = p->result_count; + + KUNIT_EXPECT_EQ(test, fileio_exec_write(&f->params), CMD_SUCCEEDED); + KUNIT_EXPECT_EQ(test, f->cmd.status, p->status); + KUNIT_EXPECT_EQ(test, f->completions, 0U); + KUNIT_EXPECT_EQ(test, f->io_count, p->calls); + KUNIT_EXPECT_EQ(test, f->written_len, p->written); + for (i = 0; i < p->written; i++) + KUNIT_EXPECT_EQ_MSG(test, f->written[i], f->input[i / 512][i % 512], + "payload byte %u", i); + for (i = 0; i < p->calls; i++) { + KUNIT_EXPECT_EQ(test, f->io_offset[i], offset); + KUNIT_EXPECT_EQ(test, f->io_length[i], remaining); + if (i < p->result_count && p->results[i] > 0) { + offset += p->results[i]; + remaining -= p->results[i]; + } + } + if (p->no_fua || p->wt || p->nv_cache) { + KUNIT_EXPECT_EQ(test, f->flush_count, 0U); + } else if (p->written == sizeof(f->written)) { + /* Failed writes above check status and progress, not FUA durability. */ + KUNIT_EXPECT_EQ(test, f->flush_count, 1U); + vdisk_test_expect_flush(test, 0, &f->data.file, p->start, p->start + 1023); + } + KUNIT_EXPECT_EQ(test, f->block_flush_count, 0U); +} + +struct vdisk_async_case { + const char *name; + bool read; + bool no_fua; + bool direct; + bool wt; + bool nv_cache; + bool dsync; + ssize_t result; + unsigned int calls; + u8 status; +}; + +static const struct vdisk_async_case vdisk_async_cases[] = { + { .name = "buffered_fua", .dsync = true, .calls = 1 }, + { .name = "direct_fua", .direct = true, .dsync = true, .calls = 1 }, + { .name = "ordinary_writeback", .no_fua = true, .calls = 1 }, + { .name = "ordinary_direct_write", .no_fua = true, .direct = true, .calls = 1 }, + { .name = "write_through", .no_fua = true, .wt = true, .dsync = true, .calls = 1 }, + { .name = "fua_and_write_through", .wt = true, .dsync = true, .calls = 1 }, + { .name = "fua_nonvolatile_cache", .nv_cache = true, .calls = 1 }, + { .name = "write_through_nonvolatile_cache", .no_fua = true, .wt = true, + .nv_cache = true, .calls = 1 }, + { .name = "read_ignores_fua", .read = true, .calls = 1 }, + { .name = "direct_read_ignores_write_through", .read = true, .direct = true, + .wt = true, .calls = 1 }, + { .name = "nowait_eagain_keeps_dsync", .direct = true, .dsync = true, + .result = -EAGAIN, .calls = 2 }, + { .name = "nowait_eopnotsupp_keeps_dsync", .direct = true, .dsync = true, + .result = -EOPNOTSUPP, .calls = 2 }, + { .name = "buffered_eagain_is_error", .dsync = true, .result = -EAGAIN, + .calls = 1, .status = SAM_STAT_CHECK_CONDITION }, + { .name = "async_write_error", .dsync = true, .result = -EIO, + .calls = 1, .status = SAM_STAT_CHECK_CONDITION }, + { .name = "async_read_error", .read = true, .result = -EIO, + .calls = 1, .status = SAM_STAT_CHECK_CONDITION }, + { .name = "queued_fua_completion", .dsync = true, .result = -EIOCBQUEUED, .calls = 1 }, +}; + +static void vdisk_async_desc(const struct vdisk_async_case *p, char *desc) +{ + snprintf(desc, KUNIT_PARAM_DESC_SIZE, "%s", p->name); +} + +KUNIT_ARRAY_PARAM(vdisk_async, vdisk_async_cases, vdisk_async_desc); + +static void vdisk_async_test(struct kunit *test) +{ + const struct vdisk_async_case *p = test->param_value; + struct vdisk_test_fixture *f = test->priv; + unsigned int i; + enum compl_status_e result; + + f->vdev.async = true; + f->vdev.o_direct_flag = p->direct; + f->vdev.wt_flag = p->wt; + f->vdev.nv_cache = p->nv_cache; + f->params.fua = !p->no_fua; + f->cmd.data_direction = p->read ? SCST_DATA_READ : SCST_DATA_WRITE; + if (p->result) { + f->results[0] = p->result; + f->result_count = 1; + } + + result = p->read ? fileio_exec_read(&f->params) : fileio_exec_write(&f->params); + KUNIT_EXPECT_EQ(test, result, RUNNING_ASYNC); + KUNIT_EXPECT_EQ(test, f->io_count, p->calls); + KUNIT_EXPECT_EQ(test, f->read_count, p->read ? p->calls : 0U); + for (i = 0; i < p->calls; i++) { + bool dsync = f->io_flags[i] & IOCB_DSYNC; + bool direct = f->io_flags[i] & IOCB_DIRECT; + bool nowait = f->io_flags[i] & IOCB_NOWAIT; + + KUNIT_EXPECT_EQ(test, f->io_offset[i], (loff_t)8192); + KUNIT_EXPECT_EQ(test, f->io_length[i], (size_t)1024); + KUNIT_EXPECT_EQ_MSG(test, dsync, p->dsync, "I/O %u: IOCB_DSYNC", i); + KUNIT_EXPECT_EQ_MSG(test, direct, p->direct, "I/O %u: IOCB_DIRECT", i); + KUNIT_EXPECT_EQ_MSG(test, nowait, p->direct && !i, "I/O %u: IOCB_NOWAIT", i); + } + if (p->result == -EIOCBQUEUED) { + struct kiocb *pending = f->pending; + + KUNIT_EXPECT_EQ(test, f->completions, 0U); + KUNIT_EXPECT_FALSE(test, f->cmd.completed); + KUNIT_ASSERT_NOT_NULL(test, pending); + f->pending = NULL; + pending->ki_complete(pending, 1024); + } + KUNIT_EXPECT_EQ(test, f->completions, 1U); + KUNIT_EXPECT_EQ(test, f->completion_state, SCST_CMD_STATE_DEFAULT); + KUNIT_EXPECT_TRUE(test, f->completed_before_callback); + KUNIT_EXPECT_TRUE(test, f->cmd.completed); + KUNIT_EXPECT_EQ(test, f->cmd.status, p->status); + KUNIT_EXPECT_EQ(test, f->flush_count, 0U); + KUNIT_EXPECT_EQ(test, f->block_flush_count, 0U); + if (!p->status && p->result != -EIOCBQUEUED) { + if (!p->read) + KUNIT_EXPECT_EQ(test, f->written_len, (size_t)1024); + for (i = 0; i < ARRAY_SIZE(f->input); i++) { + if (p->read) + KUNIT_EXPECT_PTR_EQ(test, memchr_inv(f->input[i], 0, 512), NULL); + else + KUNIT_EXPECT_EQ(test, + memcmp(f->written + i * 512, f->input[i], 512), 0); + } + } +} + +static struct kunit_case vdisk_io_cases[] = { + KUNIT_CASE_PARAM(vdisk_write_test, vdisk_write_gen_params), + KUNIT_CASE_PARAM(vdisk_async_test, vdisk_async_gen_params), + {} +}; + +scst_kunit_suite(vdisk_io_suite, "VDISK", "-io", + .init = vdisk_test_init, + .exit = vdisk_test_exit, + .test_cases = vdisk_io_cases, +); diff --git a/scst/src/dev_handlers/kunit/vdisk/suites/scst_vdisk_sync_test.c b/scst/src/dev_handlers/kunit/vdisk/suites/scst_vdisk_sync_test.c new file mode 100644 index 000000000..a24eca388 --- /dev/null +++ b/scst/src/dev_handlers/kunit/vdisk/suites/scst_vdisk_sync_test.c @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "../scst_vdisk_fixture.h" + +struct vdisk_range_case { + const char *name; + loff_t start; + loff_t length; + loff_t end; + int error; + bool no_cmd; + u8 status; +}; + +static const struct vdisk_range_case vdisk_range_cases[] = { + { .name = "empty_at_zero" }, + { .name = "empty_at_nonzero_offset", .start = 8192 }, + { .name = "one_byte", .start = 8192, .length = 1, .end = 8192 }, + { .name = "block_at_zero", .length = 512, .end = 511 }, + { .name = "block_at_nonzero_offset", .start = 8192, .length = 512, .end = 8703 }, + { .name = "cross_page", .start = 3584, .length = 1024, .end = 4607 }, + { .name = "above_32_bits", .start = 1LL << 32, .length = 4096, + .end = (1LL << 32) + 4095 }, + { .name = "last_representable_byte", .start = LLONG_MAX - 511, .length = 512, + .end = LLONG_MAX }, + { .name = "write_error_sense", .start = 8192, .length = 512, .end = 8703, + .error = -EIO, .status = SAM_STAT_CHECK_CONDITION }, + { .name = "memory_pressure_busy", .start = 8192, .length = 512, .end = 8703, + .error = -ENOMEM, .status = SAM_STAT_BUSY }, + { .name = "commandless_write_error", .start = 8192, .length = 512, .end = 8703, + .error = -EIO, .no_cmd = true }, + { .name = "commandless_memory_pressure", .start = 8192, .length = 512, .end = 8703, + .error = -ENOMEM, .no_cmd = true }, +}; + +static void vdisk_range_desc(const struct vdisk_range_case *p, char *desc) +{ + snprintf(desc, KUNIT_PARAM_DESC_SIZE, "%s", p->name); +} + +KUNIT_ARRAY_PARAM(vdisk_range, vdisk_range_cases, vdisk_range_desc); + +static void vdisk_range_test(struct kunit *test) +{ + const struct vdisk_range_case *p = test->param_value; + struct vdisk_test_fixture *f = test->priv; + int ret; + + f->data.sync_result = p->error; + ret = __vdisk_fsync_fileio(p->start, p->length, &f->dev, + p->no_cmd ? NULL : &f->cmd, &f->data.file); + KUNIT_EXPECT_EQ(test, ret, p->error); + KUNIT_EXPECT_EQ(test, f->cmd.status, p->status); + KUNIT_EXPECT_EQ(test, f->completions, 0U); + /* An empty range may be a no-op or a harmless backend synchronization. */ + if (p->length) { + KUNIT_EXPECT_EQ(test, f->flush_count, 1U); + vdisk_test_expect_flush(test, 0, &f->data.file, p->start, p->end); + } + if (p->status == SAM_STAT_CHECK_CONDITION) { + KUNIT_EXPECT_EQ(test, f->sense[2] & 0xf, MEDIUM_ERROR); + KUNIT_EXPECT_EQ(test, f->sense[12], (u8)0x03); + } +} + +struct vdisk_flush_case { + const char *name; + bool async; + bool no_cmd; + bool wt; + bool nv_cache; + bool nullio; + bool direct; + bool blockio; + unsigned int dif_shift; + int data_error; + int dif_error; + unsigned int files; + unsigned int blocks; + int error; +}; + +static const struct vdisk_flush_case vdisk_flush_cases[] = { + { .name = "fileio", .files = 1 }, + { .name = "async_fileio", .async = true, .files = 1 }, + { .name = "commandless_async_fileio", .async = true, .no_cmd = true, .files = 1 }, + { .name = "direct_fileio", .direct = true, .files = 1 }, + { .name = "async_direct_fileio", .async = true, .direct = true, .files = 1 }, + { .name = "async_fileio_error", .async = true, .data_error = -EIO, + .files = 1, .error = -EIO }, + { .name = "async_write_through", .async = true, .wt = true }, + { .name = "commandless_write_through", .async = true, .no_cmd = true, .wt = true }, + { .name = "async_nonvolatile_cache", .async = true, .nv_cache = true }, + { .name = "commandless_nonvolatile_cache", .async = true, .no_cmd = true, + .nv_cache = true }, + { .name = "async_nullio", .async = true, .nullio = true }, + { .name = "commandless_nullio", .async = true, .no_cmd = true, .nullio = true }, + { .name = "fileio_dif_512", .dif_shift = 9, .files = 2 }, + { .name = "fileio_dif_4096", .dif_shift = 12, .files = 2 }, + { .name = "data_error_skips_dif", .dif_shift = 9, .data_error = -EIO, + .files = 1, .error = -EIO }, + { .name = "dif_error_propagated", .dif_shift = 9, .dif_error = -EIO, + .files = 2, .error = -EIO }, + { .name = "blockio", .blockio = true, .blocks = 1 }, + { .name = "blockio_dif_512", .blockio = true, .dif_shift = 9, .files = 1, .blocks = 1 }, + { .name = "blockio_dif_4096", .blockio = true, .dif_shift = 12, .files = 1, + .blocks = 1 }, + { .name = "dif_error_skips_block_flush", .blockio = true, .dif_shift = 9, + .dif_error = -EIO, .files = 1, .error = -EIO }, +}; + +static void vdisk_flush_desc(const struct vdisk_flush_case *p, char *desc) +{ + snprintf(desc, KUNIT_PARAM_DESC_SIZE, "%s", p->name); +} + +KUNIT_ARRAY_PARAM(vdisk_flush, vdisk_flush_cases, vdisk_flush_desc); + +static void vdisk_flush_test(struct kunit *test) +{ + const struct vdisk_flush_case *p = test->param_value; + struct vdisk_test_fixture *f = test->priv; + int ret; + + /* The baseline bypass path dereferences cmd even when IMMED passed NULL. */ + if (p->async && p->no_cmd && (p->wt || p->nv_cache || p->nullio || p->direct)) { + kunit_skip(test, "commandless cache bypass needs a separate NULL-command fix"); + return; + } + + f->vdev.wt_flag = p->wt; + f->vdev.nv_cache = p->nv_cache; + f->vdev.nullio = p->nullio; + f->vdev.o_direct_flag = p->direct; + f->vdev.blockio = p->blockio; + f->vdev.bdev_desc.bdev = p->blockio ? &f->bdev : NULL; + f->data.sync_result = p->data_error; + f->dif.sync_result = p->dif_error; + if (p->dif_shift) { + f->vdev.dif_fd = &f->dif.file; + f->dev.block_shift = p->dif_shift; + f->dev.block_size = 1U << p->dif_shift; + } + + ret = vdisk_fsync(8192, 4096, &f->dev, GFP_KERNEL, p->no_cmd ? NULL : &f->cmd, + p->async); + KUNIT_EXPECT_EQ(test, ret, p->error); + KUNIT_EXPECT_EQ(test, f->flush_count, p->files); + KUNIT_EXPECT_EQ(test, f->block_flush_count, p->blocks); + KUNIT_EXPECT_EQ(test, f->completions, p->async && !p->no_cmd ? 1U : 0U); + if (p->async && !p->no_cmd) { + KUNIT_EXPECT_EQ(test, f->completion_state, SCST_CMD_STATE_DEFAULT); + KUNIT_EXPECT_TRUE(test, f->completed_before_callback); + } + KUNIT_EXPECT_EQ(test, !!f->cmd.completed, (p->async || p->error) && !p->no_cmd); + KUNIT_EXPECT_EQ(test, f->cmd.status, + p->error && !p->no_cmd ? SAM_STAT_CHECK_CONDITION : SAM_STAT_GOOD); + if (!p->blockio && p->files) + vdisk_test_expect_flush(test, 0, &f->data.file, 8192, 12287); + if (p->dif_shift && p->files > !p->blockio) { + /* 8-byte tags: 16+8 blocks at 512 bytes, 2+1 at 4096 bytes. */ + vdisk_test_expect_flush(test, !p->blockio, &f->dif.file, + p->dif_shift == 9 ? 128 : 16, + p->dif_shift == 9 ? 191 : 23); + } +} + +struct vdisk_backend_case { + const char *name; + bool closed; + bool inactive; + bool nullio; + bool blockio; + bool empty; + int error; + int expected; + unsigned int files; + unsigned int blocks; +}; + +static const struct vdisk_backend_case vdisk_backend_cases[] = { + { .name = "inactive_fileio", .closed = true, .inactive = true, .expected = -ENOMEDIUM }, + { .name = "unmapped_fileio", .closed = true, .expected = -ENOMEDIUM }, + { .name = "inactive_blockio", .closed = true, .inactive = true, .blockio = true, + .expected = -ENOMEDIUM }, + { .name = "unmapped_blockio", .closed = true, .blockio = true, .expected = -ENOMEDIUM }, + { .name = "nullio_without_backend", .closed = true, .nullio = true }, + { .name = "empty_file", .empty = true }, + { .name = "open_file", .files = 1 }, + { .name = "file_error", .error = -EIO, .files = 1, .expected = -EIO }, + { .name = "file_memory_pressure", .error = -ENOMEM, .files = 1, .expected = -ENOMEM }, + { .name = "open_block_device", .blockio = true, .blocks = 1 }, + { .name = "block_error", .blockio = true, .error = -EIO, .blocks = 1, .expected = -EIO }, +}; + +static void vdisk_backend_desc(const struct vdisk_backend_case *p, char *desc) +{ + snprintf(desc, KUNIT_PARAM_DESC_SIZE, "%s", p->name); +} + +KUNIT_ARRAY_PARAM(vdisk_backend, vdisk_backend_cases, vdisk_backend_desc); + +static void vdisk_backend_test(struct kunit *test) +{ + const struct vdisk_backend_case *p = test->param_value; + struct vdisk_test_fixture *f = test->priv; + int ret; + + /* Avoid dereferencing a closed backend until its lifetime is fixed. */ + if (p->closed && !p->nullio) { + kunit_skip(test, "closed-backend sync needs a separate lifetime fix"); + return; + } + + f->vdev.dev_active = !p->inactive; + f->vdev.nullio = p->nullio; + f->vdev.blockio = p->blockio; + f->vdev.fd = p->closed || p->blockio ? NULL : &f->data.file; + f->vdev.bdev_desc.bdev = p->blockio && !p->closed ? &f->bdev : NULL; + f->data.inode.i_size = p->empty ? 0 : 16384; + f->data.sync_result = p->error; + f->block_flush_result = p->error; + f->sysfs_sync = true; + + /* Exercise the existing store callback with an unregistered private device. */ + ret = vdisk_sysfs_sync_store(&f->dev.dev_kobj, NULL, "1", 1); + KUNIT_EXPECT_EQ(test, ret, p->expected ? p->expected : 1); + /* Empty files may still need metadata synchronization. */ + if (!p->empty) + KUNIT_EXPECT_EQ(test, f->flush_count, p->files); + KUNIT_EXPECT_EQ(test, f->block_flush_count, p->blocks); + KUNIT_EXPECT_EQ(test, f->completions, 0U); + KUNIT_EXPECT_EQ(test, !!f->vdev.dev_active, !p->inactive); + KUNIT_EXPECT_PTR_EQ(test, f->vdev.fd, p->closed || p->blockio ? NULL : &f->data.file); + if (p->files) + vdisk_test_expect_flush(test, 0, &f->data.file, 0, 16383); +} + +static struct kunit_case vdisk_sync_cases[] = { + KUNIT_CASE_PARAM(vdisk_range_test, vdisk_range_gen_params), + KUNIT_CASE_PARAM(vdisk_flush_test, vdisk_flush_gen_params), + KUNIT_CASE_PARAM(vdisk_backend_test, vdisk_backend_gen_params), + {} +}; + +scst_kunit_suite(vdisk_sync_suite, "VDISK", "-sync", + .init = vdisk_test_init, + .exit = vdisk_test_exit, + .test_cases = vdisk_sync_cases, +); diff --git a/scst/src/dev_handlers/scst_vdisk_main.c b/scst/src/dev_handlers/scst_vdisk_main.c index 535059d7a..3d7007c75 100644 --- a/scst/src/dev_handlers/scst_vdisk_main.c +++ b/scst/src/dev_handlers/scst_vdisk_main.c @@ -273,11 +273,14 @@ out_put: TRACE_EXIT(); } -static int vdisk_blockio_flush(struct block_device *bdev, gfp_t gfp_mask, bool report_error, - struct scst_cmd *cmd, bool async) +SCST_KUNIT_VISIBLE +int vdisk_blockio_flush(struct block_device *bdev, gfp_t gfp_mask, bool report_error, + struct scst_cmd *cmd, bool async) { int res = 0; + SCST_KUNIT_REDIRECT(vdisk_blockio_flush, bdev, gfp_mask, report_error, cmd, async); + TRACE_ENTRY(); if (async) { @@ -1383,8 +1386,16 @@ static void vdisk_detach_tgt(struct scst_tgt_dev *tgt_dev) TRACE_EXIT(); } -static int __vdisk_fsync_fileio(loff_t loff, loff_t len, struct scst_device *dev, - struct scst_cmd *cmd, struct file *file) +SCST_KUNIT_VISIBLE +int vdisk_writeback_range(struct address_space *mapping, loff_t start, loff_t end) +{ + SCST_KUNIT_REDIRECT(vdisk_writeback_range, mapping, start, end); + return filemap_write_and_wait_range(mapping, start, end); +} + +SCST_KUNIT_VISIBLE +int __vdisk_fsync_fileio(loff_t loff, loff_t len, struct scst_device *dev, + struct scst_cmd *cmd, struct file *file) { int res; @@ -1400,7 +1411,7 @@ static int __vdisk_fsync_fileio(loff_t loff, loff_t len, struct scst_device *dev #if 0 /* For sparse files we might need to sync metadata as well */ res = generic_write_sync(file, loff, len); #else - res = filemap_write_and_wait_range(file->f_mapping, loff, len); + res = vdisk_writeback_range(file->f_mapping, loff, len); #endif if (unlikely(res != 0)) { PRINT_ERROR("sync range failed (%d)", res); @@ -1482,8 +1493,9 @@ done: return res; } -static int vdisk_fsync(loff_t loff, loff_t len, struct scst_device *dev, gfp_t gfp_flags, - struct scst_cmd *cmd, bool async) +SCST_KUNIT_VISIBLE +int vdisk_fsync(loff_t loff, loff_t len, struct scst_device *dev, gfp_t gfp_flags, + struct scst_cmd *cmd, bool async) { int res = 0; struct scst_vdisk_dev *virt_dev = dev->dh_priv; @@ -3169,7 +3181,8 @@ static enum compl_status_e fileio_exec_async(struct vdisk_cmd_params *p) } #endif -static void vdisk_on_free_cmd_params(const struct vdisk_cmd_params *p) +SCST_KUNIT_VISIBLE +void vdisk_on_free_cmd_params(const struct vdisk_cmd_params *p) { if (!p->execute_async) { if (p->sync.kvec != p->sync.small_kvec) @@ -5332,7 +5345,8 @@ static enum compl_status_e fileio_exec_var_len_cmd(struct vdisk_cmd_params *p) * RUNNING_ASYNC the SCST command may already have completed before this * function returns. */ -static enum compl_status_e fileio_exec_write(struct vdisk_cmd_params *p) +SCST_KUNIT_VISIBLE +enum compl_status_e fileio_exec_write(struct vdisk_cmd_params *p) { struct scst_cmd *cmd = p->cmd; struct scst_device *dev = cmd->dev; @@ -5958,7 +5972,8 @@ static enum compl_status_e blockio_exec_read(struct vdisk_cmd_params *p) return RUNNING_ASYNC; } -static enum compl_status_e fileio_exec_read(struct vdisk_cmd_params *p) +SCST_KUNIT_VISIBLE +enum compl_status_e fileio_exec_read(struct vdisk_cmd_params *p) { struct scst_cmd *cmd = p->cmd; loff_t loff = p->loff; @@ -7477,8 +7492,9 @@ out_e_unlock: goto out_resume; } -static ssize_t vdisk_sysfs_sync_store(struct kobject *kobj, struct kobj_attribute *attr, - const char *buf, size_t count) +SCST_KUNIT_VISIBLE +ssize_t vdisk_sysfs_sync_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count) { struct scst_device *dev = container_of(kobj, struct scst_device, dev_kobj); diff --git a/scst/src/dev_handlers/scst_vdisk_test.h b/scst/src/dev_handlers/scst_vdisk_test.h index d8a061d28..d4327c605 100644 --- a/scst/src/dev_handlers/scst_vdisk_test.h +++ b/scst/src/dev_handlers/scst_vdisk_test.h @@ -8,7 +8,19 @@ struct vdisk_cmd_params; +int vdisk_blockio_flush(struct block_device *bdev, gfp_t gfp_mask, bool report_error, + struct scst_cmd *cmd, bool async); +int vdisk_writeback_range(struct address_space *mapping, loff_t start, loff_t end); +int __vdisk_fsync_fileio(loff_t loff, loff_t len, struct scst_device *dev, + struct scst_cmd *cmd, struct file *file); +int vdisk_fsync(loff_t loff, loff_t len, struct scst_device *dev, gfp_t gfp_flags, + struct scst_cmd *cmd, bool async); +void vdisk_on_free_cmd_params(const struct vdisk_cmd_params *p); enum compl_status_e vdisk_exec_sai_16(struct vdisk_cmd_params *p); +enum compl_status_e fileio_exec_write(struct vdisk_cmd_params *p); +enum compl_status_e fileio_exec_read(struct vdisk_cmd_params *p); +ssize_t vdisk_sysfs_sync_store(struct kobject *kobj, struct kobj_attribute *attr, + const char *buf, size_t count); #endif /* SCST_KUNIT_ENABLED */ #endif /* SCST_VDISK_TEST_H */ diff --git a/scst/src/kunit/objects.mk b/scst/src/kunit/objects.mk index b85363624..5646c7ec3 100644 --- a/scst/src/kunit/objects.mk +++ b/scst/src/kunit/objects.mk @@ -15,4 +15,7 @@ scst-kunit-scst := \ kunit/unmap/suites/scst_kunit_unmap.o scst-kunit-scst_vdisk := \ - kunit/vdisk/suites/scst_vdisk_capacity_test.o + kunit/vdisk/scst_vdisk_fixture.o \ + kunit/vdisk/suites/scst_vdisk_capacity_test.o \ + kunit/vdisk/suites/scst_vdisk_io_test.o \ + kunit/vdisk/suites/scst_vdisk_sync_test.o