[PATCH v2 2/2] lib/ts_fsm: add KUnit coverage for exported patterns

From: Zhiqi Zhang

Date: Sun Nov 01 2026 - 03:20:36 EST


Add regression coverage for exporting an FSM pattern and preparing another
configuration from it. Check all 14 public token types with positive and
negative matching examples, and a sequence containing a repeated class.

Compare the exported bytes with the input pattern as well as checking
matching behaviour. Verify that the original configuration remains usable
after exporting and recreating the pattern. Register cleanup actions so
that early assertion failures still destroy the configurations.

OpenAI Codex drafted these tests and the changelog and drove the recorded
automated validation.

Signed-off-by: Zhiqi Zhang <zhiqizhangg@xxxxxxxxx>
---
lib/Kconfig.debug | 14 ++++
lib/tests/Makefile | 1 +
lib/tests/textsearch_fsm_kunit.c | 138 +++++++++++++++++++++++++++++++
3 files changed, 153 insertions(+)
create mode 100644 lib/tests/textsearch_fsm_kunit.c

diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 2d22638ec..416d033b0 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -2531,6 +2531,20 @@ config STRING_HELPERS_KUNIT_TEST
depends on KUNIT
default KUNIT_ALL_TESTS

+config TEXTSEARCH_FSM_KUNIT_TEST
+ tristate "KUnit tests for FSM text search" if !KUNIT_ALL_TESTS
+ depends on KUNIT
+ select TEXTSEARCH
+ select TEXTSEARCH_FSM
+ default KUNIT_ALL_TESTS
+ help
+ Test that exporting and recreating an FSM text search configuration
+ preserves its matching behaviour. Exercise each public token type
+ and a sequence of tokens with a repeated character class. Check
+ that exporting also leaves the original configuration usable.
+
+ If unsure, say N.
+
config STRING_KUNIT_BENCH
bool "Benchmark string functions at runtime"
depends on STRING_KUNIT_TEST
diff --git a/lib/tests/Makefile b/lib/tests/Makefile
index df0537e1b..fa47ef5d3 100644
--- a/lib/tests/Makefile
+++ b/lib/tests/Makefile
@@ -54,6 +54,7 @@ CFLAGS_stackinit_kunit.o += $(call cc-disable-warning, switch-unreachable)
obj-$(CONFIG_STACKINIT_KUNIT_TEST) += stackinit_kunit.o
obj-$(CONFIG_STRING_KUNIT_TEST) += string_kunit.o
obj-$(CONFIG_STRING_HELPERS_KUNIT_TEST) += string_helpers_kunit.o
+obj-$(CONFIG_TEXTSEARCH_FSM_KUNIT_TEST) += textsearch_fsm_kunit.o
obj-$(CONFIG_USERCOPY_KUNIT_TEST) += usercopy_kunit.o
obj-$(CONFIG_UTIL_MACROS_KUNIT) += util_macros_kunit.o
obj-$(CONFIG_RATELIMIT_KUNIT_TEST) += test_ratelimit.o
diff --git a/lib/tests/textsearch_fsm_kunit.c b/lib/tests/textsearch_fsm_kunit.c
new file mode 100644
index 000000000..6cb173b97
--- /dev/null
+++ b/lib/tests/textsearch_fsm_kunit.c
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Tests for exporting and recreating FSM text search configurations. */
+
+#include <kunit/test.h>
+#include <linux/limits.h>
+#include <linux/module.h>
+#include <linux/string.h>
+#include <linux/textsearch.h>
+#include <linux/textsearch_fsm.h>
+
+struct fsm_test_case {
+ const char *name;
+ u16 type;
+ const char *match;
+ const char *miss;
+};
+
+static const struct fsm_test_case fsm_test_cases[] = {
+ { "specific", TS_FSM_SPECIFIC, "x", "y" },
+ { "wildcard", TS_FSM_WILDCARD, "A", "" },
+ { "digit", TS_FSM_DIGIT, "7", "A" },
+ { "xdigit", TS_FSM_XDIGIT, "f", "g" },
+ { "print", TS_FSM_PRINT, "A", "\n" },
+ { "alpha", TS_FSM_ALPHA, "A", "7" },
+ { "alnum", TS_FSM_ALNUM, "7", "!" },
+ { "ascii", TS_FSM_ASCII, "A", "\200" },
+ { "cntrl", TS_FSM_CNTRL, "\n", "A" },
+ { "graph", TS_FSM_GRAPH, "!", " " },
+ { "lower", TS_FSM_LOWER, "a", "A" },
+ { "upper", TS_FSM_UPPER, "A", "a" },
+ { "punct", TS_FSM_PUNCT, "!", "A" },
+ { "space", TS_FSM_SPACE, " ", "A" },
+};
+
+static void fsm_case_to_desc(const struct fsm_test_case *params, char *desc)
+{
+ snprintf(desc, KUNIT_PARAM_DESC_SIZE, "%s", params->name);
+}
+
+KUNIT_ARRAY_PARAM(fsm, fsm_test_cases, fsm_case_to_desc);
+
+static void fsm_destroy_config(void *conf)
+{
+ textsearch_destroy(conf);
+}
+
+static void fsm_expect_matches(struct kunit *test, struct ts_config *conf,
+ const struct fsm_test_case *params)
+{
+ struct ts_state state = {};
+ unsigned int pos;
+
+ pos = textsearch_find_continuous(conf, &state, params->match,
+ strlen(params->match));
+ KUNIT_EXPECT_EQ(test, pos, 0U);
+ pos = textsearch_find_continuous(conf, &state, params->miss,
+ strlen(params->miss));
+ KUNIT_EXPECT_EQ(test, pos, UINT_MAX);
+}
+
+static void fsm_check_roundtrip(struct kunit *test,
+ const struct ts_fsm_token *pattern,
+ unsigned int len,
+ const struct fsm_test_case *params)
+{
+ struct ts_fsm_token *exported;
+ struct ts_config *conf, *recreated;
+ int err;
+
+ conf = textsearch_prepare("fsm", pattern, len, GFP_KERNEL, TS_AUTOLOAD);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, conf);
+ err = kunit_add_action_or_reset(test, fsm_destroy_config, conf);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ fsm_expect_matches(test, conf, params);
+
+ KUNIT_ASSERT_EQ(test, textsearch_get_pattern_len(conf), len);
+ exported = kunit_kmalloc(test, len, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, exported);
+ memcpy(exported, textsearch_get_pattern(conf), len);
+ KUNIT_EXPECT_MEMEQ(test, exported, pattern, len);
+
+ /* Exporting a pattern must preserve the existing search behaviour. */
+ fsm_expect_matches(test, conf, params);
+
+ recreated = textsearch_prepare("fsm", exported, len, GFP_KERNEL,
+ TS_AUTOLOAD);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL_MSG(test, recreated,
+ "exported token: type=%u recur=%u value=%u",
+ exported->type, exported->recur,
+ exported->value);
+ err = kunit_add_action_or_reset(test, fsm_destroy_config, recreated);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ fsm_expect_matches(test, recreated, params);
+ fsm_expect_matches(test, conf, params);
+}
+
+static void fsm_test_pattern_roundtrip(struct kunit *test)
+{
+ const struct fsm_test_case *params = test->param_value;
+ struct ts_fsm_token pattern = {
+ .type = params->type,
+ .recur = TS_FSM_SINGLE,
+ .value = 'x',
+ };
+
+ fsm_check_roundtrip(test, &pattern, sizeof(pattern), params);
+}
+
+static void fsm_test_sequence_roundtrip(struct kunit *test)
+{
+ static const struct ts_fsm_token pattern[] = {
+ { TS_FSM_DIGIT, TS_FSM_MULTI, 0 },
+ { TS_FSM_SPECIFIC, TS_FSM_SINGLE, ':' },
+ { TS_FSM_UPPER, TS_FSM_SINGLE, 0 },
+ };
+ static const struct fsm_test_case params = {
+ .match = "77:A",
+ .miss = "77:a",
+ };
+
+ fsm_check_roundtrip(test, pattern, sizeof(pattern), &params);
+}
+
+static struct kunit_case fsm_kunit_test_cases[] = {
+ KUNIT_CASE_PARAM(fsm_test_pattern_roundtrip, fsm_gen_params),
+ KUNIT_CASE(fsm_test_sequence_roundtrip),
+ {}
+};
+
+static struct kunit_suite fsm_test_suite = {
+ .name = "textsearch-fsm",
+ .test_cases = fsm_kunit_test_cases,
+};
+
+kunit_test_suite(fsm_test_suite);
+
+MODULE_DESCRIPTION("FSM text search configuration tests");
+MODULE_LICENSE("GPL");
--
2.54.0 (Apple Git-157)