Linux SPI subsystem development
 help / color / mirror / Atom feed
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 09/14] spi: spi-mux: add KUnit tests for forwarding errors and settings
Date: Mon,  2 Nov 2026 16:30:18 +0000	[thread overview]
Message-ID: <20261102163023.2574632-10-jctchan@gmail.com> (raw)
In-Reply-To: <20261102163023.2574632-1-jctchan@gmail.com>

From: Joseph CT Chan <josephctchan@google.com>

Add a KUnit test suite (spi_mux) with a Device Tree overlay that
instantiates a fake mux controller, a fake parent SPI controller, the
real spi-mux driver, and two child SPI devices.

The suite covers three areas:
- spi_mux_error_test: forwarding errors (locked parent bus, invalid
  transfer length, mux select failure, and parent spi_setup() failure),
  verifying that the message is finalized, the mux is released, the
  caller's message fields and optimize state are restored, and
  subsequent transfers succeed.
- spi_mux_cs_delay_test: CS setup, hold, and inactive delay merging
  across ns, us, and SCK units, including zero-value SCK delays.
- spi_mux_settings_lifecycle_test: re-applying child mode, speed,
  bits_per_word, and delay changes after spi_setup(), switching
  between children with different delays, and restoring the spi-mux
  device's own delays on driver unbind.

Assisted-by: Claude, Gemini
Signed-off-by: Joseph CT Chan <josephctchan@google.com>
---
 drivers/spi/Kconfig                  |  14 +
 drivers/spi/Makefile                 |   1 +
 drivers/spi/tests/.kunitconfig       |   4 +
 drivers/spi/tests/Makefile           |   2 +
 drivers/spi/tests/kunit_spi_mux.dtso |  42 ++
 drivers/spi/tests/spi-mux-kunit.c    | 581 +++++++++++++++++++++++++++
 6 files changed, 644 insertions(+)
 create mode 100644 drivers/spi/tests/kunit_spi_mux.dtso
 create mode 100644 drivers/spi/tests/spi-mux-kunit.c

diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
index feb9265b7d0f..9a36bf9cf94f 100644
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -1386,6 +1386,20 @@ config SPI_MUX
 	  to be chip selects on this controller. It is still necessary to
 	  select one or more specific mux-controller drivers.
 
+config SPI_MUX_KUNIT_TEST
+	tristate "KUnit tests for spi-mux" if !KUNIT_ALL_TESTS
+	depends on KUNIT && SPI_MUX && OF_OVERLAY
+	select DTC
+	default KUNIT_ALL_TESTS
+	help
+	  Build KUnit tests for the SPI multiplexer driver. A device tree
+	  overlay adds a fake parent controller, a test mux chip and an
+	  spi-mux device with two children. The tests check that a message
+	  which cannot be forwarded is completed with an error and releases
+	  the mux, and that child settings and delays reach the parent.
+
+	  If unsure say N.
+
 #
 # There are lots of SPI device types, with sensors and memory
 # being probably the most widely used ones.
diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile
index 24497d3936f6..9826626995a9 100644
--- a/drivers/spi/Makefile
+++ b/drivers/spi/Makefile
@@ -16,6 +16,7 @@ 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/
+obj-$(CONFIG_SPI_MUX_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 354bafc42c2b..1631a16ce6ec 100644
--- a/drivers/spi/tests/.kunitconfig
+++ b/drivers/spi/tests/.kunitconfig
@@ -6,3 +6,7 @@ CONFIG_SPI_CORE_KUNIT_TEST=y
 CONFIG_COMPILE_TEST=y
 CONFIG_SPI_TI_QSPI=y
 CONFIG_SPI_TI_QSPI_KUNIT_TEST=y
+CONFIG_OF=y
+CONFIG_OF_OVERLAY=y
+CONFIG_SPI_MUX=y
+CONFIG_SPI_MUX_KUNIT_TEST=y
diff --git a/drivers/spi/tests/Makefile b/drivers/spi/tests/Makefile
index a5a8fc9435d4..0ec04ecd4d55 100644
--- a/drivers/spi/tests/Makefile
+++ b/drivers/spi/tests/Makefile
@@ -3,3 +3,5 @@
 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
+obj-$(CONFIG_SPI_MUX_KUNIT_TEST) += spi-mux-kunit-test.o
+spi-mux-kunit-test-y := spi-mux-kunit.o kunit_spi_mux.dtbo.o
diff --git a/drivers/spi/tests/kunit_spi_mux.dtso b/drivers/spi/tests/kunit_spi_mux.dtso
new file mode 100644
index 000000000000..48e72ba8e217
--- /dev/null
+++ b/drivers/spi/tests/kunit_spi_mux.dtso
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: GPL-2.0
+/dts-v1/;
+/plugin/;
+
+&{/} {
+	tmux: kunit-spi-mux-chip {
+		compatible = "test,spi-mux-kunit-chip";
+		#mux-control-cells = <0>;
+	};
+
+	kunit-spi-mux-parent {
+		compatible = "test,spi-mux-kunit-parent";
+		#address-cells = <1>;
+		#size-cells = <0>;
+
+		spi@0 {
+			compatible = "spi-mux";
+			reg = <0>;
+			spi-max-frequency = <1000000>;
+			spi-cs-setup-delay-ns = <1000>;
+			spi-cs-hold-delay-ns = <300>;
+			mux-controls = <&tmux>;
+			#address-cells = <1>;
+			#size-cells = <0>;
+
+			device@0 {
+				compatible = "test,spi-mux-kunit-child";
+				reg = <0>;
+				spi-max-frequency = <1000000>;
+			};
+
+			device@1 {
+				compatible = "test,spi-mux-kunit-child";
+				reg = <1>;
+				spi-max-frequency = <2000000>;
+				spi-cs-setup-delay-ns = <1500>;
+				spi-cs-hold-delay-ns = <100>;
+				spi-cs-inactive-delay-ns = <700>;
+			};
+		};
+	};
+};
diff --git a/drivers/spi/tests/spi-mux-kunit.c b/drivers/spi/tests/spi-mux-kunit.c
new file mode 100644
index 000000000000..53a4e490ddb2
--- /dev/null
+++ b/drivers/spi/tests/spi-mux-kunit.c
@@ -0,0 +1,581 @@
+// SPDX-License-Identifier: GPL-2.0
+// KUnit tests for spi-mux error handling and settings forwarding.
+#include <kunit/of.h>
+#include <kunit/platform_device.h>
+#include <kunit/test.h>
+#include <linux/completion.h>
+#include <linux/jiffies.h>
+#include <linux/kthread.h>
+#include <linux/mux/consumer.h>
+#include <linux/mux/driver.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/spi/spi.h>
+
+#define PFX "/kunit-spi-mux-parent/spi@0"
+
+struct spi_mux_test_ctx {
+	struct mux_control *mux;
+	struct spi_controller *parent;
+	struct spi_device *child[2], *mux_spi;
+	int opt_cnt, unopt_cnt, mux_err, setup_err;
+	u32 last_speed_hz, last_mode;
+	u8 last_bpw;
+	struct spi_delay last_w, last_s, last_h, last_in;
+	struct spi_transfer xfer;
+	struct spi_message msg;
+	struct completion done;
+	u8 tx[4];
+};
+
+static struct spi_mux_test_ctx *g_mux_ctx;
+
+static void kunit_done(void *p)
+{
+	complete(p);
+}
+
+/*
+ * Verify the CS and word delay values captured on the parent controller during
+ * the most recent transfer:
+ * @s:  expected cs_setup.value (ctx->last_s.value).
+ * @h:  expected cs_hold.value (ctx->last_h.value).
+ * @in: expected cs_inactive.value (ctx->last_in.value).
+ * @w:  expected word_delay.value (ctx->last_w.value).
+ *
+ * Baseline DT defaults from kunit_spi_mux.dtso:
+ * - ctx->mux_spi (spi@0):      (s = 1000 ns, h = 300 ns, in = 0, w = 0)
+ * - child[0] (device@0, 1 MHz): (s = 0,       h = 0,      in = 0, w = 0)
+ * - child[1] (device@1, 2 MHz): (s = 1500 ns, h = 100 ns, in = 700 ns, w = 0)
+ */
+static void check_delays(struct kunit *test, struct spi_mux_test_ctx *ctx,
+			 u16 s, u16 h, u16 in, u16 w)
+{
+	KUNIT_EXPECT_EQ(test, ctx->last_s.value, s);
+	KUNIT_EXPECT_EQ(test, ctx->last_h.value, h);
+	KUNIT_EXPECT_EQ(test, ctx->last_in.value, in);
+	KUNIT_EXPECT_EQ(test, ctx->last_w.value, w);
+}
+
+static void check_d(struct kunit *test, struct spi_delay d, struct spi_delay exp)
+{
+	KUNIT_EXPECT_EQ(test, d.value, exp.value);
+	KUNIT_EXPECT_EQ(test, d.unit, exp.unit);
+}
+
+static void check_msg_restored(struct kunit *test, struct spi_mux_test_ctx *ctx,
+			       struct spi_device *spi)
+{
+	kthread_flush_worker(spi->controller->kworker);
+	kthread_flush_worker(ctx->parent->kworker);
+	/*
+	 * Verify the mux semaphore was released after the failed transfer:
+	 * - Good situation (patched): spi-mux released the mux semaphore on
+	 *   error (spi_mux_select() failure or spi_async() failure), so
+	 *   mux_control_try_select() succeeds with 0.
+	 * - Bad situation (unpatched): ERR_LOCKED, ERR_ODD_LEN, and ERR_SETUP
+	 *   return early with the mux semaphore still held, so
+	 *   mux_control_try_select() fails with -EBUSY.
+	 */
+	KUNIT_ASSERT_EQ(test, 0, mux_control_try_select(ctx->mux, 0));
+	mux_control_deselect(ctx->mux);
+	/*
+	 * Verify the child caller's spi_message fields were restored:
+	 * - Good situation (patched): spi_mux_complete_cb() restored the child
+	 *   caller's spi, complete, and context pointers on spi_async()
+	 *   failure.
+	 * - Bad situation (unpatched): ERR_LOCKED and ERR_ODD_LEN return early
+	 *   after overwriting msg.spi = priv->spi,
+	 *   msg.complete = spi_mux_complete_cb, and msg.context = priv.
+	 */
+	KUNIT_EXPECT_PTR_EQ(test, ctx->msg.spi, spi);
+	KUNIT_EXPECT_PTR_EQ(test, ctx->msg.complete, &kunit_done);
+	KUNIT_EXPECT_PTR_EQ(test, ctx->msg.context, &ctx->done);
+	/*
+	 * Verify parent message optimization state was cleaned up:
+	 * - Good situation (patched): ctx->parent has max_transfer_size = 2,
+	 *   so when ERR_LOCKED fails inside spi_async(priv->spi, m) after
+	 *   __spi_optimize_message() splits the 4-byte transfer, spi_async()
+	 *   unoptimizes m (optimized = false, resources empty,
+	 *   opt_cnt == unopt_cnt).
+	 * - Bad situation (unpatched): spi_async() leaks the optimized state
+	 *   on -EBUSY, leaving msg.optimized == true, split transfers in
+	 *   msg.resources, and opt_cnt (1) != unopt_cnt (0).
+	 */
+	KUNIT_EXPECT_FALSE(test, ctx->msg.optimized);
+	KUNIT_EXPECT_TRUE(test, list_empty(&ctx->msg.resources));
+	KUNIT_EXPECT_EQ(test, ctx->opt_cnt, ctx->unopt_cnt);
+}
+
+static int spi_mux_test_chip_set(struct mux_control *mux, int state)
+{
+	return g_mux_ctx ? READ_ONCE(g_mux_ctx->mux_err) : 0;
+}
+
+static const struct mux_control_ops spi_mux_test_chip_ops = {
+	.set = spi_mux_test_chip_set,
+};
+
+static int spi_mux_test_parent_opt(struct spi_message *msg)
+{
+	if (!g_mux_ctx)
+		return -ENODEV;
+	g_mux_ctx->opt_cnt++;
+	return 0;
+}
+
+static int spi_mux_test_parent_unopt(struct spi_message *msg)
+{
+	if (!g_mux_ctx)
+		return -ENODEV;
+	g_mux_ctx->unopt_cnt++;
+	return 0;
+}
+
+static int spi_mux_test_parent_setup(struct spi_device *spi)
+{
+	if (!g_mux_ctx)
+		return -ENODEV;
+	return xchg(&g_mux_ctx->setup_err, 0);
+}
+
+static void spi_mux_test_parent_set_cs(struct spi_device *spi, bool enable)
+{
+	if (!g_mux_ctx)
+		return;
+	g_mux_ctx->last_mode = spi->mode;
+	g_mux_ctx->last_s = spi->cs_setup;
+	g_mux_ctx->last_h = spi->cs_hold;
+	g_mux_ctx->last_in = spi->cs_inactive;
+}
+
+static int spi_mux_parent_xfer(struct spi_controller *c, struct spi_device *s,
+			       struct spi_transfer *x)
+{
+	if (!g_mux_ctx)
+		return -ENODEV;
+	g_mux_ctx->last_speed_hz = x->speed_hz;
+	g_mux_ctx->last_bpw = x->bits_per_word;
+	g_mux_ctx->last_w = x->word_delay;
+	return 0;
+}
+
+static size_t spi_mux_test_parent_max_transfer_size(struct spi_device *spi)
+{
+	return 2;
+}
+
+static int spi_mux_test_probe(struct platform_device *pdev)
+{
+	struct spi_controller *ctlr;
+	struct mux_chip *chip;
+
+	if (!g_mux_ctx)
+		return -ENODEV;
+	if (of_device_is_compatible(pdev->dev.of_node, "test,spi-mux-kunit-chip")) {
+		chip = devm_mux_chip_alloc(&pdev->dev, 1, 0);
+		if (IS_ERR(chip))
+			return PTR_ERR(chip);
+		chip->ops = &spi_mux_test_chip_ops;
+		chip->mux[0].states = 2;
+		g_mux_ctx->mux = &chip->mux[0];
+		return devm_mux_chip_register(&pdev->dev, chip);
+	}
+	ctlr = devm_spi_alloc_host(&pdev->dev, 0);
+	if (!ctlr)
+		return -ENOMEM;
+	ctlr->bus_num = -1;
+	ctlr->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH | SPI_CS_WORD;
+	ctlr->bits_per_word_mask = SPI_BPW_MASK(8) | SPI_BPW_MASK(16);
+	ctlr->max_transfer_size = spi_mux_test_parent_max_transfer_size;
+	ctlr->setup = spi_mux_test_parent_setup;
+	ctlr->set_cs = spi_mux_test_parent_set_cs;
+	ctlr->transfer_one = spi_mux_parent_xfer;
+	ctlr->optimize_message = spi_mux_test_parent_opt;
+	ctlr->unoptimize_message = spi_mux_test_parent_unopt;
+	g_mux_ctx->parent = ctlr;
+	return devm_spi_register_controller(&pdev->dev, ctlr);
+}
+
+static const struct of_device_id spi_mux_test_match[] = {
+	{ .compatible = "test,spi-mux-kunit-chip" },
+	{ .compatible = "test,spi-mux-kunit-parent" },
+	{}
+};
+
+static struct platform_driver spi_mux_test_driver = {
+	.probe = spi_mux_test_probe,
+	.driver = {
+		.name = "spi-mux-kunit-driver",
+		.of_match_table = spi_mux_test_match,
+		.probe_type = PROBE_FORCE_SYNCHRONOUS,
+	},
+};
+
+KUNIT_DEFINE_ACTION_WRAPPER(spi_mux_test_put_dev, put_device, struct device *);
+
+static struct spi_device *spi_mux_test_find_dev(struct kunit *test, const char *path)
+{
+	struct device_node *np = of_find_node_by_path(path);
+	struct device *dev = bus_find_device_by_of_node(&spi_bus_type, np);
+
+	of_node_put(np);
+	KUNIT_ASSERT_NOT_NULL(test, dev);
+	KUNIT_ASSERT_EQ(test, 0, kunit_add_action_or_reset(test, spi_mux_test_put_dev, dev));
+	return to_spi_device(dev);
+}
+
+static int spi_mux_test_init(struct kunit *test)
+{
+	struct spi_mux_test_ctx *ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	test->priv = ctx;
+	g_mux_ctx = ctx;
+	init_completion(&ctx->done);
+	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;
+	KUNIT_ASSERT_EQ(test, 0, kunit_platform_driver_register(test, &spi_mux_test_driver));
+	KUNIT_ASSERT_EQ(test, 0, of_overlay_apply_kunit(test, kunit_spi_mux));
+	ctx->child[0] = spi_mux_test_find_dev(test, PFX "/device@0");
+	ctx->child[1] = spi_mux_test_find_dev(test, PFX "/device@1");
+	ctx->mux_spi = to_spi_device(ctx->child[0]->controller->dev.parent);
+	return 0;
+}
+
+static void spi_mux_test_exit(struct kunit *test)
+{
+	struct spi_mux_test_ctx *ctx = test->priv;
+
+	if (ctx) {
+		if (ctx->child[0])
+			kthread_flush_worker(ctx->child[0]->controller->kworker);
+		if (ctx->parent)
+			kthread_flush_worker(ctx->parent->kworker);
+		if (ctx->mux) {
+			int ret;
+
+			ctx->mux_err = 0;
+			ret = mux_control_try_select(ctx->mux, 0);
+			if (!ret || ret == -EBUSY)
+				mux_control_deselect(ctx->mux);
+		}
+	}
+	g_mux_ctx = NULL;
+}
+
+static int spi_mux_test_send(struct kunit *test, struct spi_device *spi,
+			     unsigned int len, u8 bpw)
+{
+	struct spi_mux_test_ctx *ctx = test->priv;
+	int ret;
+
+	reinit_completion(&ctx->done);
+	ctx->xfer.len = len;
+	ctx->xfer.bits_per_word = bpw;
+	ctx->xfer.speed_hz = 0;
+	ctx->xfer.word_delay = (struct spi_delay){ 0 };
+	ctx->msg.status = -EINPROGRESS;
+	ctx->msg.actual_length = 0;
+	ctx->msg.spi = spi;
+	ret = spi_async(spi, &ctx->msg);
+	if (ret)
+		return ret;
+	return wait_for_completion_timeout(&ctx->done, msecs_to_jiffies(5000)) ?
+		ctx->msg.status : -ETIMEDOUT;
+}
+
+static int spi_mux_test_send_locked(struct kunit *test, struct spi_device *spi,
+				    struct spi_controller *parent,
+				    unsigned int len, u8 bpw)
+{
+	int ret;
+
+	spi_bus_lock(parent);
+	ret = spi_mux_test_send(test, spi, len, bpw);
+	spi_bus_unlock(parent);
+
+	return ret;
+}
+
+static void send_4b(struct kunit *test, struct spi_device *spi)
+{
+	struct spi_mux_test_ctx *ctx = test->priv;
+
+	KUNIT_ASSERT_EQ(test, 0, spi_mux_test_send(test, spi, 4, 0));
+	kthread_flush_worker(spi->controller->kworker);
+	kthread_flush_worker(ctx->parent->kworker);
+}
+
+static void check_sync_resend(struct kunit *test, struct spi_device *spi, u32 exp_hz)
+{
+	struct spi_mux_test_ctx *ctx = test->priv;
+
+	ctx->xfer.len = 4;
+	ctx->xfer.bits_per_word = 0;
+	ctx->xfer.speed_hz = 0;
+	ctx->msg.spi = spi;
+	KUNIT_EXPECT_EQ(test, 0, spi_sync(spi, &ctx->msg));
+	KUNIT_EXPECT_EQ(test, ctx->msg.actual_length, 4);
+	KUNIT_EXPECT_EQ(test, ctx->last_speed_hz, exp_hz);
+}
+
+enum spi_mux_err_type {
+	ERR_LOCKED,
+	ERR_ODD_LEN,
+	ERR_SELECT,
+	ERR_SETUP,
+};
+
+struct spi_mux_err_case {
+	const char *name;
+	enum spi_mux_err_type type;
+	int exp;
+};
+
+static const struct spi_mux_err_case spi_mux_err_cases[] = {
+	{ "bus_locked", ERR_LOCKED, -EBUSY },
+	{ "rejects_odd_len", ERR_ODD_LEN, -EINVAL },
+	{ "select_fails", ERR_SELECT, -EIO },
+	{ "parent_setup_fails", ERR_SETUP, -EIO },
+};
+
+struct spi_mux_delay_case {
+	const char *name;
+	u8 child;
+	struct spi_delay set_s, set_h, set_in, exp_s, exp_h, exp_in;
+};
+
+#define NS(v)	{ (v), SPI_DELAY_UNIT_NSECS }
+#define US(v)	{ (v), SPI_DELAY_UNIT_USECS }
+#define SCK(v)	{ (v), SPI_DELAY_UNIT_SCK }
+
+/*
+ * Each case exercises a distinct branch of spi_mux_merge_cs_delay() against
+ * mux_spi's DT baseline delays (s = 1000 ns, h = 300 ns, in = 0):
+ * 1. longer (child[1]): child[1] DT s = 1500 ns (> mux 1000 ns), runtime
+ *    h = 500 ns (> mux 300 ns), DT in = 700 ns (> mux 0 ns) -> selects child
+ *    delays (NS(1500), NS(500), NS(700)).
+ * 2. tie (child[0]): runtime s = US(1) (1000 ns, tied with mux NS(1000)),
+ *    h = 0, in = NS(200) -> on tie (child_ns > mux_ns is false), keeps mux's
+ *    NS(1000) unit/value, resets h to mux NS(300), and selects child
+ *    in = NS(200).
+ * 3. sck_zero (child[0]): runtime in = SCK(4) while mux in is 0 (!mux->value)
+ *    -> spi_delay_to_ns() returns -EINVAL (child_ns < 0), but because mux
+ *    delay is 0, passes through SCK(4).
+ * 4. sck_set (child[0]): runtime s = SCK(4) while mux s is non-zero
+ *    (NS(1000)), and h = NS(500) -> logs warning and falls back to mux
+ *    NS(1000) for s, while selecting NS(500) for h.
+ *
+ * Bad situation (unpatched): spi_mux_select() never copies CS delays from the
+ * child, leaving ctx->mux_spi at (NS(1000), NS(300), 0) for all 4 cases.
+ */
+static const struct spi_mux_delay_case spi_mux_delay_cases[] = {
+	{ "longer", 1, {}, NS(500), {}, NS(1500), NS(500), NS(700) },
+	{ "tie", 0, US(1), {}, NS(200), NS(1000), NS(300), NS(200) },
+	{ "sck_zero", 0, {}, {}, SCK(4), NS(1000), NS(300), SCK(4) },
+	{ "sck_set", 0, SCK(4), NS(500), {}, NS(1000), NS(500), {} },
+};
+
+static void spi_mux_err_desc(const struct spi_mux_err_case *p, char *desc)
+{
+	strscpy(desc, p->name, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(spi_mux_err, spi_mux_err_cases, spi_mux_err_desc);
+
+static void spi_mux_delay_desc(const struct spi_mux_delay_case *p, char *desc)
+{
+	strscpy(desc, p->name, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(spi_mux_delay, spi_mux_delay_cases, spi_mux_delay_desc);
+
+static void spi_mux_error_test(struct kunit *test)
+{
+	const struct spi_mux_err_case *p = test->param_value;
+	struct spi_mux_test_ctx *ctx = test->priv;
+	int child = (p->type == ERR_SELECT || p->type == ERR_SETUP);
+	int len = (p->type == ERR_ODD_LEN) ? 3 : 4;
+	u8 bpw = (p->type == ERR_ODD_LEN) ? 16 : 0;
+	int ret;
+
+	/*
+	 * spi-mux skips mux_control_select() and spi_setup(priv->spi) when
+	 * priv->current_cs == spi_get_chipselect(spi, 0). Prime
+	 * priv->current_cs = 0 on child[0] so targeting child[1] (child = 1)
+	 * forces a channel switch that executes both mux_control_select()
+	 * (ERR_SELECT) and spi_setup(priv->spi) (ERR_SETUP).
+	 */
+	if (p->type >= ERR_SELECT) {
+		send_4b(test, ctx->child[0]);
+		ctx->mux_err = (p->type == ERR_SELECT) ? -EIO : 0;
+		ctx->setup_err = (p->type == ERR_SETUP) ? -EIO : 0;
+	}
+
+	if (p->type == ERR_LOCKED)
+		ret = spi_mux_test_send_locked(test, ctx->child[child], ctx->parent, len, bpw);
+	else
+		ret = spi_mux_test_send(test, ctx->child[child], len, bpw);
+
+	ctx->mux_err = 0;
+	ctx->setup_err = 0;
+	if (ret == -ETIMEDOUT) {
+		kthread_flush_worker(ctx->child[child]->controller->kworker);
+		kthread_flush_worker(ctx->parent->kworker);
+	}
+	/*
+	 * Good situation (patched): spi_mux_transfer_one_message() finalizes
+	 * ctx->msg with p->exp (-EBUSY, -EINVAL, or -EIO).
+	 * Bad situation (unpatched): spi_finalize_current_message() is skipped
+	 * on all 4 error paths so spi_mux_test_send() times out (-ETIMEDOUT).
+	 */
+	KUNIT_ASSERT_EQ(test, ret, p->exp);
+	check_msg_restored(test, ctx, ctx->child[child]);
+	/*
+	 * In ERR_SETUP, spi_mux_select(child[1]) overwrites
+	 * priv->spi->max_speed_hz with child[1]'s 2000000 Hz before
+	 * spi_setup(priv->spi) fails:
+	 * - Good situation (patched): spi_mux_select() invalidates
+	 *   priv->current_cs = SPI_MUX_NO_CS on error, so transferring on
+	 *   child[0] re-applies 1000000 Hz.
+	 * - Bad situation (if priv->current_cs remained 0):
+	 *   spi_mux_select(child[0]) would see current_cs == 0 and return
+	 *   early, running child[0] at child[1]'s 2000000 Hz.
+	 */
+	if (p->type == ERR_SETUP)
+		check_sync_resend(test, ctx->child[0], 1000000);
+	check_sync_resend(test, ctx->child[child], child ? 2000000 : 1000000);
+}
+
+static void spi_mux_cs_delay_test(struct kunit *test)
+{
+	const struct spi_mux_delay_case *p = test->param_value;
+	struct spi_mux_test_ctx *ctx = test->priv;
+	struct spi_device *spi = ctx->child[p->child];
+
+	/*
+	 * Transfer on child[1] with cs_hold = NS(500) first to overwrite
+	 * priv->spi->cs_hold with 500 ns, so when switching to child[0] whose
+	 * cs_hold.value == 0 (!child->value), the test verifies
+	 * spi_mux_merge_cs_delay() actively writes *dst = *mux (300 ns)
+	 * instead of leaving child[1]'s 500 ns stale.
+	 */
+	if (!p->child) {
+		ctx->child[1]->cs_hold = (struct spi_delay)NS(500);
+		send_4b(test, ctx->child[1]);
+	}
+	spi->cs_setup = p->set_s.value ? p->set_s : spi->cs_setup;
+	spi->cs_hold = p->set_h.value ? p->set_h : spi->cs_hold;
+	spi->cs_inactive = p->set_in.value ? p->set_in : spi->cs_inactive;
+	send_4b(test, spi);
+	check_d(test, ctx->last_s, p->exp_s);
+	check_d(test, ctx->last_h, p->exp_h);
+	check_d(test, ctx->last_in, p->exp_in);
+}
+
+static void spi_mux_settings_lifecycle_test(struct kunit *test)
+{
+	struct spi_mux_test_ctx *ctx = test->priv;
+	struct device *dev = &ctx->mux_spi->dev;
+	struct spi_device *spi = ctx->child[0];
+
+	/*
+	 * Phase 1 & 2: Select child[0] (priv->current_cs = 0), then mutate
+	 * child[0]'s mode (SPI_CS_HIGH), max_speed_hz (500000), bits_per_word
+	 * (16), and cs_setup (1200 ns), call spi_setup(child[0]), and transfer
+	 * on child[0] again without switching channels:
+	 * - Good situation (patched): spi_mux_setup() sets
+	 *   priv->current_cs = SPI_MUX_NO_CS, forcing the next transfer on
+	 *   child[0] to re-apply (500000 Hz, 16-bit, SPI_CS_HIGH, 1200 ns) to
+	 *   the parent.
+	 * - Bad situation (unpatched): spi_mux_setup() leaves
+	 *   priv->current_cs == 0, so spi_mux_select(child[0]) returns early
+	 *   and the transfer runs with stale (1000000 Hz, 8-bit, 1000 ns)
+	 *   settings.
+	 */
+	send_4b(test, spi);
+	KUNIT_EXPECT_EQ(test, ctx->opt_cnt, 1);
+	KUNIT_EXPECT_EQ(test, ctx->unopt_cnt, 1);
+	spi->mode ^= SPI_CS_HIGH;
+	spi->max_speed_hz = 500000;
+	spi->bits_per_word = 16;
+	spi->cs_setup = (struct spi_delay)NS(1200);
+	KUNIT_ASSERT_EQ(test, 0, spi_setup(spi));
+	send_4b(test, spi);
+	KUNIT_EXPECT_EQ(test, ctx->last_speed_hz, 500000);
+	KUNIT_EXPECT_EQ(test, ctx->last_bpw, 16);
+	KUNIT_EXPECT_TRUE(test, !!(ctx->last_mode & SPI_CS_HIGH));
+	KUNIT_EXPECT_EQ(test, ctx->last_s.value, 1200);
+	spi->cs_setup = (struct spi_delay){};
+	KUNIT_ASSERT_EQ(test, 0, spi_setup(spi));
+	/*
+	 * Phase 3: Switch child[1] -> child[0] -> child[1]:
+	 * - Good situation (patched): switching to child[1] applies child[1]'s
+	 *   merged CS delays (1500, 500, 700) and word_delay (2 us); switching
+	 *   back to child[0] (delays cleared to 0) restores the mux baseline
+	 *   (1000, 300, 0, 0); switching to child[1] again re-applies
+	 *   (1500, 500, 700, 2).
+	 * - Bad situation (unpatched): child[1]'s CS and word delays are never
+	 *   copied to priv->spi, so check_delays(test, ctx, 1500, 500, 700, 2)
+	 *   fails with (1000, 300, 0, 0).
+	 */
+	ctx->child[1]->cs_hold = (struct spi_delay)NS(500);
+	ctx->child[1]->word_delay = (struct spi_delay)US(2);
+	ctx->child[1]->cs_inactive = (struct spi_delay)NS(700);
+	send_4b(test, ctx->child[1]);
+	KUNIT_EXPECT_EQ(test, ctx->last_bpw, 8);
+	check_delays(test, ctx, 1500, 500, 700, 2);
+	send_4b(test, ctx->child[0]);
+	check_delays(test, ctx, 1000, 300, 0, 0);
+	send_4b(test, ctx->child[1]);
+	check_delays(test, ctx, 1500, 500, 700, 2);
+	kthread_flush_worker(spi->controller->kworker);
+	kthread_flush_worker(ctx->parent->kworker);
+	ctx->child[0] = NULL;
+	ctx->child[1] = NULL;
+	/*
+	 * Phase 4: Unbind spi-mux while child[1]'s (1500, 500, 700, 2) delays
+	 * are active on ctx->mux_spi, then re-attach spi-mux and transfer on
+	 * child[0]:
+	 * - Good situation (patched): the devm cleanup action
+	 *   spi_mux_restore_delays() restores ctx->mux_spi's original DT
+	 *   delays (1000, 300, 0, 0) on unbind so re-probing does not inherit
+	 *   child[1]'s delays as the new baseline.
+	 * - Bad situation (without spi_mux_restore_delays()): ctx->mux_spi
+	 *   retains (1500, 500, 700, 2) across unbind, poisoning
+	 *   priv->mux_cs_* on re-probe.
+	 */
+	device_release_driver(dev);
+	KUNIT_EXPECT_EQ(test, ctx->mux_spi->cs_setup.value, 1000);
+	KUNIT_EXPECT_EQ(test, ctx->mux_spi->cs_hold.value, 300);
+	KUNIT_EXPECT_EQ(test, ctx->mux_spi->cs_inactive.value, 0);
+	KUNIT_EXPECT_EQ(test, ctx->mux_spi->word_delay.value, 0);
+	KUNIT_ASSERT_EQ(test, 1, device_attach(dev));
+	ctx->child[0] = spi_mux_test_find_dev(test, PFX "/device@0");
+	ctx->child[1] = spi_mux_test_find_dev(test, PFX "/device@1");
+	send_4b(test, ctx->child[0]);
+	check_delays(test, ctx, 1000, 300, 0, 0);
+}
+
+static struct kunit_case spi_mux_test_cases[] = {
+	KUNIT_CASE_PARAM(spi_mux_error_test, spi_mux_err_gen_params),
+	KUNIT_CASE_PARAM(spi_mux_cs_delay_test, spi_mux_delay_gen_params),
+	KUNIT_CASE(spi_mux_settings_lifecycle_test),
+	{}
+};
+
+static struct kunit_suite spi_mux_test_suite = {
+	.name = "spi_mux",
+	.init = spi_mux_test_init,
+	.exit = spi_mux_test_exit,
+	.test_cases = spi_mux_test_cases,
+};
+
+kunit_test_suite(spi_mux_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for spi-mux");
+MODULE_LICENSE("GPL");
-- 
2.56.0.317.g454f8302e1-goog


  parent reply	other threads:[~2026-11-02 16:34 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 ` [PATCH 08/14] spi: spi-ti-qspi: add KUnit tests for transfer error finalization Joseph CT Chan
2026-11-02 16:30 ` Joseph CT Chan [this message]
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-10-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