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 08/14] spi: spi-ti-qspi: add KUnit tests for transfer error finalization
Date: Mon, 2 Nov 2026 16:30:17 +0000 [thread overview]
Message-ID: <20261102163023.2574632-9-jctchan@gmail.com> (raw)
In-Reply-To: <20261102163023.2574632-1-jctchan@gmail.com>
From: Joseph CT Chan <josephctchan@google.com>
Add KUnit tests for ti_qspi_start_transfer_one() covering the transfer
error handling path.
On ARCH=um, emulate the TI QSPI controller MMIO via logic_iomem:
- ti_qspi_test: verify that queued and synchronous transfers hitting a
ti_qspi_poll_wc() write completion timeout complete with -ETIMEDOUT,
that QSPI_INVAL is written to end the frame, and that subsequent
transfers succeed cleanly once QSPI_SPI_STATUS_REG reports QSPI_WC.
Assisted-by: Claude, Gemini
Signed-off-by: Joseph CT Chan <josephctchan@google.com>
---
drivers/spi/Kconfig | 12 ++
drivers/spi/Makefile | 1 +
drivers/spi/tests/.kunitconfig | 4 +
drivers/spi/tests/Makefile | 1 +
drivers/spi/tests/spi-ti-qspi-kunit.c | 225 ++++++++++++++++++++++++++
5 files changed, 243 insertions(+)
create mode 100644 drivers/spi/tests/spi-ti-qspi-kunit.c
diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
index a7b343d124a9..feb9265b7d0f 100644
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -1258,6 +1258,18 @@ config SPI_TI_QSPI
This device supports single, dual and quad read support, while
it only supports single write mode.
+config SPI_TI_QSPI_KUNIT_TEST
+ bool "KUnit test for TI QSPI controller driver" if !KUNIT_ALL_TESTS
+ depends on KUNIT=y && SPI_TI_QSPI=y && INDIRECT_IOMEM && !HAVE_CLK
+ default KUNIT_ALL_TESTS
+ help
+ This option builds the KUnit test suite for the TI QSPI controller
+ driver. It verifies that transfer timeout errors are properly
+ finalized with -ETIMEDOUT and that QSPI_INVAL is written to the
+ command register.
+
+ If unsure, say N.
+
config SPI_TLE62X0
tristate "Infineon TLE62X0 (for power switching)"
depends on SYSFS
diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile
index d4a806b46b2c..24497d3936f6 100644
--- a/drivers/spi/Makefile
+++ b/drivers/spi/Makefile
@@ -15,6 +15,7 @@ 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/
+obj-$(CONFIG_SPI_TI_QSPI_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
index f781425d258e..354bafc42c2b 100644
--- a/drivers/spi/tests/.kunitconfig
+++ b/drivers/spi/tests/.kunitconfig
@@ -1,4 +1,8 @@
CONFIG_KUNIT=y
+CONFIG_KUNIT_UML_PCI=y
CONFIG_SPI=y
CONFIG_SPI_MASTER=y
CONFIG_SPI_CORE_KUNIT_TEST=y
+CONFIG_COMPILE_TEST=y
+CONFIG_SPI_TI_QSPI=y
+CONFIG_SPI_TI_QSPI_KUNIT_TEST=y
diff --git a/drivers/spi/tests/Makefile b/drivers/spi/tests/Makefile
index 6dbf53643619..a5a8fc9435d4 100644
--- a/drivers/spi/tests/Makefile
+++ b/drivers/spi/tests/Makefile
@@ -2,3 +2,4 @@
obj-$(CONFIG_SPI_DMA_KUNIT_TEST) += spi-dma-kunit.o
obj-$(CONFIG_SPI_CORE_KUNIT_TEST) += spi-core-kunit.o
+obj-$(CONFIG_SPI_TI_QSPI_KUNIT_TEST) += spi-ti-qspi-kunit.o
diff --git a/drivers/spi/tests/spi-ti-qspi-kunit.c b/drivers/spi/tests/spi-ti-qspi-kunit.c
new file mode 100644
index 000000000000..71ec2464a34a
--- /dev/null
+++ b/drivers/spi/tests/spi-ti-qspi-kunit.c
@@ -0,0 +1,225 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <kunit/test.h>
+#include <linux/completion.h>
+#include <linux/jiffies.h>
+#include <linux/kthread.h>
+#include <linux/logic_iomem.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/spi/spi.h>
+
+#define QSPI_SPI_CMD_REG 0x48
+#define QSPI_SPI_STATUS_REG 0x4c
+#define QSPI_WC BIT(1)
+#define QSPI_INVAL (4 << 16)
+
+struct ti_qspi_test_ctx {
+ struct platform_device *pdev;
+ struct spi_device *spi;
+ struct spi_message msg;
+ struct spi_transfer xfer;
+ struct completion done;
+ u8 tx[4];
+};
+
+static bool g_err, g_inval, g_region_registered;
+static struct resource g_iomem_res = DEFINE_RES_MEM(0x30000000, 0x2000);
+
+static unsigned long ti_qspi_io_r(void *p, unsigned int off, int sz)
+{
+ return (off == QSPI_SPI_STATUS_REG && !g_err) ? QSPI_WC : 0;
+}
+
+static void ti_qspi_io_w(void *p, unsigned int off, int sz, unsigned long val)
+{
+ if (off == QSPI_SPI_CMD_REG && (val & QSPI_INVAL))
+ g_inval = true;
+}
+
+static const struct logic_iomem_ops g_iops = {
+ .read = ti_qspi_io_r,
+ .write = ti_qspi_io_w,
+};
+
+static long ti_qspi_map(unsigned long o, size_t s, const struct logic_iomem_ops **ops, void **p)
+{
+ *ops = &g_iops;
+ return 0;
+}
+
+static const struct logic_iomem_region_ops g_rops = {
+ .map = ti_qspi_map,
+};
+
+static int ti_qspi_find_ctlr(struct device *d, void *data)
+{
+ *(struct spi_controller **)data = container_of(d, struct spi_controller, dev);
+ return 1;
+}
+
+static void kunit_done(void *p)
+{
+ complete(p);
+}
+
+static int ti_qspi_init(struct kunit *t)
+{
+ const struct resource res[] = {
+ DEFINE_RES_MEM(0x30000000, 0x1000),
+ DEFINE_RES_MEM(0x30001000, 0x1000),
+ DEFINE_RES_IRQ(1),
+ };
+ struct ti_qspi_test_ctx *ctx;
+ struct spi_board_info info = { .modalias = "ti-qspi-test" };
+ struct spi_controller *ctlr = NULL;
+ struct platform_device *pdev;
+
+ g_err = false;
+ g_inval = false;
+ ctx = kunit_kzalloc(t, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(t, ctx);
+ t->priv = ctx;
+
+ pdev = platform_device_register_simple("ti-qspi", -1, res, ARRAY_SIZE(res));
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(t, pdev);
+ ctx->pdev = pdev;
+
+ device_for_each_child(&pdev->dev, &ctlr, ti_qspi_find_ctlr);
+ if (!ctlr) {
+ platform_device_unregister(pdev);
+ ctx->pdev = NULL;
+ KUNIT_FAIL_AND_ABORT(t, "TI QSPI controller device not found");
+ }
+
+ ctlr->max_speed_hz = 50000000;
+ ctx->spi = spi_new_device(ctlr, &info);
+ if (!ctx->spi) {
+ pm_runtime_get_noresume(&pdev->dev);
+ platform_device_unregister(pdev);
+ ctx->pdev = NULL;
+ KUNIT_FAIL_AND_ABORT(t, "Failed to create spi child device");
+ }
+
+ init_completion(&ctx->done);
+ memcpy(ctx->tx, "ABCD", sizeof(ctx->tx));
+ ctx->xfer.tx_buf = ctx->tx;
+ ctx->xfer.len = sizeof(ctx->tx);
+ spi_message_init_with_transfers(&ctx->msg, &ctx->xfer, 1);
+ return 0;
+}
+
+static void ti_qspi_exit(struct kunit *t)
+{
+ struct ti_qspi_test_ctx *ctx = t->priv;
+
+ if (!ctx)
+ return;
+ if (ctx->spi)
+ spi_unregister_device(ctx->spi);
+ if (ctx->pdev) {
+ pm_runtime_get_noresume(&ctx->pdev->dev);
+ platform_device_unregister(ctx->pdev);
+ }
+}
+
+static void check_sync(struct kunit *t, struct spi_device *s, struct spi_message *m, int exp)
+{
+ g_inval = false;
+ /*
+ * Good situation (patched): ti_qspi_start_transfer_one() sets
+ * m->status = status (exp, either -ETIMEDOUT on error or 0 on
+ * recovery) before calling spi_finalize_current_message(host), and
+ * spi_sync() returns m->status.
+ * Bad situation (unpatched): on transfer error (exp == -ETIMEDOUT),
+ * ti_qspi_start_transfer_one() returns early without updating
+ * m->status or finalizing the message, leaving m->status stuck at
+ * -EINPROGRESS (-115), which __spi_sync() returns on the noqueue path.
+ */
+ KUNIT_EXPECT_EQ(t, spi_sync(s, m), exp);
+ KUNIT_EXPECT_EQ(t, m->status, exp);
+ /*
+ * Good situation (patched): spi_sync() calls
+ * spi_maybe_optimize_message() at entry (setting m->optimized = true
+ * while the transfer is in flight), and ti_qspi_start_transfer_one()
+ * calls spi_finalize_current_message(host) on both success and error
+ * paths, which runs spi_maybe_unoptimize_message() and resets
+ * m->optimized = false before spi_sync() returns.
+ * Bad situation (unpatched): when qspi_transfer_msg() fails with
+ * -ETIMEDOUT, ti_qspi_start_transfer_one() returns early without
+ * calling spi_finalize_current_message(host), so
+ * spi_maybe_unoptimize_message() never runs and m->optimized remains
+ * stuck at true after spi_sync() returns.
+ */
+ KUNIT_EXPECT_FALSE(t, m->optimized);
+ /*
+ * Good situation (patched): ti_qspi_start_transfer_one() writes
+ * QSPI_INVAL to QSPI_SPI_CMD_REG (g_inval = true) in its epilogue to
+ * terminate the hardware frame even when a transfer fails.
+ * Bad situation (unpatched): the early return on error skips writing
+ * QSPI_INVAL, leaving g_inval == false.
+ */
+ KUNIT_EXPECT_TRUE(t, g_inval);
+}
+
+static void ti_qspi_test(struct kunit *t)
+{
+ struct ti_qspi_test_ctx *ctx = t->priv;
+ unsigned long timeout;
+
+ g_err = true;
+ check_sync(t, ctx->spi, &ctx->msg, -ETIMEDOUT);
+
+ /*
+ * Exercise the queued kworker path via spi_async() with g_err = true:
+ * - Good situation (patched): ti_qspi_start_transfer_one() finalizes
+ * the failed message, completing ctx->done, setting
+ * ctx->msg.status = -ETIMEDOUT, resetting ctx->msg.optimized = false,
+ * and writing QSPI_INVAL (g_inval = true).
+ * - Bad situation (unpatched): skipping spi_finalize_current_message()
+ * never signals ctx->done (causing wait_for_completion_timeout() to
+ * time out) and leaves ctx->msg.optimized == true.
+ */
+ ctx->msg.complete = kunit_done;
+ ctx->msg.context = &ctx->done;
+ g_inval = false;
+ reinit_completion(&ctx->done);
+ KUNIT_ASSERT_EQ(t, spi_async(ctx->spi, &ctx->msg), 0);
+
+ timeout = wait_for_completion_timeout(&ctx->done, msecs_to_jiffies(4000));
+ kthread_flush_worker(ctx->spi->controller->kworker);
+ if (!timeout)
+ KUNIT_FAIL_AND_ABORT(t, "ti-qspi async transfer timed out");
+
+ KUNIT_EXPECT_EQ(t, ctx->msg.status, -ETIMEDOUT);
+ KUNIT_EXPECT_FALSE(t, ctx->msg.optimized);
+ KUNIT_EXPECT_TRUE(t, g_inval);
+ /* Clear the injected fault and verify a subsequent transfer succeeds. */
+ g_err = false;
+ check_sync(t, ctx->spi, &ctx->msg, 0);
+}
+
+static int ti_qspi_suite_init(struct kunit_suite *s)
+{
+ int ret = g_region_registered ? 0 : logic_iomem_add_region(&g_iomem_res, &g_rops);
+
+ g_region_registered = !ret;
+ return ret;
+}
+
+static struct kunit_case ti_qspi_test_cases[] = {
+ KUNIT_CASE_SLOW(ti_qspi_test),
+ {}
+};
+
+static struct kunit_suite ti_qspi_test_suite = {
+ .name = "spi_ti_qspi",
+ .suite_init = ti_qspi_suite_init,
+ .init = ti_qspi_init,
+ .exit = ti_qspi_exit,
+ .test_cases = ti_qspi_test_cases,
+};
+
+kunit_test_suite(ti_qspi_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for TI QSPI controller driver");
+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 ` [PATCH 07/14] spi: add KUnit tests for message optimize state on queue errors Joseph CT Chan
2026-11-02 17:29 ` David Lechner
2026-11-03 22:41 ` Joseph CT Chan
2026-11-02 16:30 ` Joseph CT Chan [this message]
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-9-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