From: Joseph CT Chan <jctchan@gmail.com>
To: Mark Brown <broonie@kernel.org>
Cc: Bart Van Assche <bvanassche@acm.org>,
David Lechner <dlechner@baylibre.com>,
Conor Dooley <conor.dooley@microchip.com>,
linux-spi@vger.kernel.org,
Joseph CT Chan <josephctchan@google.com>
Subject: [PATCH 07/14] spi: add KUnit tests for message optimize state on queue errors
Date: Mon, 2 Nov 2026 16:30:16 +0000 [thread overview]
Message-ID: <20261102163023.2574632-8-jctchan@gmail.com> (raw)
In-Reply-To: <20261102163023.2574632-1-jctchan@gmail.com>
From: Joseph CT Chan <josephctchan@google.com>
spi_async() and spi_sync() optimize a message before they queue it. If
queuing fails, for example because another device holds spi_bus_lock()
on the controller, because mem_ops rejects the message, or because the
controller is suspended, the message is never finalized, and finalizing
is where the core normally unoptimizes it.
Add KUnit tests with a fake controller that counts its optimize_message()
and unoptimize_message() calls, splits transfers across a 2-byte boundary
to exercise msg.resources, and tracks msg.opt_state. The tests cover both
async and sync queue errors, verify split transfers are cleaned up and
opt_state cleared, check that a message optimized by the peripheral
driver is left alone on error, and check that a successful spi_sync()
optimizes and unoptimizes the message exactly once on both queued and
unqueued paths.
Without "spi: unoptimize message when queuing it fails" the queue error
cases fail with unoptimize_count == 0, msg.resources leaked, and
msg.optimized still true; the other cases pass.
Tested with KUnit on ARCH=um: all cases pass with the fixes in this series
applied. Builds with W=1 on x86_64.
Assisted-by: Claude, Gemini
Signed-off-by: Joseph CT Chan <josephctchan@google.com>
---
drivers/spi/Kconfig | 13 ++
drivers/spi/Makefile | 1 +
drivers/spi/tests/.kunitconfig | 4 +
drivers/spi/tests/Makefile | 1 +
drivers/spi/tests/spi-core-kunit.c | 308 +++++++++++++++++++++++++++++
5 files changed, 327 insertions(+)
create mode 100644 drivers/spi/tests/.kunitconfig
create mode 100644 drivers/spi/tests/spi-core-kunit.c
diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
index 49088e5886d3..a7b343d124a9 100644
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -1398,6 +1398,19 @@ config SPI_DMA_KUNIT_TEST
If unsure say N.
+config SPI_CORE_KUNIT_TEST
+ tristate "KUnit tests for SPI core message optimize state" if !KUNIT_ALL_TESTS
+ depends on KUNIT
+ default KUNIT_ALL_TESTS
+ help
+ Build KUnit tests for the SPI core message optimize state. The tests
+ register a fake controller and check that spi_async() and spi_sync()
+ undo the optimization of a message they fail to queue, that a
+ message optimized by the peripheral driver is left alone, and that a
+ successful transfer optimizes and unoptimizes exactly once.
+
+ If unsure say N.
+
config SPI_LOOPBACK_TEST
tristate "spi loopback test framework support"
depends on m
diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile
index b493f50b93e3..d4a806b46b2c 100644
--- a/drivers/spi/Makefile
+++ b/drivers/spi/Makefile
@@ -14,6 +14,7 @@ obj-$(CONFIG_SPI_OFFLOAD) += spi-offload.o
obj-$(CONFIG_SPI_SPIDEV) += spidev.o
obj-$(CONFIG_SPI_LOOPBACK_TEST) += spi-loopback-test.o
obj-$(CONFIG_SPI_DMA_KUNIT_TEST) += tests/
+obj-$(CONFIG_SPI_CORE_KUNIT_TEST) += tests/
# SPI master controller drivers (bus)
obj-$(CONFIG_SPI_AIROHA_SNFI) += spi-airoha-snfi.o
diff --git a/drivers/spi/tests/.kunitconfig b/drivers/spi/tests/.kunitconfig
new file mode 100644
index 000000000000..f781425d258e
--- /dev/null
+++ b/drivers/spi/tests/.kunitconfig
@@ -0,0 +1,4 @@
+CONFIG_KUNIT=y
+CONFIG_SPI=y
+CONFIG_SPI_MASTER=y
+CONFIG_SPI_CORE_KUNIT_TEST=y
diff --git a/drivers/spi/tests/Makefile b/drivers/spi/tests/Makefile
index 26689e0cb28a..6dbf53643619 100644
--- a/drivers/spi/tests/Makefile
+++ b/drivers/spi/tests/Makefile
@@ -1,3 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_SPI_DMA_KUNIT_TEST) += spi-dma-kunit.o
+obj-$(CONFIG_SPI_CORE_KUNIT_TEST) += spi-core-kunit.o
diff --git a/drivers/spi/tests/spi-core-kunit.c b/drivers/spi/tests/spi-core-kunit.c
new file mode 100644
index 000000000000..e1578f2f8eb3
--- /dev/null
+++ b/drivers/spi/tests/spi-core-kunit.c
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: GPL-2.0
+// KUnit tests for the SPI core message optimize state.
+#include <kunit/device.h>
+#include <kunit/test.h>
+#include <linux/completion.h>
+#include <linux/errno.h>
+#include <linux/jiffies.h>
+#include <linux/spi/spi.h>
+#include <linux/spi/spi-mem.h>
+
+struct spi_core_test_ctx {
+ struct spi_controller *ctlr;
+ struct spi_device *spi;
+ int optimize_count, unoptimize_count;
+ bool bad_opt_state;
+ struct spi_transfer xfer;
+ struct spi_message msg;
+ struct completion done;
+ u8 tx[4];
+};
+
+static int spi_core_test_optimize(struct spi_message *msg)
+{
+ struct spi_core_test_ctx *ctx = spi_controller_get_devdata(msg->spi->controller);
+
+ ctx->optimize_count++;
+ msg->opt_state = ctx;
+ return 0;
+}
+
+static int spi_core_test_unoptimize(struct spi_message *msg)
+{
+ struct spi_core_test_ctx *ctx = spi_controller_get_devdata(msg->spi->controller);
+
+ ctx->unoptimize_count++;
+ if (msg->opt_state != ctx)
+ ctx->bad_opt_state = true;
+ return 0;
+}
+
+static int spi_core_test_xfer_one(struct spi_controller *ctlr, struct spi_message *msg)
+{
+ struct spi_transfer *xfer;
+
+ list_for_each_entry(xfer, &msg->transfers, transfer_list)
+ msg->actual_length += xfer->len;
+ msg->status = 0;
+ spi_finalize_current_message(ctlr);
+ return 0;
+}
+
+static int spi_core_test_exec_op(struct spi_mem *mem, const struct spi_mem_op *op)
+{
+ return -EOPNOTSUPP;
+}
+
+static const struct spi_controller_mem_ops spi_core_test_mem_ops = {
+ .exec_op = spi_core_test_exec_op,
+};
+
+static size_t spi_core_test_max_transfer_size(struct spi_device *spi)
+{
+ return 2;
+}
+
+static void kunit_done(void *p)
+{
+ complete(p);
+}
+
+static struct spi_core_test_ctx *spi_core_test_ctx_new(struct kunit *test, bool mem_only)
+{
+ struct spi_board_info info = { .modalias = "spi-core-kunit", .max_speed_hz = 1000000 };
+ struct device *dev = kunit_device_register(test, "spi-core-kunit");
+ struct spi_controller *ctlr;
+ struct spi_core_test_ctx *ctx;
+
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev);
+ ctlr = devm_spi_alloc_host(dev, sizeof(struct spi_core_test_ctx));
+ KUNIT_ASSERT_NOT_NULL(test, ctlr);
+ ctx = spi_controller_get_devdata(ctlr);
+
+ ctx->ctlr = ctlr;
+ ctlr->bus_num = -1;
+ ctlr->num_chipselect = 1;
+ ctlr->max_transfer_size = spi_core_test_max_transfer_size;
+ ctlr->optimize_message = spi_core_test_optimize;
+ ctlr->unoptimize_message = spi_core_test_unoptimize;
+ ctlr->mem_ops = mem_only ? &spi_core_test_mem_ops : NULL;
+ ctlr->transfer_one_message = mem_only ? NULL : spi_core_test_xfer_one;
+ KUNIT_ASSERT_EQ(test, 0, devm_spi_register_controller(dev, ctlr));
+ ctx->spi = spi_new_device(ctlr, &info);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->spi);
+ init_completion(&ctx->done);
+ /*
+ * Pair a 4-byte dummy TX buffer with max_transfer_size = 2 so
+ * __spi_optimize_message() splits the 1 transfer into two 2-byte
+ * transfers tracked in msg.resources, allowing check_state() to
+ * verify that spi_res_release() undoes transfer splitting.
+ */
+ ctx->xfer.tx_buf = ctx->tx;
+ ctx->xfer.len = sizeof(ctx->tx);
+ spi_message_init_with_transfers(&ctx->msg, &ctx->xfer, 1);
+ ctx->msg.complete = kunit_done;
+ ctx->msg.context = &ctx->done;
+ return ctx;
+}
+
+enum spi_core_err_mode {
+ ERR_NONE,
+ ERR_BUS_LOCKED,
+ ERR_MEM_OPS,
+ ERR_SUSPEND,
+};
+
+struct spi_core_err_param {
+ const char *name;
+ enum spi_core_err_mode mode;
+ int exp;
+ bool use_sync;
+ bool pre_opt;
+ bool must_async;
+};
+
+static const struct spi_core_err_param spi_core_err_cases[] = {
+ { "bus_locked_async", ERR_BUS_LOCKED, -EBUSY },
+ { "suspend_async", ERR_SUSPEND, -ESHUTDOWN },
+ { "mem_ops_sync", ERR_MEM_OPS, -ENOTSUPP, true },
+ { "pre_opt_locked", ERR_BUS_LOCKED, -EBUSY, false, true },
+ { "pre_opt_mem_ops_sync", ERR_MEM_OPS, -ENOTSUPP, true, true },
+ { "sync_success", ERR_NONE, 0, true },
+ { "sync_success_queued", ERR_NONE, 0, true, false, true },
+};
+
+static void spi_core_err_desc(const struct spi_core_err_param *t, char *desc)
+{
+ strscpy(desc, t->name, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(spi_core_err, spi_core_err_cases, spi_core_err_desc);
+
+/*
+ * Verify cumulative controller optimize/unoptimize callback counts and the
+ * current optimization state of ctx->msg:
+ * @opt: expected cumulative ctlr->optimize_message() count
+ * (ctx->optimize_count).
+ * @unopt: expected cumulative ctlr->unoptimize_message() count
+ * (ctx->unoptimize_count).
+ * @state: expected current optimization state of ctx->msg:
+ * - true: currently optimized (msg.optimized == true,
+ * msg.opt_state == ctx, split-transfer resource present in
+ * msg.resources, and 2 split transfers in msg.transfers).
+ * - false: unoptimized (msg.optimized == false, msg.opt_state == NULL,
+ * msg.resources is empty, and original 1 transfer restored).
+ */
+static void check_state(struct kunit *test, struct spi_core_test_ctx *ctx,
+ int opt, int unopt, bool state)
+{
+ KUNIT_EXPECT_EQ(test, ctx->optimize_count, opt);
+ KUNIT_EXPECT_EQ(test, ctx->unoptimize_count, unopt);
+ KUNIT_EXPECT_FALSE(test, ctx->bad_opt_state);
+ KUNIT_EXPECT_EQ(test, ctx->msg.optimized, state);
+ KUNIT_EXPECT_EQ(test, !ctx->msg.opt_state, !state);
+ KUNIT_EXPECT_EQ(test, list_empty(&ctx->msg.resources), !state);
+ KUNIT_EXPECT_EQ(test, list_is_singular(&ctx->msg.transfers), !state);
+}
+
+static int spi_core_test_send_locked(struct spi_device *spi, struct spi_message *msg)
+{
+ int ret;
+
+ spi_bus_lock(spi->controller);
+ ret = spi_async(spi, msg);
+ spi_bus_unlock(spi->controller);
+
+ return ret;
+}
+
+static void spi_core_queue_error_test(struct kunit *test)
+{
+ const struct spi_core_err_param *p = test->param_value;
+ struct spi_core_test_ctx *ctx = spi_core_test_ctx_new(test, p->mode == ERR_MEM_OPS);
+ int exp = p->exp, ret;
+
+ ctx->ctlr->must_async = p->must_async;
+ if (p->pre_opt)
+ KUNIT_ASSERT_EQ(test, spi_optimize_message(ctx->spi, &ctx->msg), 0);
+ if (p->mode == ERR_SUSPEND)
+ KUNIT_ASSERT_EQ(test, 0, spi_controller_suspend(ctx->ctlr));
+
+ if (p->mode == ERR_BUS_LOCKED)
+ ret = spi_core_test_send_locked(ctx->spi, &ctx->msg);
+ else if (p->use_sync)
+ ret = spi_sync(ctx->spi, &ctx->msg);
+ else
+ ret = spi_async(ctx->spi, &ctx->msg);
+
+ if (p->mode == ERR_SUSPEND)
+ KUNIT_ASSERT_EQ(test, 0, spi_controller_resume(ctx->ctlr));
+
+ KUNIT_EXPECT_EQ(test, ret, exp);
+ if (!exp) {
+ KUNIT_EXPECT_EQ(test, ctx->msg.actual_length, sizeof(ctx->tx));
+ /*
+ * Normal error-free situation for the ERR_NONE control cases
+ * (sync_success on the noqueue fast-path and
+ * sync_success_queued on the kthread worker path):
+ * - Expected: (opt = 1, unopt = 1, state = false).
+ * Meaning: spi_sync() optimized ctx->msg once before the
+ * transfer (opt = 1) and unoptimized it once on completion
+ * (unopt = 1), leaving ctx->msg unoptimized (state = false:
+ * msg.resources is empty and the single 4-byte transfer is
+ * restored).
+ */
+ check_state(test, ctx, 1, 1, false);
+ return;
+ }
+
+ /*
+ * Check message optimization state immediately after queuing fails
+ * across the 5 error test cases:
+ *
+ * Group A: Core-optimized queue failures (p->pre_opt == false)
+ * 1. bus_locked_async: spi_async() fails with -EBUSY while the bus
+ * lock is held by another caller.
+ * 2. suspend_async: spi_async() fails with -ESHUTDOWN while the
+ * controller queue is stopped by spi_controller_suspend().
+ * 3. mem_ops_sync: spi_sync() fails with -ENOTSUPP on a controller
+ * that only implements mem_ops (ctlr->transfer == NULL).
+ * - Expected (patched): (opt = 1, unopt = 1, state = false).
+ * Meaning: spi_async() / __spi_sync() optimized ctx->msg (opt = 1)
+ * before queuing failed, and spi_maybe_unoptimize_message() on the
+ * error path immediately unoptimized it (unopt = 1), restoring
+ * ctx->msg to the clean unoptimized state (state = false:
+ * msg.optimized == false, msg.opt_state == NULL, split transfers
+ * in msg.resources freed, and original 1 transfer restored).
+ * - Broken behavior watched for (unpatched): (opt = 1, unopt = 0,
+ * state = true).
+ * Meaning: spi_async() / __spi_sync() returned the queue error
+ * without calling spi_maybe_unoptimize_message(), leaving
+ * unopt = 0 and ctx->msg stuck in the optimized state
+ * (state = true: msg.optimized == true, msg.opt_state == ctx, and
+ * the 2 split transfers leaked on msg.resources).
+ *
+ * Group B: Caller pre-optimized queue failures (p->pre_opt == true)
+ * 4. pre_opt_locked: caller pre-optimized ctx->msg via
+ * spi_optimize_message(), then spi_async() fails with -EBUSY.
+ * 5. pre_opt_mem_ops_sync: caller pre-optimized ctx->msg via
+ * spi_optimize_message(), then spi_sync() fails with -ENOTSUPP.
+ * - Expected (patched): (opt = 1, unopt = 0, state = true).
+ * Meaning: the caller explicitly pre-optimized ctx->msg (opt = 1,
+ * msg.pre_optimized == true) before submission, so
+ * spi_maybe_unoptimize_message() on the queue error path must
+ * preserve the caller's pre-optimization (unopt = 0, state = true)
+ * until the caller explicitly calls spi_unoptimize_message() below.
+ * - Broken behavior watched for (if error cleanup unconditionally
+ * called __spi_unoptimize_message() instead of
+ * spi_maybe_unoptimize_message()): (opt = 1, unopt = 1,
+ * state = false).
+ * Meaning: the error path would prematurely destroy the caller's
+ * pre-optimized state, causing a double-unoptimize when the caller
+ * later calls spi_unoptimize_message().
+ */
+ check_state(test, ctx, 1, p->pre_opt ? 0 : 1, p->pre_opt);
+ if (p->pre_opt)
+ spi_unoptimize_message(&ctx->msg);
+ if (p->mode == ERR_MEM_OPS)
+ return;
+
+ reinit_completion(&ctx->done);
+ KUNIT_ASSERT_EQ(test, spi_async(ctx->spi, &ctx->msg), 0);
+ KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&ctx->done, msecs_to_jiffies(1000)), 0);
+ KUNIT_EXPECT_EQ(test, ctx->msg.status, 0);
+ /*
+ * Normal error-free situation after retry for the retryable error
+ * cases (bus_locked_async after spi_bus_unlock(), suspend_async after
+ * spi_controller_resume(), and pre_opt_locked after
+ * spi_unoptimize_message() + spi_bus_unlock()):
+ * - Expected (patched): (opt = 2, unopt = 2, state = false).
+ * Meaning: resubmitting the same ctx->msg after the transient error
+ * clears optimizes it a second time (opt = 2) and unoptimizes it on
+ * completion (unopt = 2), leaving ctx->msg unoptimized
+ * (state = false).
+ * - Broken behavior watched for (unpatched): (opt = 2, unopt = 1,
+ * state = false).
+ * Meaning: in bus_locked_async and suspend_async, the first failed
+ * attempt leaked ctx->msg in the optimized state (opt = 1,
+ * unopt = 0, state = true), so retrying the leaked message
+ * re-optimizes an already-optimized message (opt = 2) and only
+ * unoptimizes once when the retry completes (unopt = 1).
+ */
+ check_state(test, ctx, 2, 2, false);
+}
+
+static struct kunit_case spi_core_optimize_cases[] = {
+ KUNIT_CASE_PARAM(spi_core_queue_error_test, spi_core_err_gen_params),
+ {}
+};
+
+static struct kunit_suite spi_core_optimize_suite = {
+ .name = "spi_core_optimize",
+ .test_cases = spi_core_optimize_cases,
+};
+
+kunit_test_suite(spi_core_optimize_suite);
+
+MODULE_DESCRIPTION("KUnit tests for SPI core message optimize state");
+MODULE_LICENSE("GPL");
--
2.56.0.317.g454f8302e1-goog
next prev parent reply other threads:[~2026-11-02 16:33 UTC|newest]
Thread overview: 20+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-02 16:30 [PATCH 00/14] spi: spi-mux, core, ti-qspi: error handling, delay fixes, KUnit tests, and lock context annotations Joseph CT Chan
2026-11-02 16:30 ` [PATCH 01/14] spi: unoptimize message when queuing it fails Joseph CT Chan
2026-11-02 17:00 ` David Lechner
2026-11-02 16:30 ` [PATCH 02/14] spi: spi-mux: release mux when parent setup fails Joseph CT Chan
2026-11-02 16:30 ` [PATCH 03/14] spi: spi-mux: re-apply settings after child calls spi_setup() Joseph CT Chan
2026-11-02 16:30 ` [PATCH 04/14] spi: spi-mux: finalize message when forwarding fails Joseph CT Chan
2026-11-02 16:30 ` [PATCH 05/14] spi: spi-ti-qspi: finalize message on transfer error Joseph CT Chan
2026-11-02 16:30 ` [PATCH 06/14] spi: spi-mux: pass child CS and word delays to the parent Joseph CT Chan
2026-11-02 16:30 ` Joseph CT Chan [this message]
2026-11-02 17:29 ` [PATCH 07/14] spi: add KUnit tests for message optimize state on queue errors David Lechner
2026-11-03 22:41 ` Joseph CT Chan
2026-11-02 16:30 ` [PATCH 08/14] spi: spi-ti-qspi: add KUnit tests for transfer error finalization Joseph CT Chan
2026-11-02 16:30 ` [PATCH 09/14] spi: spi-mux: add KUnit tests for forwarding errors and settings Joseph CT Chan
2026-11-02 16:30 ` [PATCH 10/14] spi: Split spi_write_then_read() Joseph CT Chan
2026-11-02 16:30 ` [PATCH 11/14] spi: Add lock context annotations for bus_lock_mutex Joseph CT Chan
2026-11-02 16:30 ` [PATCH 12/14] spi: spi-mem: Add lock context annotations Joseph CT Chan
2026-11-02 16:30 ` [PATCH 13/14] spi: microchip-core-qspi: " Joseph CT Chan
2026-11-02 16:30 ` [PATCH 14/14] spi: Enable lock context analysis Joseph CT Chan
2026-11-02 17:07 ` [PATCH 00/14] spi: spi-mux, core, ti-qspi: error handling, delay fixes, KUnit tests, and lock context annotations David Lechner
2026-11-03 22:41 ` Joseph CT Chan
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261102163023.2574632-8-jctchan@gmail.com \
--to=jctchan@gmail.com \
--cc=broonie@kernel.org \
--cc=bvanassche@acm.org \
--cc=conor.dooley@microchip.com \
--cc=dlechner@baylibre.com \
--cc=josephctchan@google.com \
--cc=linux-spi@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox