From 50eb95a451cd0403ad9b9b0d92aba45bbce02802 Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Tue, 13 Oct 2026 11:55:59 +0300 Subject: [PATCH] kunit: Add VDISK write and synchronization regressions Link separate I/O and synchronization suites with shared fixtures. Use private file operations and native task-scoped block-flush and writeback stubs. Distinguish page writeback from filesystem synchronization and accept either fsync or fdatasync. The writeback boundary retains the original API and arguments. Assert FUA offsets, inclusive ranges, filesystem synchronization, asynchronous flags, errors and completion counts. Check failed writes for status and progress without requiring full-range FUA synchronization. Allow empty-range and empty-file requests to omit backend synchronization or perform it. Exercise the original sysfs store on a private device. Explicitly skip unsafe closed-backend and commandless-bypass cases pending separate production fixes. --- .../kunit/vdisk/scst_vdisk_fixture.c | 216 +++++++++++++++ .../kunit/vdisk/scst_vdisk_fixture.h | 68 +++++ .../kunit/vdisk/suites/scst_vdisk_io_test.c | 225 ++++++++++++++++ .../kunit/vdisk/suites/scst_vdisk_sync_test.c | 249 ++++++++++++++++++ scst/src/dev_handlers/scst_vdisk_main.c | 40 ++- scst/src/dev_handlers/scst_vdisk_test.h | 12 + scst/src/kunit/objects.mk | 5 +- 7 files changed, 802 insertions(+), 13 deletions(-) create mode 100644 scst/src/dev_handlers/kunit/vdisk/scst_vdisk_fixture.c create mode 100644 scst/src/dev_handlers/kunit/vdisk/scst_vdisk_fixture.h create mode 100644 scst/src/dev_handlers/kunit/vdisk/suites/scst_vdisk_io_test.c create mode 100644 scst/src/dev_handlers/kunit/vdisk/suites/scst_vdisk_sync_test.c 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