From e284f3299aed5b040259b53f8fd6f99f62258157 Mon Sep 17 00:00:00 2001 From: Gleb Chesnokov Date: Tue, 13 Oct 2026 11:55:59 +0300 Subject: [PATCH] kunit: Test LUN rollback, teardown and external blockers Add private access declarations and task-scoped boundaries for sysfs, copy-manager admission, grace periods and blocking waits. Preserve their original operations outside the current test task. Share the existing ACG release-work layout through the private core header so the worker and its separate test object use one definition. Link separate lifecycle and blocker suites covering rollback, mapping detach order, ACG worker locking and reference ownership. Cleanup tolerates failed initialization and assertions. --- doc/KUNIT.md | 2 +- scripts/generate-kernel-patch | 3 +- .../lifecycle/suites/scst_kunit_blocking.c | 192 +++++++++ .../lifecycle/suites/scst_kunit_lifecycle.c | 368 ++++++++++++++++++ scst/src/kunit/objects.mk | 2 + scst/src/scst_lib.c | 111 ++++-- scst/src/scst_lib_test.h | 26 ++ scst/src/scst_priv.h | 6 + 8 files changed, 680 insertions(+), 30 deletions(-) create mode 100644 scst/src/kunit/lifecycle/suites/scst_kunit_blocking.c create mode 100644 scst/src/kunit/lifecycle/suites/scst_kunit_lifecycle.c create mode 100644 scst/src/scst_lib_test.h diff --git a/doc/KUNIT.md b/doc/KUNIT.md index 11427d7da..b3cbd4ad3 100644 --- a/doc/KUNIT.md +++ b/doc/KUNIT.md @@ -60,7 +60,7 @@ validates the selection against the supplied `.ko` files. | Module | Labels | | --- | --- | -| `scst.ko` | `ALUA CDB CM EVENT LIB ORDER UNMAP` | +| `scst.ko` | `ALUA CDB CM EVENT LIB LIFECYCLE ORDER UNMAP` | The runner reads suite metadata from the modules and loads selected owners with their dependencies. Unselected suites are not counted as skipped. diff --git a/scripts/generate-kernel-patch b/scripts/generate-kernel-patch index bdcd26544..b7651a0c1 100755 --- a/scripts/generate-kernel-patch +++ b/scripts/generate-kernel-patch @@ -316,7 +316,8 @@ scst/src/scst_copy_mgr_test.h" scst_05_targ="scst/src/scst_targ.c scst/src/scst_local_cmd.c \ scst/src/scst_local_cmd.h \ scst/src/scst_targ_test.h" -scst_06_lib="scst/src/scst_lib.c" +scst_06_lib="scst/src/scst_lib.c \ +scst/src/scst_lib_test.h" scst_07_pres="scst/src/scst_pres.h scst/src/scst_pres.c" scst_08_sysfs="scst/src/scst_sysfs.c" scst_09_debug="scst/include/scst_debug.h scst/src/scst_debug.c" diff --git a/scst/src/kunit/lifecycle/suites/scst_kunit_blocking.c b/scst/src/kunit/lifecycle/suites/scst_kunit_blocking.c new file mode 100644 index 000000000..814f577e8 --- /dev/null +++ b/scst/src/kunit/lifecycle/suites/scst_kunit_blocking.c @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "../../scst_kunit.h" + +#ifdef INSIDE_KERNEL_TREE +#include +#else +#include "scst.h" +#endif +#include "../../../scst_priv.h" +#include "../../../scst_lib_test.h" + +struct blocking_fixture { + struct scst_device dev; + wait_queue_entry_t waiter; + wait_queue_head_t *queue; + unsigned int waits; + unsigned int wakes; + unsigned int callbacks; + int wait_result; + u8 payload[4]; +}; + +static int blocking_wait(struct scst_device *dev, wait_queue_head_t *w) +{ + struct kunit *test = kunit_get_current_test(); + struct blocking_fixture *f = test->priv; + struct scst_ext_blocker *b; + + lockdep_assert_held(&dev->dev_lock); + b = list_first_entry(&dev->ext_blockers_list, typeof(*b), ext_blockers_list_entry); + KUNIT_EXPECT_PTR_EQ(test, w, (wait_queue_head_t *)b->ext_blocker_data); + KUNIT_EXPECT_EQ(test, kref_read(&b->refcount), 2U); + KUNIT_EXPECT_TRUE(test, !!dev->ext_blocking_pending); + f->waits++; + /* Return before the completion worker, as in the reported interleaving. */ + if (!f->wait_result) + dev->on_dev_cmd_count = 0; + return f->wait_result; +} + +static int blocking_wake(wait_queue_entry_t *entry, unsigned int mode, int flags, void *key) +{ + struct blocking_fixture *f = container_of(entry, typeof(*f), waiter); + + f->wakes++; + list_del_init(&entry->entry); + f->queue = NULL; + return 1; +} + +static void blocking_callback(struct scst_device *dev, u8 *data, int len) +{ + struct kunit *test = kunit_get_current_test(); + struct blocking_fixture *f = test->priv; + bool unlocked; + + KUNIT_EXPECT_PTR_EQ(test, dev, &f->dev); + KUNIT_EXPECT_EQ(test, len, (int)sizeof(f->payload)); + KUNIT_EXPECT_PTR_NE(test, data, &f->payload[0]); + KUNIT_EXPECT_EQ(test, memcmp(data, f->payload, sizeof(f->payload)), 0); + unlocked = spin_trylock_bh(&dev->dev_lock); + if (unlocked) + spin_unlock_bh(&dev->dev_lock); + KUNIT_EXPECT_TRUE(test, unlocked); + f->callbacks++; +} + +static int blocking_init(struct kunit *test) +{ + struct blocking_fixture *f; + + test->priv = NULL; + f = kunit_kzalloc(test, sizeof(*f), GFP_KERNEL); + if (!f) + return -ENOMEM; + test->priv = f; + f->dev.virt_name = "kunit-private-blocking"; + spin_lock_init(&f->dev.dev_lock); + INIT_LIST_HEAD(&f->dev.ext_blockers_list); + INIT_LIST_HEAD(&f->dev.blocked_cmd_list); + INIT_WORK(&f->dev.ext_blockers_work, scst_ext_blocking_done_fn); + init_waitqueue_func_entry(&f->waiter, blocking_wake); + memcpy(f->payload, "test", sizeof(f->payload)); + kunit_activate_static_stub(test, scst_wait_ext_block_dev, blocking_wait); + return 0; +} + +static void blocking_exit(struct kunit *test) +{ + struct blocking_fixture *f = test->priv; + struct scst_ext_blocker *b, *tmp; + + if (!f) + return; + test->priv = NULL; + if (f->queue) + remove_wait_queue(f->queue, &f->waiter); + list_for_each_entry_safe(b, tmp, &f->dev.ext_blockers_list, ext_blockers_list_entry) { + list_del(&b->ext_blockers_list_entry); + scst_ext_blocker_put(b); + } +} + +struct blocking_case { + const char *name; + bool active; + bool interrupted; + bool serialized; + bool asynchronous; + bool stpg; +}; + +static const struct blocking_case blocking_cases_data[] = { + { "sync-idle" }, + { "sync-caller-returns-before-worker", true }, + { "sync-interrupted-before-worker", true, true }, + { "sync-reuses-serialization", false, false, true }, + { "sync-interrupted-reuses-serialization", true, true, true }, + { "async-immediate", false, false, false, true }, + { "async-deferred", true, false, false, true }, + { "async-stpg", true, false, false, true, true }, + { "async-serialized", true, false, true, true }, +}; + +static void blocking_desc(const struct blocking_case *p, char *desc) +{ + strscpy(desc, p->name, KUNIT_PARAM_DESC_SIZE); +} + +KUNIT_ARRAY_PARAM(blocking, blocking_cases_data, blocking_desc); + +static void blocking_test(struct kunit *test) +{ + const struct blocking_case *p = test->param_value; + struct blocking_fixture *f = test->priv; + struct scst_ext_blocker *b; + int rc; + + f->dev.on_dev_cmd_count = p->active; + f->dev.strictly_serialized_cmd_waiting = p->serialized; + f->dev.block_count = p->serialized; + f->wait_result = p->interrupted ? -ERESTARTSYS : 0; + if (p->asynchronous) + rc = scst_ext_block_dev(&f->dev, blocking_callback, f->payload, + sizeof(f->payload), p->stpg); + else + rc = scst_sync_ext_block_dev(&f->dev); + KUNIT_EXPECT_EQ(test, rc, p->interrupted ? -ERESTARTSYS : 0); + KUNIT_EXPECT_EQ(test, f->dev.block_count, 1); + KUNIT_EXPECT_EQ(test, f->dev.ext_blocks_cnt, 1); + KUNIT_EXPECT_FALSE(test, !!f->dev.strictly_serialized_cmd_waiting); + KUNIT_EXPECT_EQ(test, f->waits, (unsigned int)(p->active && !p->asynchronous)); + if (p->active) { + KUNIT_ASSERT_FALSE(test, list_empty(&f->dev.ext_blockers_list)); + b = list_first_entry(&f->dev.ext_blockers_list, typeof(*b), + ext_blockers_list_entry); + KUNIT_EXPECT_EQ(test, kref_read(&b->refcount), 1U); + if (!p->asynchronous) { + /* The caller's reference is gone; its heap wait queue must still live. */ + f->queue = (void *)b->ext_blocker_data; + add_wait_queue(f->queue, &f->waiter); + } + f->dev.on_dev_cmd_count = 0; + f->dev.ext_unblock_scheduled = 1; + scst_ext_blocking_done_fn(&f->dev.ext_blockers_work); + } + KUNIT_EXPECT_EQ(test, f->wakes, (unsigned int)(p->active && !p->asynchronous)); + KUNIT_EXPECT_EQ(test, f->callbacks, (unsigned int)p->asynchronous); + KUNIT_EXPECT_TRUE(test, list_empty(&f->dev.ext_blockers_list)); + KUNIT_EXPECT_FALSE(test, !!f->dev.ext_unblock_scheduled); + KUNIT_EXPECT_FALSE(test, !!f->dev.ext_blocking_pending); + if (p->stpg) { + scst_ext_unblock_dev(&f->dev, false); + KUNIT_EXPECT_EQ(test, f->dev.block_count, 1); + } + scst_ext_unblock_dev(&f->dev, p->stpg); + KUNIT_EXPECT_EQ(test, f->dev.block_count, 0); + KUNIT_EXPECT_EQ(test, f->dev.ext_blocks_cnt, 0); + scst_ext_unblock_dev(&f->dev, false); + KUNIT_EXPECT_EQ(test, f->dev.block_count, 0); +} + +static struct kunit_case blocking_cases[] = { + KUNIT_CASE_PARAM(blocking_test, blocking_gen_params), + {} +}; + +scst_kunit_suite(blocking_suite, "LIFECYCLE", "-blocking", + .init = blocking_init, + .exit = blocking_exit, + .test_cases = blocking_cases, +); diff --git a/scst/src/kunit/lifecycle/suites/scst_kunit_lifecycle.c b/scst/src/kunit/lifecycle/suites/scst_kunit_lifecycle.c new file mode 100644 index 000000000..0d631a953 --- /dev/null +++ b/scst/src/kunit/lifecycle/suites/scst_kunit_lifecycle.c @@ -0,0 +1,368 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "../../scst_kunit.h" + +#ifdef INSIDE_KERNEL_TREE +#include +#else +#include "scst.h" +#endif +#include "../../../scst_priv.h" +#include "../../../scst_lib_test.h" + +#define LIFECYCLE_MAPPINGS 12 + +struct lifecycle_fixture { + struct scst_device dev; + struct scst_tgt tgt; + struct scst_tgt_template tgtt; + struct scst_acg acg; + struct scst_session sessions[3]; + struct scst_tgt_dev mappings[LIFECYCLE_MAPPINGS]; + bool published[LIFECYCLE_MAPPINGS]; + bool removed[LIFECYCLE_MAPPINGS]; + bool freed[LIFECYCLE_MAPPINGS]; + unsigned int retired[LIFECYCLE_MAPPINGS]; + int alloc_results[3]; + unsigned int alloc_calls; + unsigned int deletes; + unsigned int frees; + unsigned int grace_periods; + unsigned int cm_adds; + unsigned int cm_deletes; + unsigned int sysfs_creates; + unsigned int acg_frees; + int cm_result; + int sysfs_result; + bool batch; +}; + +static void lifecycle_publish(struct lifecycle_fixture *f, unsigned int index, + struct scst_session *sess, u64 lun) +{ + struct scst_tgt_dev *td = &f->mappings[index]; + + td->dev = &f->dev; + td->sess = sess; + td->tgtt = &f->tgtt; + td->lun = lun; + list_add_tail(&td->dev_tgt_dev_list_entry, &f->dev.dev_tgt_dev_list); + list_add_tail_rcu(&td->sess_tgt_dev_list_entry, + &sess->sess_tgt_dev_list[SESS_TGT_DEV_LIST_HASH_FN(lun)]); + f->published[index] = true; +} + +static int lifecycle_alloc(struct scst_session *sess, struct scst_acg_dev *ad, + struct scst_tgt_dev **out) +{ + struct kunit *test = kunit_get_current_test(); + struct lifecycle_fixture *f = test->priv; + unsigned int index = f->alloc_calls++; + + KUNIT_EXPECT_TRUE(test, mutex_is_locked(&scst_mutex)); + if (index >= ARRAY_SIZE(f->alloc_results)) + return -EOVERFLOW; + if (f->alloc_results[index]) + return f->alloc_results[index]; + lifecycle_publish(f, index, sess, ad->lun); + f->mappings[index].acg_dev = ad; + *out = &f->mappings[index]; + return 0; +} + +static void lifecycle_sysfs_remove(struct scst_tgt_dev *td) +{ + struct kunit *test = kunit_get_current_test(); + struct lifecycle_fixture *f = test->priv; + unsigned int index = td - f->mappings; + + KUNIT_EXPECT_TRUE(test, mutex_is_locked(&scst_mutex)); + KUNIT_EXPECT_TRUE(test, mutex_is_locked(&td->sess->tgt_dev_list_mutex)); + rcu_read_lock(); + KUNIT_EXPECT_PTR_EQ(test, scst_lookup_tgt_dev(td->sess, td->lun), NULL); + rcu_read_unlock(); + KUNIT_EXPECT_FALSE(test, f->removed[index]); + f->removed[index] = true; + f->deletes++; +} + +static void lifecycle_grace_period(void) +{ + struct kunit *test = kunit_get_current_test(); + struct lifecycle_fixture *f = test->priv; + unsigned int i; + + KUNIT_EXPECT_TRUE(test, mutex_is_locked(&scst_mutex)); + for (i = 0; i < ARRAY_SIZE(f->sessions); i++) + KUNIT_EXPECT_FALSE(test, mutex_is_locked(&f->sessions[i].tgt_dev_list_mutex)); + f->grace_periods++; + for (i = 0; i < ARRAY_SIZE(f->mappings); i++) + if (f->removed[i]) + f->retired[i] = f->grace_periods; +} + +static void lifecycle_free(struct scst_tgt_dev *td) +{ + struct kunit *test = kunit_get_current_test(); + struct lifecycle_fixture *f = test->priv; + unsigned int index = td - f->mappings; + + KUNIT_EXPECT_TRUE(test, f->removed[index]); + KUNIT_EXPECT_GT(test, f->retired[index], 0U); + KUNIT_EXPECT_FALSE(test, f->freed[index]); + KUNIT_EXPECT_FALSE(test, mutex_is_locked(&td->sess->tgt_dev_list_mutex)); + if (f->batch) + KUNIT_EXPECT_TRUE(test, list_empty(&f->dev.dev_tgt_dev_list)); + f->freed[index] = true; + f->frees++; +} + +static int lifecycle_cm_add(struct scst_acg_dev *ad, u64 lun, unsigned int *flags) +{ + struct kunit *test = kunit_get_current_test(); + struct lifecycle_fixture *f = test->priv; + + KUNIT_EXPECT_PTR_EQ(test, ad->acg, &f->acg); + KUNIT_EXPECT_EQ(test, lun, ad->lun); + KUNIT_EXPECT_EQ(test, (bool)ad->acg_dev_rd_only, !!(*flags & SCST_ADD_LUN_READ_ONLY)); + f->cm_adds++; + return f->cm_result; +} + +static void lifecycle_cm_del(struct scst_acg_dev *ad) +{ + struct kunit *test = kunit_get_current_test(); + struct lifecycle_fixture *f = test->priv; + + KUNIT_EXPECT_PTR_EQ(test, ad->dev, &f->dev); + KUNIT_EXPECT_EQ(test, f->frees, 0U); + f->cm_deletes++; +} + +static int lifecycle_sysfs_create(struct scst_acg_dev *ad, struct kobject *parent) +{ + struct kunit *test = kunit_get_current_test(); + struct lifecycle_fixture *f = test->priv; + + KUNIT_EXPECT_PTR_EQ(test, ad->dev, &f->dev); + KUNIT_EXPECT_PTR_EQ(test, parent, &f->tgt.tgt_kobj); + f->sysfs_creates++; + return f->sysfs_result; +} + +static void lifecycle_ref_release(struct percpu_ref *ref) +{ +} + +static int lifecycle_init(struct kunit *test) +{ + struct lifecycle_fixture *f; + unsigned int i, j; + int rc; + + test->priv = NULL; + f = kunit_kzalloc(test, sizeof(*f), GFP_KERNEL); + if (!f) + return -ENOMEM; + rc = percpu_ref_init(&f->dev.refcnt, lifecycle_ref_release, + PERCPU_REF_INIT_ATOMIC, GFP_KERNEL); + if (rc) + return rc; + f->dev.virt_name = "kunit-private-device"; + spin_lock_init(&f->dev.dev_lock); + INIT_LIST_HEAD(&f->dev.dev_tgt_dev_list); + INIT_LIST_HEAD(&f->dev.dev_acg_dev_list); + f->tgt.tgt_name = "kunit-private-target"; + f->tgt.tgtt = &f->tgtt; + f->acg.tgt = &f->tgt; + f->acg.acg_name = "kunit-private-group"; + INIT_LIST_HEAD(&f->acg.acg_dev_list); + INIT_LIST_HEAD(&f->acg.acg_sess_list); + for (i = 0; i < ARRAY_SIZE(f->sessions); i++) { + f->sessions[i].tgt = &f->tgt; + mutex_init(&f->sessions[i].tgt_dev_list_mutex); + INIT_LIST_HEAD(&f->sessions[i].acg_sess_list_entry); + for (j = 0; j < SESS_TGT_DEV_LIST_HASH_SIZE; j++) + INIT_LIST_HEAD(&f->sessions[i].sess_tgt_dev_list[j]); + } + test->priv = f; + kunit_activate_static_stub(test, scst_alloc_add_tgt_dev, lifecycle_alloc); + kunit_activate_static_stub(test, scst_tgt_dev_sysfs_remove, lifecycle_sysfs_remove); + kunit_activate_static_stub(test, scst_free_tgt_dev, lifecycle_free); + kunit_activate_static_stub(test, scst_tgt_dev_grace_period, lifecycle_grace_period); + kunit_activate_static_stub(test, scst_lun_cm_add, lifecycle_cm_add); + kunit_activate_static_stub(test, scst_lun_cm_del, lifecycle_cm_del); + kunit_activate_static_stub(test, scst_lun_sysfs_create, lifecycle_sysfs_create); + return 0; +} + +static void lifecycle_exit(struct kunit *test) +{ + struct lifecycle_fixture *f = test->priv; + struct scst_acg_dev *ad, *tmp; + unsigned int i; + + if (!f) + return; + /* Also reclaim an ACG allocation if an assertion interrupted a success case. */ + for (i = 0; i < ARRAY_SIZE(f->mappings); i++) { + if (f->published[i] && !f->removed[i]) { + list_del(&f->mappings[i].dev_tgt_dev_list_entry); + list_del(&f->mappings[i].sess_tgt_dev_list_entry); + } + } + mutex_lock(&scst_mutex); + list_for_each_entry_safe(ad, tmp, &f->acg.acg_dev_list, acg_dev_list_entry) + scst_del_free_acg_dev(ad, false); + mutex_unlock(&scst_mutex); + percpu_ref_exit(&f->dev.refcnt); + test->priv = NULL; +} + +struct lifecycle_lun_case { + const char *name; + unsigned int sessions; + int alloc[3]; + int cm_error; + int sysfs_error; + unsigned int flags; + int result; + unsigned int mappings; +}; + +static const struct lifecycle_lun_case lifecycle_luns[] = { + { "empty-group", 0, {}, 0, 0, 0, 0, 0 }, + { "three-sessions", 3, {}, 0, 0, 0, 0, 3 }, + { "read-only", 3, {}, 0, 0, SCST_ADD_LUN_READ_ONLY, 0, 3 }, + { "first-allocation-fails", 3, { -ENOMEM }, 0, 0, 0, -ENOMEM, 0 }, + { "second-allocation-fails", 3, { 0, -ENOMEM }, 0, 0, 0, -ENOMEM, 1 }, + { "third-allocation-fails", 3, { 0, 0, -EIO }, 0, 0, 0, -EIO, 2 }, + { "skip-denied-session", 3, { 0, -EPERM }, 0, 0, 0, 0, 2 }, + { "all-sessions-denied", 3, { -EPERM, -EPERM, -EPERM }, 0, 0, 0, 0, 0 }, + { "cm-reject-empty", 0, {}, -EINVAL, 0, 0, -EINVAL, 0 }, + { "cm-reject-one", 1, {}, -EINVAL, 0, SCST_ADD_LUN_READ_ONLY, -EINVAL, 1 }, + { "cm-reject-three", 3, {}, -EINVAL, 0, SCST_ADD_LUN_READ_ONLY, -EINVAL, 3 }, + { "sysfs-fails", 3, {}, 0, -ENOMEM, 0, -ENOMEM, 3 }, + { "cm-origin-skips-cm", 3, {}, -EIO, 0, SCST_ADD_LUN_CM, 0, 3 }, + { "cm-origin-sysfs-fails", 3, {}, 0, -EIO, SCST_ADD_LUN_CM, -EIO, 3 }, +}; + +static void lifecycle_lun_desc(const struct lifecycle_lun_case *p, char *desc) +{ + strscpy(desc, p->name, KUNIT_PARAM_DESC_SIZE); +} + +KUNIT_ARRAY_PARAM(lifecycle_lun, lifecycle_luns, lifecycle_lun_desc); + +static void lifecycle_lun_test(struct kunit *test) +{ + const struct lifecycle_lun_case *p = test->param_value; + struct lifecycle_fixture *f = test->priv; + struct scst_acg_dev *out = NULL; + unsigned int i; + int rc; + + memcpy(f->alloc_results, p->alloc, sizeof(p->alloc)); + f->cm_result = p->cm_error; + f->sysfs_result = p->sysfs_error; + for (i = 0; i < p->sessions; i++) + list_add_tail(&f->sessions[i].acg_sess_list_entry, &f->acg.acg_sess_list); + mutex_lock(&scst_mutex); + rc = scst_acg_add_lun(&f->acg, &f->tgt.tgt_kobj, &f->dev, 7, p->flags, &out); + mutex_unlock(&scst_mutex); + KUNIT_EXPECT_EQ(test, rc, p->result); + KUNIT_EXPECT_EQ(test, f->cm_deletes, + (unsigned int)(p->sysfs_error && !(p->flags & SCST_ADD_LUN_CM))); + if (rc) { + KUNIT_EXPECT_PTR_EQ(test, out, NULL); + KUNIT_EXPECT_EQ(test, f->deletes, p->mappings); + KUNIT_EXPECT_EQ(test, f->frees, p->mappings); + KUNIT_EXPECT_EQ(test, f->grace_periods, p->mappings); + KUNIT_EXPECT_TRUE(test, list_empty(&f->dev.dev_tgt_dev_list)); + KUNIT_EXPECT_TRUE(test, list_empty(&f->dev.dev_acg_dev_list)); + KUNIT_EXPECT_TRUE(test, list_empty(&f->acg.acg_dev_list)); + } else { + KUNIT_ASSERT_NOT_NULL(test, out); + KUNIT_EXPECT_EQ(test, out->lun, (u64)7); + KUNIT_EXPECT_EQ(test, f->frees, 0U); + KUNIT_EXPECT_EQ(test, f->sysfs_creates, 1U); + KUNIT_EXPECT_EQ(test, f->cm_adds, (unsigned int)!(p->flags & SCST_ADD_LUN_CM)); + } +} + +struct lifecycle_batch_case { + const char *name; + unsigned int count; + unsigned int stride; +}; + +static const struct lifecycle_batch_case lifecycle_batches[] = { + { "empty", 0, 1 }, + { "one", 1, 1 }, + { "separate-buckets", 12, 1 }, + { "same-bucket", 12, SESS_TGT_DEV_LIST_HASH_SIZE }, +}; + +static void lifecycle_batch_desc(const struct lifecycle_batch_case *p, char *desc) +{ + strscpy(desc, p->name, KUNIT_PARAM_DESC_SIZE); +} + +KUNIT_ARRAY_PARAM(lifecycle_batch, lifecycle_batches, lifecycle_batch_desc); + +static void lifecycle_batch_test(struct kunit *test) +{ + const struct lifecycle_batch_case *p = test->param_value; + struct lifecycle_fixture *f = test->priv; + unsigned int i; + + f->batch = true; + for (i = 0; i < p->count; i++) + lifecycle_publish(f, i, &f->sessions[0], i * p->stride); + mutex_lock(&scst_mutex); + scst_sess_free_tgt_devs(&f->sessions[0]); + mutex_unlock(&scst_mutex); + KUNIT_EXPECT_EQ(test, f->grace_periods, 1U); + KUNIT_EXPECT_EQ(test, f->deletes, p->count); + KUNIT_EXPECT_EQ(test, f->frees, p->count); + for (i = 0; i < SESS_TGT_DEV_LIST_HASH_SIZE; i++) + KUNIT_EXPECT_TRUE(test, list_empty(&f->sessions[0].sess_tgt_dev_list[i])); +} + +static void lifecycle_free_acg(struct scst_acg *acg) +{ + struct kunit *test = kunit_get_current_test(); + struct lifecycle_fixture *f = test->priv; + + KUNIT_EXPECT_PTR_EQ(test, acg, &f->acg); + KUNIT_EXPECT_TRUE(test, mutex_is_locked(&scst_mutex)); + lockdep_assert_held(&scst_mutex); + f->acg_frees++; +} + +static void lifecycle_acg_worker_test(struct kunit *test) +{ + struct lifecycle_fixture *f = test->priv; + struct scst_acg_release_work *work; + + /* Invoke the actual worker synchronously: static stubs are task-local. */ + work = kzalloc_obj(*work); + KUNIT_ASSERT_NOT_NULL(test, work); + work->acg = &f->acg; + INIT_WORK(&work->work, scst_release_acg_work); + kunit_activate_static_stub(test, scst_free_acg, lifecycle_free_acg); + scst_release_acg_work(&work->work); + KUNIT_EXPECT_EQ(test, f->acg_frees, 1U); +} + +static struct kunit_case lifecycle_cases[] = { + KUNIT_CASE_PARAM(lifecycle_lun_test, lifecycle_lun_gen_params), + KUNIT_CASE_PARAM(lifecycle_batch_test, lifecycle_batch_gen_params), + KUNIT_CASE(lifecycle_acg_worker_test), + {} +}; + +scst_kunit_suite(lifecycle_suite, "LIFECYCLE", "", + .init = lifecycle_init, + .exit = lifecycle_exit, + .test_cases = lifecycle_cases, +); diff --git a/scst/src/kunit/objects.mk b/scst/src/kunit/objects.mk index 5fc3e86d5..39181e493 100644 --- a/scst/src/kunit/objects.mk +++ b/scst/src/kunit/objects.mk @@ -9,5 +9,7 @@ scst-kunit-scst := \ kunit/event/suites/scst_kunit_event_import.o \ kunit/event/suites/scst_kunit_event_delivery.o \ kunit/lib/suites/scst_kunit_lib.o \ + kunit/lifecycle/suites/scst_kunit_lifecycle.o \ + kunit/lifecycle/suites/scst_kunit_blocking.o \ kunit/order/suites/scst_kunit_order.o \ kunit/unmap/suites/scst_kunit_unmap.o diff --git a/scst/src/scst_lib.c b/scst/src/scst_lib.c index 36148be01..6d00a878a 100644 --- a/scst/src/scst_lib.c +++ b/scst/src/scst_lib.c @@ -55,6 +55,14 @@ #include "scst_priv.h" #include "scst_mem.h" #include "scst_pres.h" +#include "scst_lib_test.h" + +#ifndef SCST_KUNIT_ENABLED +static int scst_alloc_add_tgt_dev(struct scst_session *sess, + struct scst_acg_dev *acg_dev, struct scst_tgt_dev **out_tgt_dev); +static void scst_free_tgt_dev(struct scst_tgt_dev *tgt_dev); +static void scst_ext_blocking_done_fn(struct work_struct *work); +#endif #undef DEFAULT_SYMBOL_NAMESPACE #define DEFAULT_SYMBOL_NAMESPACE SCST_NAMESPACE @@ -1636,7 +1644,6 @@ static const struct scst_sdbops scst_scsi_op_table[] = { #define SCST_CDB_TBL_SIZE (ARRAY_SIZE(scst_scsi_op_table)) static void scst_del_tgt_dev(struct scst_tgt_dev *tgt_dev); -static void scst_free_tgt_dev(struct scst_tgt_dev *tgt_dev); static void scst_check_internal_sense(struct scst_device *dev, int result, uint8_t *sense, int sense_len); static void scst_queue_report_luns_changed_UA(struct scst_session *sess, @@ -1648,8 +1655,6 @@ static void scst_alloc_set_UA(struct scst_tgt_dev *tgt_dev, static void scst_free_all_UA(struct scst_tgt_dev *tgt_dev); static void scst_release_space(struct scst_cmd *cmd); static void scst_clear_reservation(struct scst_tgt_dev *tgt_dev); -static int scst_alloc_add_tgt_dev(struct scst_session *sess, - struct scst_acg_dev *acg_dev, struct scst_tgt_dev **out_tgt_dev); static void scst_tgt_retry_timer_fn(struct timer_list *timer); #ifdef CONFIG_SCST_DEBUG_TM @@ -4262,8 +4267,6 @@ static void scst_init_order_data(struct scst_order_data *order_data) spin_lock_init(&order_data->init_done_lock); } -static void scst_ext_blocking_done_fn(struct work_struct *work); - static int scst_dif_none(struct scst_cmd *cmd); #ifdef CONFIG_SCST_DIF_INJECT_CORRUPTED_TAGS static int scst_dif_none_type1(struct scst_cmd *cmd); @@ -4452,7 +4455,8 @@ static void scst_free_acg_dev(struct scst_acg_dev *acg_dev) * The activity supposed to be suspended and scst_mutex held or the * corresponding target supposed to be stopped. */ -static void scst_del_free_acg_dev(struct scst_acg_dev *acg_dev, bool del_sysfs) +SCST_KUNIT_VISIBLE +void scst_del_free_acg_dev(struct scst_acg_dev *acg_dev, bool del_sysfs) { TRACE_ENTRY(); scst_del_acg_dev(acg_dev, true, del_sysfs); @@ -4530,6 +4534,42 @@ out: return res; } +/* Boundaries shared by LUN rollback and session teardown. */ +SCST_KUNIT_VISIBLE +void scst_tgt_dev_grace_period(void) +{ + SCST_KUNIT_REDIRECT(scst_tgt_dev_grace_period); + synchronize_rcu(); +} + +SCST_KUNIT_VISIBLE +int scst_lun_cm_add(struct scst_acg_dev *acg_dev, u64 lun, unsigned int *flags) +{ + SCST_KUNIT_REDIRECT(scst_lun_cm_add, acg_dev, lun, flags); + return scst_cm_on_add_lun(acg_dev, lun, flags); +} + +SCST_KUNIT_VISIBLE +void scst_lun_cm_del(struct scst_acg_dev *acg_dev) +{ + SCST_KUNIT_REDIRECT(scst_lun_cm_del, acg_dev); + scst_cm_on_del_lun(acg_dev, false); +} + +SCST_KUNIT_VISIBLE +int scst_lun_sysfs_create(struct scst_acg_dev *acg_dev, struct kobject *parent) +{ + SCST_KUNIT_REDIRECT(scst_lun_sysfs_create, acg_dev, parent); + return scst_acg_dev_sysfs_create(acg_dev, parent); +} + +SCST_KUNIT_VISIBLE +void scst_tgt_dev_sysfs_remove(struct scst_tgt_dev *tgt_dev) +{ + SCST_KUNIT_REDIRECT(scst_tgt_dev_sysfs_remove, tgt_dev); + scst_tgt_dev_sysfs_del(tgt_dev); +} + /* The caller must hold scst_mutex. */ int scst_acg_add_lun(struct scst_acg *acg, struct kobject *parent, struct scst_device *dev, uint64_t lun, unsigned int flags, @@ -4581,12 +4621,12 @@ int scst_acg_add_lun(struct scst_acg *acg, struct kobject *parent, } if (!(flags & SCST_ADD_LUN_CM)) { - res = scst_cm_on_add_lun(acg_dev, lun, &flags); + res = scst_lun_cm_add(acg_dev, lun, &flags); if (res != 0) goto out_free; } - res = scst_acg_dev_sysfs_create(acg_dev, parent); + res = scst_lun_sysfs_create(acg_dev, parent); if (res != 0) goto out_on_del; @@ -4606,7 +4646,7 @@ out: out_on_del: if (!(flags & SCST_ADD_LUN_CM)) - scst_cm_on_del_lun(acg_dev, false); + scst_lun_cm_del(acg_dev); out_free: list_for_each_entry_safe(tgt_dev, tt, &tmp_tgt_dev_list, @@ -4617,7 +4657,7 @@ out_free: scst_del_tgt_dev(tgt_dev); mutex_unlock(&sess->tgt_dev_list_mutex); - synchronize_rcu(); + scst_tgt_dev_grace_period(); scst_free_tgt_dev(tgt_dev); } @@ -4983,7 +5023,8 @@ static void scst_del_acg(struct scst_acg *acg) * * The caller must hold scst_mutex and activity must have been suspended. */ -static void scst_free_acg(struct scst_acg *acg) +SCST_KUNIT_VISIBLE +void scst_free_acg(struct scst_acg *acg) { struct scst_acg_dev *acg_dev, *acg_dev_tmp; struct scst_acn *acn, *acn_tmp; @@ -4992,6 +5033,8 @@ static void scst_free_acg(struct scst_acg *acg) struct scst_tgt_dev *tgt_dev, *tt; LIST_HEAD(tgt_dev_list); + SCST_KUNIT_REDIRECT(scst_free_acg, acg); + lockdep_assert_held(&scst_mutex); TRACE_DBG("Freeing acg %s/%s", tgt->tgt_name, acg->acg_name); @@ -5026,12 +5069,8 @@ static void scst_free_acg(struct scst_acg *acg) kobject_put(&tgt->tgt_kobj); } -struct scst_acg_release_work { - struct work_struct work; - struct scst_acg *acg; -}; - -static void scst_release_acg_work(struct work_struct *work) +SCST_KUNIT_VISIBLE +void scst_release_acg_work(struct work_struct *work) { struct scst_acg_release_work *release_work = container_of(work, typeof(*release_work), work); @@ -5116,7 +5155,6 @@ int scst_del_free_acg(struct scst_acg *acg, bool close_sessions) return 0; } - /* The activity supposed to be suspended and scst_mutex held */ struct scst_acg *scst_tgt_find_acg(struct scst_tgt *tgt, const char *name) { @@ -5135,7 +5173,6 @@ struct scst_acg *scst_tgt_find_acg(struct scst_tgt *tgt, const char *name) return acg_ret; } - /* scst_mutex supposed to be held */ static struct scst_tgt_dev *scst_find_shared_io_tgt_dev( struct scst_tgt_dev *tgt_dev) @@ -5468,8 +5505,9 @@ static __be16 scst_dif_ip_fn(const void *data, unsigned int len); * session. May be invoked from inside scst_check_reassign_sessions() which * means that sess->acg can be NULL. */ -static int scst_alloc_add_tgt_dev(struct scst_session *sess, - struct scst_acg_dev *acg_dev, struct scst_tgt_dev **out_tgt_dev) +SCST_KUNIT_VISIBLE +int scst_alloc_add_tgt_dev(struct scst_session *sess, + struct scst_acg_dev *acg_dev, struct scst_tgt_dev **out_tgt_dev) { int res = 0; struct scst_tgt_template *tgtt = sess->tgt->tgtt; @@ -5480,6 +5518,8 @@ static int scst_alloc_add_tgt_dev(struct scst_session *sess, int sl; uint8_t sense_buffer[SCST_STANDARD_SENSE_LEN]; + SCST_KUNIT_REDIRECT(scst_alloc_add_tgt_dev, sess, acg_dev, out_tgt_dev); + TRACE_ENTRY(); tgt_dev = kmem_cache_zalloc(scst_tgtd_cachep, GFP_KERNEL); @@ -5703,7 +5743,7 @@ static void scst_del_tgt_dev(struct scst_tgt_dev *tgt_dev) list_del_rcu(&tgt_dev->sess_tgt_dev_list_entry); - scst_tgt_dev_sysfs_del(tgt_dev); + scst_tgt_dev_sysfs_remove(tgt_dev); } /* @@ -5712,11 +5752,14 @@ static void scst_del_tgt_dev(struct scst_tgt_dev *tgt_dev) * if another thread could still be accessing @tgt_dev from inside an RCU * read-side critical section. */ -static void scst_free_tgt_dev(struct scst_tgt_dev *tgt_dev) +SCST_KUNIT_VISIBLE +void scst_free_tgt_dev(struct scst_tgt_dev *tgt_dev) { struct scst_tgt_template *tgtt = tgt_dev->tgtt; struct scst_device *dev = tgt_dev->dev; + SCST_KUNIT_REDIRECT(scst_free_tgt_dev, tgt_dev); + TRACE_ENTRY(); #ifdef CONFIG_SCST_EXTRACHECKS @@ -5750,7 +5793,8 @@ static void scst_free_tgt_dev(struct scst_tgt_dev *tgt_dev) } /* scst_mutex supposed to be held */ -static void scst_sess_free_tgt_devs(struct scst_session *sess) +SCST_KUNIT_VISIBLE +void scst_sess_free_tgt_devs(struct scst_session *sess) { int i; struct scst_tgt_dev *tgt_dev, *tt; @@ -5772,7 +5816,7 @@ static void scst_sess_free_tgt_devs(struct scst_session *sess) } mutex_unlock(&sess->tgt_dev_list_mutex); - synchronize_rcu(); + scst_tgt_dev_grace_period(); list_for_each_entry_safe(tgt_dev, tt, &tgt_dev_list, extra_tgt_dev_list_entry) scst_free_tgt_dev(tgt_dev); @@ -15022,7 +15066,8 @@ static inline void scst_ext_blocker_release(struct kref *kref) kfree(b); } -static inline void scst_ext_blocker_put(struct scst_ext_blocker *b) +SCST_KUNIT_VISIBLE_INLINE +void scst_ext_blocker_put(struct scst_ext_blocker *b) { kref_put(&b->refcount, scst_ext_blocker_release); } @@ -15070,7 +15115,8 @@ static void __scst_ext_blocking_done(struct scst_device *dev) TRACE_EXIT(); } -static void scst_ext_blocking_done_fn(struct work_struct *work) +SCST_KUNIT_VISIBLE +void scst_ext_blocking_done_fn(struct work_struct *work) { struct scst_device *dev = container_of(work, struct scst_device, ext_blockers_work); @@ -15155,6 +15201,15 @@ static void scst_dev_ext_block(struct scst_device *dev, bool block_stpg) TRACE_EXIT(); } +/* The caller holds dev_lock with BH disabled; the wait may drop the lock. */ +SCST_KUNIT_VISIBLE +int scst_wait_ext_block_dev(struct scst_device *dev, wait_queue_head_t *w) +{ + SCST_KUNIT_REDIRECT(scst_wait_ext_block_dev, dev, w); + return scst_wait_event_interruptible_lock_bh(*w, dev->on_dev_cmd_count == 0, + dev->dev_lock); +} + int scst_sync_ext_block_dev(struct scst_device *dev) { struct scst_ext_blocker *b; @@ -15192,7 +15247,7 @@ int scst_sync_ext_block_dev(struct scst_device *dev) b->ext_blocker_done_fn = scst_sync_ext_blocking_done; scst_ext_blocker_get(b); - res = scst_wait_event_interruptible_lock_bh(*w, dev->on_dev_cmd_count == 0, dev->dev_lock); + res = scst_wait_ext_block_dev(dev, w); spin_unlock_bh(&dev->dev_lock); diff --git a/scst/src/scst_lib_test.h b/scst/src/scst_lib_test.h new file mode 100644 index 000000000..390f70f62 --- /dev/null +++ b/scst/src/scst_lib_test.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef SCST_LIB_TEST_H +#define SCST_LIB_TEST_H + +#include "scst_test.h" + +#ifdef SCST_KUNIT_ENABLED + +void scst_del_free_acg_dev(struct scst_acg_dev *acg_dev, bool del_sysfs); +void scst_tgt_dev_grace_period(void); +int scst_lun_cm_add(struct scst_acg_dev *acg_dev, u64 lun, unsigned int *flags); +void scst_lun_cm_del(struct scst_acg_dev *acg_dev); +int scst_lun_sysfs_create(struct scst_acg_dev *acg_dev, struct kobject *parent); +void scst_tgt_dev_sysfs_remove(struct scst_tgt_dev *tgt_dev); +void scst_free_acg(struct scst_acg *acg); +void scst_release_acg_work(struct work_struct *work); +int scst_alloc_add_tgt_dev(struct scst_session *sess, + struct scst_acg_dev *acg_dev, struct scst_tgt_dev **out_tgt_dev); +void scst_free_tgt_dev(struct scst_tgt_dev *tgt_dev); +void scst_sess_free_tgt_devs(struct scst_session *sess); +void scst_ext_blocker_put(struct scst_ext_blocker *b); +void scst_ext_blocking_done_fn(struct work_struct *work); +int scst_wait_ext_block_dev(struct scst_device *dev, wait_queue_head_t *w); + +#endif /* SCST_KUNIT_ENABLED */ +#endif /* SCST_LIB_TEST_H */ diff --git a/scst/src/scst_priv.h b/scst/src/scst_priv.h index b24ead593..0fac9d21b 100644 --- a/scst/src/scst_priv.h +++ b/scst/src/scst_priv.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -344,6 +345,11 @@ int scst_alloc_device(gfp_t gfp_mask, int nodeid, struct scst_device **out_dev); void scst_free_device(struct scst_device *dev); bool scst_device_is_exported(struct scst_device *dev); +struct scst_acg_release_work { + struct work_struct work; + struct scst_acg *acg; +}; + int scst_alloc_add_acg(struct scst_tgt *tgt, const char *acg_name, bool tgt_acg, struct scst_acg **out_acg); int scst_del_free_acg(struct scst_acg *acg, bool close_sessions);