public inbox for linux-kernel@vger.kernel.org
 help / color / mirror / Atom feed
From: wen.yang@linux.dev
To: Steven Rostedt <rostedt@goodmis.org>,
	Gabriele Monaco <gmonaco@redhat.com>,
	Masami Hiramatsu <mhiramat@kernel.org>,
	Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: linux-trace-kernel@vger.kernel.org, linux-kernel@vger.kernel.org,
	Wen Yang <wen.yang@linux.dev>
Subject: [RFC PATCH 3/4] rv/tlob: Add KUnit tests for the tlob monitor
Date: Mon, 13 Apr 2026 03:27:20 +0800	[thread overview]
Message-ID: <0a7f41ff8cb13f8601920ead2979db2ee5f2d442.1776020428.git.wen.yang@linux.dev> (raw)
In-Reply-To: <cover.1776020428.git.wen.yang@linux.dev>

From: Wen Yang <wen.yang@linux.dev>

Add six KUnit test suites gated behind CONFIG_TLOB_KUNIT_TEST
(depends on RV_MON_TLOB && KUNIT; default KUNIT_ALL_TESTS).
A .kunitconfig fragment is provided for the kunit.py runner.

Coverage: automaton state transitions and self-loops; start/stop API
error paths (duplicate start, missing start, overflow threshold,
table-full, immediate deadline); scheduler context-switch accounting
for on/off-CPU time; violation tracepoint payload fields; ring buffer
push, drop-new overflow, and wakeup; and the uprobe line parser.

Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
 kernel/trace/rv/Makefile                   |    1 +
 kernel/trace/rv/monitors/tlob/.kunitconfig |    5 +
 kernel/trace/rv/monitors/tlob/Kconfig      |   12 +
 kernel/trace/rv/monitors/tlob/tlob.c       |    1 +
 kernel/trace/rv/monitors/tlob/tlob_kunit.c | 1194 ++++++++++++++++++++
 5 files changed, 1213 insertions(+)
 create mode 100644 kernel/trace/rv/monitors/tlob/.kunitconfig
 create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.c

diff --git a/kernel/trace/rv/Makefile b/kernel/trace/rv/Makefile
index cc3781a3b..6d963207d 100644
--- a/kernel/trace/rv/Makefile
+++ b/kernel/trace/rv/Makefile
@@ -19,6 +19,7 @@ obj-$(CONFIG_RV_MON_NRP) += monitors/nrp/nrp.o
 obj-$(CONFIG_RV_MON_SSSW) += monitors/sssw/sssw.o
 obj-$(CONFIG_RV_MON_OPID) += monitors/opid/opid.o
 obj-$(CONFIG_RV_MON_TLOB) += monitors/tlob/tlob.o
+obj-$(CONFIG_TLOB_KUNIT_TEST) += monitors/tlob/tlob_kunit.o
 # Add new monitors here
 obj-$(CONFIG_RV_REACTORS) += rv_reactors.o
 obj-$(CONFIG_RV_REACT_PRINTK) += reactor_printk.o
diff --git a/kernel/trace/rv/monitors/tlob/.kunitconfig b/kernel/trace/rv/monitors/tlob/.kunitconfig
new file mode 100644
index 000000000..977c58601
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/.kunitconfig
@@ -0,0 +1,5 @@
+CONFIG_FTRACE=y
+CONFIG_KUNIT=y
+CONFIG_RV=y
+CONFIG_RV_MON_TLOB=y
+CONFIG_TLOB_KUNIT_TEST=y
diff --git a/kernel/trace/rv/monitors/tlob/Kconfig b/kernel/trace/rv/monitors/tlob/Kconfig
index 010237480..4ccd2f881 100644
--- a/kernel/trace/rv/monitors/tlob/Kconfig
+++ b/kernel/trace/rv/monitors/tlob/Kconfig
@@ -49,3 +49,15 @@ config RV_MON_TLOB
 	  For further information, see:
 	    Documentation/trace/rv/monitor_tlob.rst
 
+config TLOB_KUNIT_TEST
+	tristate "KUnit tests for tlob monitor" if !KUNIT_ALL_TESTS
+	depends on RV_MON_TLOB && KUNIT
+	default KUNIT_ALL_TESTS
+	help
+	  Enable KUnit in-kernel unit tests for the tlob RV monitor.
+
+	  Tests cover automaton state transitions, the hash table helpers,
+	  the start/stop task interface, and the event ring buffer including
+	  overflow handling and wakeup behaviour.
+
+	  Say Y or M here to run the tlob KUnit test suite; otherwise say N.
diff --git a/kernel/trace/rv/monitors/tlob/tlob.c b/kernel/trace/rv/monitors/tlob/tlob.c
index a6e474025..dd959eb9b 100644
--- a/kernel/trace/rv/monitors/tlob/tlob.c
+++ b/kernel/trace/rv/monitors/tlob/tlob.c
@@ -784,6 +784,7 @@ VISIBLE_IF_KUNIT int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
 	*path_out  = buf + n;
 	return 0;
 }
+EXPORT_SYMBOL_IF_KUNIT(tlob_parse_uprobe_line);
 
 static ssize_t tlob_monitor_write(struct file *file,
 				  const char __user *ubuf,
diff --git a/kernel/trace/rv/monitors/tlob/tlob_kunit.c b/kernel/trace/rv/monitors/tlob/tlob_kunit.c
new file mode 100644
index 000000000..64f5abb34
--- /dev/null
+++ b/kernel/trace/rv/monitors/tlob/tlob_kunit.c
@@ -0,0 +1,1194 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for the tlob RV monitor.
+ *
+ * tlob_automaton:         DA transition table coverage.
+ * tlob_task_api:          tlob_start_task()/tlob_stop_task() lifecycle and errors.
+ * tlob_sched_integration: on/off-CPU accounting across real context switches.
+ * tlob_trace_output:      tlob_budget_exceeded tracepoint field verification.
+ * tlob_event_buf:         ring buffer push, overflow, and wakeup.
+ * tlob_parse_uprobe:      uprobe format string parser acceptance and rejection.
+ *
+ * The duplicate-(binary, offset_start) constraint enforced by tlob_add_uprobe()
+ * is not covered here: that function calls kern_path() and requires a real
+ * filesystem, which is outside the scope of unit tests. It is covered by the
+ * uprobe_duplicate_offset case in tools/testing/selftests/rv/test_tlob.sh.
+ */
+#include <kunit/test.h>
+#include <linux/atomic.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/kthread.h>
+#include <linux/ktime.h>
+#include <linux/mutex.h>
+#include <linux/sched.h>
+#include <linux/sched/task.h>
+#include <linux/tracepoint.h>
+
+/*
+ * Pull in the rv tracepoint declarations so that
+ * register_trace_tlob_budget_exceeded() is available.
+ * No CREATE_TRACE_POINTS here  --  the tracepoint implementation lives in rv.c.
+ */
+#include <rv_trace.h>
+
+#include "tlob.h"
+
+/*
+ * da_handle_event_tlob - apply one automaton transition on @da_mon.
+ *
+ * This helper is used only by the KUnit automaton suite. It applies the
+ * tlob transition table directly on a supplied da_monitor without touching
+ * per-task slots, tracepoints, or timers.
+ */
+static void da_handle_event_tlob(struct da_monitor *da_mon,
+				 enum events_tlob event)
+{
+	enum states_tlob curr_state = (enum states_tlob)da_mon->curr_state;
+	enum states_tlob next_state =
+		(enum states_tlob)automaton_tlob.function[curr_state][event];
+
+	if (next_state != INVALID_STATE)
+		da_mon->curr_state = next_state;
+}
+
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
+
+/*
+ * Suite 1: automaton state-machine transitions
+ */
+
+/* unmonitored -> trace_start -> on_cpu */
+static void tlob_unmonitored_to_on_cpu(struct kunit *test)
+{
+	struct da_monitor mon = { .curr_state = unmonitored_tlob };
+
+	da_handle_event_tlob(&mon, trace_start_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)on_cpu_tlob);
+}
+
+/* on_cpu -> switch_out -> off_cpu */
+static void tlob_on_cpu_switch_out(struct kunit *test)
+{
+	struct da_monitor mon = { .curr_state = on_cpu_tlob };
+
+	da_handle_event_tlob(&mon, switch_out_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)off_cpu_tlob);
+}
+
+/* off_cpu -> switch_in -> on_cpu */
+static void tlob_off_cpu_switch_in(struct kunit *test)
+{
+	struct da_monitor mon = { .curr_state = off_cpu_tlob };
+
+	da_handle_event_tlob(&mon, switch_in_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)on_cpu_tlob);
+}
+
+/* on_cpu -> budget_expired -> unmonitored */
+static void tlob_on_cpu_budget_expired(struct kunit *test)
+{
+	struct da_monitor mon = { .curr_state = on_cpu_tlob };
+
+	da_handle_event_tlob(&mon, budget_expired_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)unmonitored_tlob);
+}
+
+/* off_cpu -> budget_expired -> unmonitored */
+static void tlob_off_cpu_budget_expired(struct kunit *test)
+{
+	struct da_monitor mon = { .curr_state = off_cpu_tlob };
+
+	da_handle_event_tlob(&mon, budget_expired_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)unmonitored_tlob);
+}
+
+/* on_cpu -> trace_stop -> unmonitored */
+static void tlob_on_cpu_trace_stop(struct kunit *test)
+{
+	struct da_monitor mon = { .curr_state = on_cpu_tlob };
+
+	da_handle_event_tlob(&mon, trace_stop_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)unmonitored_tlob);
+}
+
+/* off_cpu -> trace_stop -> unmonitored */
+static void tlob_off_cpu_trace_stop(struct kunit *test)
+{
+	struct da_monitor mon = { .curr_state = off_cpu_tlob };
+
+	da_handle_event_tlob(&mon, trace_stop_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)unmonitored_tlob);
+}
+
+/* budget_expired -> unmonitored; a single trace_start re-enters on_cpu. */
+static void tlob_violation_then_restart(struct kunit *test)
+{
+	struct da_monitor mon = { .curr_state = unmonitored_tlob };
+
+	da_handle_event_tlob(&mon, trace_start_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)on_cpu_tlob);
+
+	da_handle_event_tlob(&mon, budget_expired_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)unmonitored_tlob);
+
+	/* Single trace_start is sufficient to re-enter on_cpu */
+	da_handle_event_tlob(&mon, trace_start_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)on_cpu_tlob);
+
+	da_handle_event_tlob(&mon, trace_stop_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)unmonitored_tlob);
+}
+
+/* off_cpu self-loops on switch_out and sched_wakeup. */
+static void tlob_off_cpu_self_loops(struct kunit *test)
+{
+	static const enum events_tlob events[] = {
+		switch_out_tlob, sched_wakeup_tlob,
+	};
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(events); i++) {
+		struct da_monitor mon = { .curr_state = off_cpu_tlob };
+
+		da_handle_event_tlob(&mon, events[i]);
+		KUNIT_EXPECT_EQ_MSG(test, (int)mon.curr_state,
+				    (int)off_cpu_tlob,
+				    "event %u should self-loop in off_cpu",
+				    events[i]);
+	}
+}
+
+/* on_cpu self-loops on sched_wakeup. */
+static void tlob_on_cpu_self_loops(struct kunit *test)
+{
+	struct da_monitor mon = { .curr_state = on_cpu_tlob };
+
+	da_handle_event_tlob(&mon, sched_wakeup_tlob);
+	KUNIT_EXPECT_EQ_MSG(test, (int)mon.curr_state, (int)on_cpu_tlob,
+			    "sched_wakeup should self-loop in on_cpu");
+}
+
+/* Scheduling events in unmonitored self-loop (no state change). */
+static void tlob_unmonitored_ignores_sched(struct kunit *test)
+{
+	static const enum events_tlob events[] = {
+		switch_in_tlob, switch_out_tlob, sched_wakeup_tlob,
+	};
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(events); i++) {
+		struct da_monitor mon = { .curr_state = unmonitored_tlob };
+
+		da_handle_event_tlob(&mon, events[i]);
+		KUNIT_EXPECT_EQ_MSG(test, (int)mon.curr_state,
+				    (int)unmonitored_tlob,
+				    "event %u should self-loop in unmonitored",
+				    events[i]);
+	}
+}
+
+static void tlob_full_happy_path(struct kunit *test)
+{
+	struct da_monitor mon = { .curr_state = unmonitored_tlob };
+
+	da_handle_event_tlob(&mon, trace_start_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)on_cpu_tlob);
+
+	da_handle_event_tlob(&mon, switch_out_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)off_cpu_tlob);
+
+	da_handle_event_tlob(&mon, switch_in_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)on_cpu_tlob);
+
+	da_handle_event_tlob(&mon, trace_stop_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)unmonitored_tlob);
+}
+
+static void tlob_multiple_switches(struct kunit *test)
+{
+	struct da_monitor mon = { .curr_state = unmonitored_tlob };
+	int i;
+
+	da_handle_event_tlob(&mon, trace_start_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)on_cpu_tlob);
+
+	for (i = 0; i < 3; i++) {
+		da_handle_event_tlob(&mon, switch_out_tlob);
+		KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)off_cpu_tlob);
+		da_handle_event_tlob(&mon, switch_in_tlob);
+		KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)on_cpu_tlob);
+	}
+
+	da_handle_event_tlob(&mon, trace_stop_tlob);
+	KUNIT_EXPECT_EQ(test, (int)mon.curr_state, (int)unmonitored_tlob);
+}
+
+static struct kunit_case tlob_automaton_cases[] = {
+	KUNIT_CASE(tlob_unmonitored_to_on_cpu),
+	KUNIT_CASE(tlob_on_cpu_switch_out),
+	KUNIT_CASE(tlob_off_cpu_switch_in),
+	KUNIT_CASE(tlob_on_cpu_budget_expired),
+	KUNIT_CASE(tlob_off_cpu_budget_expired),
+	KUNIT_CASE(tlob_on_cpu_trace_stop),
+	KUNIT_CASE(tlob_off_cpu_trace_stop),
+	KUNIT_CASE(tlob_off_cpu_self_loops),
+	KUNIT_CASE(tlob_on_cpu_self_loops),
+	KUNIT_CASE(tlob_unmonitored_ignores_sched),
+	KUNIT_CASE(tlob_full_happy_path),
+	KUNIT_CASE(tlob_violation_then_restart),
+	KUNIT_CASE(tlob_multiple_switches),
+	{}
+};
+
+static struct kunit_suite tlob_automaton_suite = {
+	.name       = "tlob_automaton",
+	.test_cases = tlob_automaton_cases,
+};
+
+/*
+ * Suite 2: task registration API
+ */
+
+/* Basic start/stop cycle */
+static void tlob_start_stop_ok(struct kunit *test)
+{
+	int ret;
+
+	ret = tlob_start_task(current, 10000000 /* 10 s, won't fire */, NULL, 0);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), 0);
+}
+
+/* Double start must return -EEXIST. */
+static void tlob_double_start(struct kunit *test)
+{
+	KUNIT_ASSERT_EQ(test, tlob_start_task(current, 10000000, NULL, 0), 0);
+	KUNIT_EXPECT_EQ(test, tlob_start_task(current, 10000000, NULL, 0), -EEXIST);
+	tlob_stop_task(current);
+}
+
+/* Stop without start must return -ESRCH. */
+static void tlob_stop_without_start(struct kunit *test)
+{
+	tlob_stop_task(current);  /* clear any stale entry first */
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), -ESRCH);
+}
+
+/*
+ * A 1 us budget fires before tlob_stop_task() is called. Either the
+ * timer wins (-ESRCH) or we are very fast (0); both are valid.
+ */
+static void tlob_immediate_deadline(struct kunit *test)
+{
+	int ret = tlob_start_task(current, 1 /* 1 us - fires almost immediately */, NULL, 0);
+
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	/* Let the 1 us timer fire */
+	udelay(100);
+	/*
+	 * By now the hrtimer has almost certainly fired. Either it has
+	 * (returns -ESRCH) or we were very fast (returns 0). Both are
+	 * acceptable; just ensure no crash and the table is clean after.
+	 */
+	ret = tlob_stop_task(current);
+	KUNIT_EXPECT_TRUE(test, ret == 0 || ret == -ESRCH);
+}
+
+/*
+ * Fill the table to TLOB_MAX_MONITORED using kthreads (each needs a
+ * distinct task_struct), then verify the next start returns -ENOSPC.
+ */
+struct tlob_waiter_ctx {
+	struct completion start;
+	struct completion done;
+};
+
+static int tlob_waiter_fn(void *arg)
+{
+	struct tlob_waiter_ctx *ctx = arg;
+
+	wait_for_completion(&ctx->start);
+	complete(&ctx->done);
+	return 0;
+}
+
+static void tlob_enospc(struct kunit *test)
+{
+	struct tlob_waiter_ctx *ctxs;
+	struct task_struct **threads;
+	int i, ret;
+
+	ctxs = kunit_kcalloc(test, TLOB_MAX_MONITORED,
+			     sizeof(*ctxs), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, ctxs);
+
+	threads = kunit_kcalloc(test, TLOB_MAX_MONITORED,
+				sizeof(*threads), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, threads);
+
+	/* Start TLOB_MAX_MONITORED kthreads and monitor each */
+	for (i = 0; i < TLOB_MAX_MONITORED; i++) {
+		init_completion(&ctxs[i].start);
+		init_completion(&ctxs[i].done);
+
+		threads[i] = kthread_run(tlob_waiter_fn, &ctxs[i],
+					 "tlob_waiter_%d", i);
+		if (IS_ERR(threads[i])) {
+			KUNIT_FAIL(test, "kthread_run failed at i=%d", i);
+			threads[i] = NULL;
+			goto cleanup;
+		}
+		get_task_struct(threads[i]);
+
+		ret = tlob_start_task(threads[i], 10000000, NULL, 0);
+		if (ret != 0) {
+			KUNIT_FAIL(test, "tlob_start_task failed at i=%d: %d",
+				   i, ret);
+			put_task_struct(threads[i]);
+			complete(&ctxs[i].start);
+			goto cleanup;
+		}
+	}
+
+	/* The table is now full: one more must fail with -ENOSPC */
+	ret = tlob_start_task(current, 10000000, NULL, 0);
+	KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+
+cleanup:
+	/*
+	 * Two-pass cleanup: cancel tlob monitoring and unblock kthreads first,
+	 * then kthread_stop() to wait for full exit before releasing refs.
+	 */
+	for (i = 0; i < TLOB_MAX_MONITORED; i++) {
+		if (!threads[i])
+			break;
+		tlob_stop_task(threads[i]);
+		complete(&ctxs[i].start);
+	}
+	for (i = 0; i < TLOB_MAX_MONITORED; i++) {
+		if (!threads[i])
+			break;
+		kthread_stop(threads[i]);
+		put_task_struct(threads[i]);
+	}
+}
+
+/*
+ * A kthread holds a mutex for 80 ms; arm a 10 ms budget, burn ~1 ms
+ * on-CPU, then block on the mutex. The timer fires off-CPU; stop
+ * must return -ESRCH.
+ */
+struct tlob_holder_ctx {
+	struct mutex		lock;
+	struct completion	ready;
+	unsigned int		hold_ms;
+};
+
+static int tlob_holder_fn(void *arg)
+{
+	struct tlob_holder_ctx *ctx = arg;
+
+	mutex_lock(&ctx->lock);
+	complete(&ctx->ready);
+	msleep(ctx->hold_ms);
+	mutex_unlock(&ctx->lock);
+	return 0;
+}
+
+static void tlob_deadline_fires_off_cpu(struct kunit *test)
+{
+	struct tlob_holder_ctx ctx = { .hold_ms = 80 };
+	struct task_struct *holder;
+	ktime_t t0;
+	int ret;
+
+	mutex_init(&ctx.lock);
+	init_completion(&ctx.ready);
+
+	holder = kthread_run(tlob_holder_fn, &ctx, "tlob_holder_kunit");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, holder);
+	wait_for_completion(&ctx.ready);
+
+	/* Arm 10 ms budget while kthread holds the mutex. */
+	ret = tlob_start_task(current, 10000, NULL, 0);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	/* Phase 1: burn ~1 ms on-CPU to exercise on_cpu accounting. */
+	t0 = ktime_get();
+	while (ktime_us_delta(ktime_get(), t0) < 1000)
+		cpu_relax();
+
+	/*
+	 * Phase 2: block on the mutex -> on_cpu->off_cpu transition.
+	 * The 10 ms budget fires while we are off-CPU.
+	 */
+	mutex_lock(&ctx.lock);
+	mutex_unlock(&ctx.lock);
+
+	/* Timer already fired and removed the entry -> -ESRCH */
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), -ESRCH);
+}
+
+/* Arm a 1 ms budget and busy-spin for 50 ms; timer fires on-CPU. */
+static void tlob_deadline_fires_on_cpu(struct kunit *test)
+{
+	ktime_t t0;
+	int ret;
+
+	ret = tlob_start_task(current, 1000 /* 1 ms */, NULL, 0);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	/* Busy-spin 50 ms - 50x the budget */
+	t0 = ktime_get();
+	while (ktime_us_delta(ktime_get(), t0) < 50000)
+		cpu_relax();
+
+	/* Timer fired during the spin; entry is gone */
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), -ESRCH);
+}
+
+/*
+ * Start three tasks, call tlob_destroy_monitor() + tlob_init_monitor(),
+ * and verify the table is empty afterwards.
+ */
+static int tlob_dummy_fn(void *arg)
+{
+	wait_for_completion((struct completion *)arg);
+	return 0;
+}
+
+static void tlob_stop_all_cleanup(struct kunit *test)
+{
+	struct completion done1, done2;
+	struct task_struct *t1, *t2;
+	int ret;
+
+	init_completion(&done1);
+	init_completion(&done2);
+
+	t1 = kthread_run(tlob_dummy_fn, &done1, "tlob_dummy1");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, t1);
+	get_task_struct(t1);
+
+	t2 = kthread_run(tlob_dummy_fn, &done2, "tlob_dummy2");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, t2);
+	get_task_struct(t2);
+
+	KUNIT_ASSERT_EQ(test, tlob_start_task(current, 10000000, NULL, 0), 0);
+	KUNIT_ASSERT_EQ(test, tlob_start_task(t1, 10000000, NULL, 0), 0);
+	KUNIT_ASSERT_EQ(test, tlob_start_task(t2, 10000000, NULL, 0), 0);
+
+	/* Destroy clears all entries via tlob_stop_all() */
+	tlob_destroy_monitor();
+	ret = tlob_init_monitor();
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	/* Table must be empty now */
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(current), -ESRCH);
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(t1), -ESRCH);
+	KUNIT_EXPECT_EQ(test, tlob_stop_task(t2), -ESRCH);
+
+	complete(&done1);
+	complete(&done2);
+	/*
+	 * completions live on stack; wait for kthreads to exit before return.
+	 */
+	kthread_stop(t1);
+	kthread_stop(t2);
+	put_task_struct(t1);
+	put_task_struct(t2);
+}
+
+/* A threshold that overflows ktime_t must be rejected with -ERANGE. */
+static void tlob_overflow_threshold(struct kunit *test)
+{
+	/* KTIME_MAX / NSEC_PER_USEC + 1 overflows ktime_t */
+	u64 too_large = (u64)(KTIME_MAX / NSEC_PER_USEC) + 1;
+
+	KUNIT_EXPECT_EQ(test,
+		tlob_start_task(current, too_large, NULL, 0),
+		-ERANGE);
+}
+
+static int tlob_task_api_suite_init(struct kunit_suite *suite)
+{
+	return tlob_init_monitor();
+}
+
+static void tlob_task_api_suite_exit(struct kunit_suite *suite)
+{
+	tlob_destroy_monitor();
+}
+
+static struct kunit_case tlob_task_api_cases[] = {
+	KUNIT_CASE(tlob_start_stop_ok),
+	KUNIT_CASE(tlob_double_start),
+	KUNIT_CASE(tlob_stop_without_start),
+	KUNIT_CASE(tlob_immediate_deadline),
+	KUNIT_CASE(tlob_enospc),
+	KUNIT_CASE(tlob_overflow_threshold),
+	KUNIT_CASE(tlob_deadline_fires_off_cpu),
+	KUNIT_CASE(tlob_deadline_fires_on_cpu),
+	KUNIT_CASE(tlob_stop_all_cleanup),
+	{}
+};
+
+static struct kunit_suite tlob_task_api_suite = {
+	.name       = "tlob_task_api",
+	.suite_init = tlob_task_api_suite_init,
+	.suite_exit = tlob_task_api_suite_exit,
+	.test_cases = tlob_task_api_cases,
+};
+
+/*
+ * Suite 3: scheduling integration
+ */
+
+struct tlob_ping_ctx {
+	struct completion ping;
+	struct completion pong;
+};
+
+static int tlob_ping_fn(void *arg)
+{
+	struct tlob_ping_ctx *ctx = arg;
+
+	/* Wait for main to give us the CPU back */
+	wait_for_completion(&ctx->ping);
+	complete(&ctx->pong);
+	return 0;
+}
+
+/* Force two context switches and verify stop returns 0 (within budget). */
+static void tlob_sched_switch_accounting(struct kunit *test)
+{
+	struct tlob_ping_ctx ctx;
+	struct task_struct *peer;
+	int ret;
+
+	init_completion(&ctx.ping);
+	init_completion(&ctx.pong);
+
+	peer = kthread_run(tlob_ping_fn, &ctx, "tlob_ping_kunit");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, peer);
+
+	/* Arm a generous 5 s budget so the timer never fires */
+	ret = tlob_start_task(current, 5000000, NULL, 0);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	/*
+	 * complete(ping) -> peer runs, forcing a context switch out and back.
+	 */
+	complete(&ctx.ping);
+	wait_for_completion(&ctx.pong);
+
+	/*
+	 * Back on CPU after one off-CPU interval; stop must return 0.
+	 */
+	ret = tlob_stop_task(current);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+}
+
+/*
+ * Verify that monitoring a kthread (not current) works: start on behalf
+ * of a kthread, let it block, then stop it.
+ */
+static int tlob_block_fn(void *arg)
+{
+	struct completion *done = arg;
+
+	/* Block briefly, exercising off_cpu accounting for this task */
+	msleep(20);
+	complete(done);
+	return 0;
+}
+
+static void tlob_monitor_other_task(struct kunit *test)
+{
+	struct completion done;
+	struct task_struct *target;
+	int ret;
+
+	init_completion(&done);
+
+	target = kthread_run(tlob_block_fn, &done, "tlob_target_kunit");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, target);
+	get_task_struct(target);
+
+	/* Arm a 5 s budget for the target task */
+	ret = tlob_start_task(target, 5000000, NULL, 0);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	wait_for_completion(&done);
+
+	/*
+	 * Target has finished; stop_task may return 0 (still in htable)
+	 * or -ESRCH (kthread exited and timer fired / entry cleaned up).
+	 */
+	ret = tlob_stop_task(target);
+	KUNIT_EXPECT_TRUE(test, ret == 0 || ret == -ESRCH);
+	put_task_struct(target);
+}
+
+static int tlob_sched_suite_init(struct kunit_suite *suite)
+{
+	return tlob_init_monitor();
+}
+
+static void tlob_sched_suite_exit(struct kunit_suite *suite)
+{
+	tlob_destroy_monitor();
+}
+
+static struct kunit_case tlob_sched_integration_cases[] = {
+	KUNIT_CASE(tlob_sched_switch_accounting),
+	KUNIT_CASE(tlob_monitor_other_task),
+	{}
+};
+
+static struct kunit_suite tlob_sched_integration_suite = {
+	.name       = "tlob_sched_integration",
+	.suite_init = tlob_sched_suite_init,
+	.suite_exit = tlob_sched_suite_exit,
+	.test_cases = tlob_sched_integration_cases,
+};
+
+/*
+ * Suite 4: ftrace tracepoint field verification
+ */
+
+/* Capture fields from trace_tlob_budget_exceeded for inspection. */
+struct tlob_exceeded_capture {
+	atomic_t	fired;		/* 1 after first call */
+	pid_t		pid;
+	u64		threshold_us;
+	u64		on_cpu_us;
+	u64		off_cpu_us;
+	u32		switches;
+	bool		state_is_on_cpu;
+	u64		tag;
+};
+
+static void
+probe_tlob_budget_exceeded(void *data,
+			   struct task_struct *task, u64 threshold_us,
+			   u64 on_cpu_us, u64 off_cpu_us,
+			   u32 switches, bool state_is_on_cpu, u64 tag)
+{
+	struct tlob_exceeded_capture *cap = data;
+
+	/* Only capture the first event to avoid races. */
+	if (atomic_cmpxchg(&cap->fired, 0, 1) != 0)
+		return;
+
+	cap->pid		= task->pid;
+	cap->threshold_us	= threshold_us;
+	cap->on_cpu_us		= on_cpu_us;
+	cap->off_cpu_us		= off_cpu_us;
+	cap->switches		= switches;
+	cap->state_is_on_cpu	= state_is_on_cpu;
+	cap->tag		= tag;
+}
+
+/*
+ * Arm a 2 ms budget and busy-spin for 60 ms. Verify the tracepoint fires
+ * once with matching threshold, correct pid, and total time >= budget.
+ *
+ * state_is_on_cpu is not asserted: preemption during the spin makes it
+ * non-deterministic.
+ */
+static void tlob_trace_budget_exceeded_on_cpu(struct kunit *test)
+{
+	struct tlob_exceeded_capture cap = {};
+	const u64 threshold_us = 2000; /* 2 ms */
+	ktime_t t0;
+	int ret;
+
+	atomic_set(&cap.fired, 0);
+
+	ret = register_trace_tlob_budget_exceeded(probe_tlob_budget_exceeded,
+						  &cap);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	ret = tlob_start_task(current, threshold_us, NULL, 0);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	/* Busy-spin 60 ms  --  30x the budget */
+	t0 = ktime_get();
+	while (ktime_us_delta(ktime_get(), t0) < 60000)
+		cpu_relax();
+
+	/* Entry removed by timer; stop returns -ESRCH */
+	tlob_stop_task(current);
+
+	/*
+	 * Synchronise: ensure the probe callback has completed before we
+	 * read the captured fields.
+	 */
+	tracepoint_synchronize_unregister();
+	unregister_trace_tlob_budget_exceeded(probe_tlob_budget_exceeded, &cap);
+
+	KUNIT_EXPECT_EQ(test, atomic_read(&cap.fired), 1);
+	KUNIT_EXPECT_EQ(test, (int)cap.pid, (int)current->pid);
+	KUNIT_EXPECT_EQ(test, cap.threshold_us, threshold_us);
+	/* Total elapsed must cover at least the budget */
+	KUNIT_EXPECT_GE(test, cap.on_cpu_us + cap.off_cpu_us, threshold_us);
+}
+
+/*
+ * Holder kthread grabs a mutex for 80 ms; arm 10 ms budget, burn ~1 ms
+ * on-CPU, then block on the mutex. Timer fires off-CPU. Verify:
+ * state_is_on_cpu == false, switches >= 1, off_cpu_us > 0.
+ */
+static void tlob_trace_budget_exceeded_off_cpu(struct kunit *test)
+{
+	struct tlob_exceeded_capture cap = {};
+	struct tlob_holder_ctx ctx = { .hold_ms = 80 };
+	struct task_struct *holder;
+	const u64 threshold_us = 10000; /* 10 ms */
+	ktime_t t0;
+	int ret;
+
+	atomic_set(&cap.fired, 0);
+
+	mutex_init(&ctx.lock);
+	init_completion(&ctx.ready);
+
+	holder = kthread_run(tlob_holder_fn, &ctx, "tlob_holder2_kunit");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, holder);
+	wait_for_completion(&ctx.ready);
+
+	ret = register_trace_tlob_budget_exceeded(probe_tlob_budget_exceeded,
+						  &cap);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	ret = tlob_start_task(current, threshold_us, NULL, 0);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	/* Phase 1: ~1 ms on-CPU */
+	t0 = ktime_get();
+	while (ktime_us_delta(ktime_get(), t0) < 1000)
+		cpu_relax();
+
+	/* Phase 2: block -> off-CPU; timer fires here */
+	mutex_lock(&ctx.lock);
+	mutex_unlock(&ctx.lock);
+
+	tlob_stop_task(current);
+
+	tracepoint_synchronize_unregister();
+	unregister_trace_tlob_budget_exceeded(probe_tlob_budget_exceeded, &cap);
+
+	KUNIT_EXPECT_EQ(test, atomic_read(&cap.fired), 1);
+	KUNIT_EXPECT_EQ(test, cap.threshold_us, threshold_us);
+	/* Violation happened off-CPU */
+	KUNIT_EXPECT_FALSE(test, cap.state_is_on_cpu);
+	/* At least the switch_out event was counted */
+	KUNIT_EXPECT_GE(test, (u64)cap.switches, (u64)1);
+	/* Off-CPU time must be non-zero */
+	KUNIT_EXPECT_GT(test, cap.off_cpu_us, (u64)0);
+}
+
+/* threshold_us in the tracepoint must exactly match the start argument. */
+static void tlob_trace_threshold_field_accuracy(struct kunit *test)
+{
+	static const u64 thresholds[] = { 500, 1000, 3000 };
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(thresholds); i++) {
+		struct tlob_exceeded_capture cap = {};
+		ktime_t t0;
+		int ret;
+
+		atomic_set(&cap.fired, 0);
+
+		ret = register_trace_tlob_budget_exceeded(
+			probe_tlob_budget_exceeded, &cap);
+		KUNIT_ASSERT_EQ(test, ret, 0);
+
+		ret = tlob_start_task(current, thresholds[i], NULL, 0);
+		KUNIT_ASSERT_EQ(test, ret, 0);
+
+		/* Spin for 20x the threshold to ensure timer fires */
+		t0 = ktime_get();
+		while (ktime_us_delta(ktime_get(), t0) <
+		       (s64)(thresholds[i] * 20))
+			cpu_relax();
+
+		tlob_stop_task(current);
+
+		tracepoint_synchronize_unregister();
+		unregister_trace_tlob_budget_exceeded(
+			probe_tlob_budget_exceeded, &cap);
+
+		KUNIT_EXPECT_EQ_MSG(test, cap.threshold_us, thresholds[i],
+				    "threshold mismatch for entry %u", i);
+	}
+}
+
+static int tlob_trace_suite_init(struct kunit_suite *suite)
+{
+	int ret;
+
+	ret = tlob_init_monitor();
+	if (ret)
+		return ret;
+	return tlob_enable_hooks();
+}
+
+static void tlob_trace_suite_exit(struct kunit_suite *suite)
+{
+	tlob_disable_hooks();
+	tlob_destroy_monitor();
+}
+
+static struct kunit_case tlob_trace_output_cases[] = {
+	KUNIT_CASE(tlob_trace_budget_exceeded_on_cpu),
+	KUNIT_CASE(tlob_trace_budget_exceeded_off_cpu),
+	KUNIT_CASE(tlob_trace_threshold_field_accuracy),
+	{}
+};
+
+static struct kunit_suite tlob_trace_output_suite = {
+	.name       = "tlob_trace_output",
+	.suite_init = tlob_trace_suite_init,
+	.suite_exit = tlob_trace_suite_exit,
+	.test_cases = tlob_trace_output_cases,
+};
+
+/* Suite 5: ring buffer */
+
+/*
+ * Allocate a synthetic rv_file_priv for ring buffer tests. Uses
+ * kunit_kzalloc() instead of __get_free_pages() since the ring is never
+ * mmap'd here.
+ */
+static struct rv_file_priv *alloc_priv_kunit(struct kunit *test, u32 cap)
+{
+	struct rv_file_priv *priv;
+	struct tlob_ring *ring;
+
+	priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return NULL;
+
+	ring = &priv->ring;
+
+	ring->page = kunit_kzalloc(test, sizeof(struct tlob_mmap_page),
+				   GFP_KERNEL);
+	if (!ring->page)
+		return NULL;
+
+	ring->data = kunit_kzalloc(test, cap * sizeof(struct tlob_event),
+				   GFP_KERNEL);
+	if (!ring->data)
+		return NULL;
+
+	ring->mask            = cap - 1;
+	ring->page->capacity  = cap;
+	ring->page->version   = 1;
+	ring->page->data_offset = PAGE_SIZE; /* nominal; not used in tests */
+	ring->page->record_size = sizeof(struct tlob_event);
+	spin_lock_init(&ring->lock);
+	init_waitqueue_head(&priv->waitq);
+	return priv;
+}
+
+/* Push one record and verify all fields survive the round-trip. */
+static void tlob_event_push_one(struct kunit *test)
+{
+	struct rv_file_priv *priv;
+	struct tlob_ring *ring;
+	struct tlob_event in = {
+		.tid		= 1234,
+		.threshold_us	= 5000,
+		.on_cpu_us	= 3000,
+		.off_cpu_us	= 2000,
+		.switches	= 3,
+		.state		= 1,
+	};
+	struct tlob_event out = {};
+	u32 tail;
+
+	priv = alloc_priv_kunit(test, TLOB_RING_DEFAULT_CAP);
+	KUNIT_ASSERT_NOT_NULL(test, priv);
+
+	ring = &priv->ring;
+
+	tlob_event_push_kunit(priv, &in);
+
+	/* One record written, none dropped */
+	KUNIT_EXPECT_EQ(test, ring->page->data_head, 1u);
+	KUNIT_EXPECT_EQ(test, ring->page->data_tail, 0u);
+	KUNIT_EXPECT_EQ(test, ring->page->dropped,   0ull);
+
+	/* Dequeue manually */
+	tail = ring->page->data_tail;
+	out  = ring->data[tail & ring->mask];
+	ring->page->data_tail = tail + 1;
+
+	KUNIT_EXPECT_EQ(test, out.tid,          in.tid);
+	KUNIT_EXPECT_EQ(test, out.threshold_us, in.threshold_us);
+	KUNIT_EXPECT_EQ(test, out.on_cpu_us,    in.on_cpu_us);
+	KUNIT_EXPECT_EQ(test, out.off_cpu_us,   in.off_cpu_us);
+	KUNIT_EXPECT_EQ(test, out.switches,     in.switches);
+	KUNIT_EXPECT_EQ(test, out.state,        in.state);
+
+	/* Ring is now empty */
+	KUNIT_EXPECT_EQ(test, ring->page->data_head, ring->page->data_tail);
+}
+
+/*
+ * Fill to capacity, push one more. Drop-new policy: head stays at cap,
+ * dropped == 1, oldest record is preserved.
+ */
+static void tlob_event_push_overflow(struct kunit *test)
+{
+	struct rv_file_priv *priv;
+	struct tlob_ring *ring;
+	struct tlob_event ntf = {};
+	struct tlob_event out = {};
+	const u32 cap = TLOB_RING_MIN_CAP;
+	u32 i;
+
+	priv = alloc_priv_kunit(test, cap);
+	KUNIT_ASSERT_NOT_NULL(test, priv);
+
+	ring = &priv->ring;
+
+	/* Push cap + 1 records; tid encodes the sequence */
+	for (i = 0; i <= cap; i++) {
+		ntf.tid          = i;
+		ntf.threshold_us = (u64)i * 1000;
+		tlob_event_push_kunit(priv, &ntf);
+	}
+
+	/* Drop-new: head stopped at cap; one record was silently discarded */
+	KUNIT_EXPECT_EQ(test, ring->page->data_head, cap);
+	KUNIT_EXPECT_EQ(test, ring->page->data_tail, 0u);
+	KUNIT_EXPECT_EQ(test, ring->page->dropped,   1ull);
+
+	/* Oldest surviving record must be the first one pushed (tid == 0) */
+	out = ring->data[ring->page->data_tail & ring->mask];
+	KUNIT_EXPECT_EQ(test, out.tid, 0u);
+
+	/* Drain the ring; the last record must have tid == cap - 1 */
+	for (i = 0; i < cap; i++) {
+		u32 tail = ring->page->data_tail;
+
+		out = ring->data[tail & ring->mask];
+		ring->page->data_tail = tail + 1;
+	}
+	KUNIT_EXPECT_EQ(test, out.tid, cap - 1);
+	KUNIT_EXPECT_EQ(test, ring->page->data_head, ring->page->data_tail);
+}
+
+/* A freshly initialised ring is empty. */
+static void tlob_event_empty(struct kunit *test)
+{
+	struct rv_file_priv *priv;
+	struct tlob_ring *ring;
+
+	priv = alloc_priv_kunit(test, TLOB_RING_DEFAULT_CAP);
+	KUNIT_ASSERT_NOT_NULL(test, priv);
+
+	ring = &priv->ring;
+
+	KUNIT_EXPECT_EQ(test, ring->page->data_head, 0u);
+	KUNIT_EXPECT_EQ(test, ring->page->data_tail, 0u);
+	KUNIT_EXPECT_EQ(test, ring->page->dropped,   0ull);
+}
+
+/* A kthread blocks on wait_event_interruptible(); pushing one record must
+ * wake it within 1 s.
+ */
+
+struct tlob_wakeup_ctx {
+	struct rv_file_priv	*priv;
+	struct completion	 ready;
+	struct completion	 done;
+	int			 woke;
+};
+
+static int tlob_wakeup_thread(void *arg)
+{
+	struct tlob_wakeup_ctx *ctx = arg;
+	struct tlob_ring *ring = &ctx->priv->ring;
+
+	complete(&ctx->ready);
+
+	wait_event_interruptible(ctx->priv->waitq,
+		smp_load_acquire(&ring->page->data_head) !=
+		READ_ONCE(ring->page->data_tail) ||
+		kthread_should_stop());
+
+	if (smp_load_acquire(&ring->page->data_head) !=
+	    READ_ONCE(ring->page->data_tail))
+		ctx->woke = 1;
+
+	complete(&ctx->done);
+	return 0;
+}
+
+static void tlob_ring_wakeup(struct kunit *test)
+{
+	struct rv_file_priv *priv;
+	struct tlob_wakeup_ctx ctx;
+	struct task_struct *t;
+	struct tlob_event ev = { .tid = 99 };
+	long timeout;
+
+	priv = alloc_priv_kunit(test, TLOB_RING_DEFAULT_CAP);
+	KUNIT_ASSERT_NOT_NULL(test, priv);
+
+	init_completion(&ctx.ready);
+	init_completion(&ctx.done);
+	ctx.priv = priv;
+	ctx.woke = 0;
+
+	t = kthread_run(tlob_wakeup_thread, &ctx, "tlob_wakeup_kunit");
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, t);
+	get_task_struct(t);
+
+	/* Let the kthread reach wait_event_interruptible */
+	wait_for_completion(&ctx.ready);
+	usleep_range(10000, 20000);
+
+	/* Push one record  --  must wake the waiter */
+	tlob_event_push_kunit(priv, &ev);
+
+	timeout = wait_for_completion_timeout(&ctx.done, msecs_to_jiffies(1000));
+	kthread_stop(t);
+	put_task_struct(t);
+
+	KUNIT_EXPECT_GT(test, timeout, 0L);
+	KUNIT_EXPECT_EQ(test, ctx.woke, 1);
+	KUNIT_EXPECT_EQ(test, priv->ring.page->data_head, 1u);
+}
+
+static struct kunit_case tlob_event_buf_cases[] = {
+	KUNIT_CASE(tlob_event_push_one),
+	KUNIT_CASE(tlob_event_push_overflow),
+	KUNIT_CASE(tlob_event_empty),
+	KUNIT_CASE(tlob_ring_wakeup),
+	{}
+};
+
+static struct kunit_suite tlob_event_buf_suite = {
+	.name       = "tlob_event_buf",
+	.test_cases = tlob_event_buf_cases,
+};
+
+/* Suite 6: uprobe format string parser */
+
+/* Happy path: decimal offsets, plain path. */
+static void tlob_parse_decimal_offsets(struct kunit *test)
+{
+	char buf[] = "5000:4768:4848:/usr/bin/myapp";
+	u64 thr; loff_t start, stop; char *path;
+
+	KUNIT_EXPECT_EQ(test,
+		tlob_parse_uprobe_line(buf, &thr, &path, &start, &stop),
+		0);
+	KUNIT_EXPECT_EQ(test, thr,      (u64)5000);
+	KUNIT_EXPECT_EQ(test, start,    (loff_t)4768);
+	KUNIT_EXPECT_EQ(test, stop,     (loff_t)4848);
+	KUNIT_EXPECT_STREQ(test, path,  "/usr/bin/myapp");
+}
+
+/* Happy path: 0x-prefixed hex offsets. */
+static void tlob_parse_hex_offsets(struct kunit *test)
+{
+	char buf[] = "10000:0x12a0:0x12f0:/usr/bin/myapp";
+	u64 thr; loff_t start, stop; char *path;
+
+	KUNIT_EXPECT_EQ(test,
+		tlob_parse_uprobe_line(buf, &thr, &path, &start, &stop),
+		0);
+	KUNIT_EXPECT_EQ(test, start,   (loff_t)0x12a0);
+	KUNIT_EXPECT_EQ(test, stop,    (loff_t)0x12f0);
+	KUNIT_EXPECT_STREQ(test, path, "/usr/bin/myapp");
+}
+
+/* Path containing ':' must not be truncated. */
+static void tlob_parse_path_with_colon(struct kunit *test)
+{
+	char buf[] = "1000:0x100:0x200:/opt/my:app/bin";
+	u64 thr; loff_t start, stop; char *path;
+
+	KUNIT_EXPECT_EQ(test,
+		tlob_parse_uprobe_line(buf, &thr, &path, &start, &stop),
+		0);
+	KUNIT_EXPECT_STREQ(test, path, "/opt/my:app/bin");
+}
+
+/* Zero threshold must be rejected. */
+static void tlob_parse_zero_threshold(struct kunit *test)
+{
+	char buf[] = "0:0x100:0x200:/usr/bin/myapp";
+	u64 thr; loff_t start, stop; char *path;
+
+	KUNIT_EXPECT_EQ(test,
+		tlob_parse_uprobe_line(buf, &thr, &path, &start, &stop),
+		-EINVAL);
+}
+
+/* Empty path (trailing ':' with nothing after) must be rejected. */
+static void tlob_parse_empty_path(struct kunit *test)
+{
+	char buf[] = "5000:0x100:0x200:";
+	u64 thr; loff_t start, stop; char *path;
+
+	KUNIT_EXPECT_EQ(test,
+		tlob_parse_uprobe_line(buf, &thr, &path, &start, &stop),
+		-EINVAL);
+}
+
+/* Missing field (3 tokens instead of 4) must be rejected. */
+static void tlob_parse_too_few_fields(struct kunit *test)
+{
+	char buf[] = "5000:0x100:/usr/bin/myapp";
+	u64 thr; loff_t start, stop; char *path;
+
+	KUNIT_EXPECT_EQ(test,
+		tlob_parse_uprobe_line(buf, &thr, &path, &start, &stop),
+		-EINVAL);
+}
+
+/* Negative offset must be rejected. */
+static void tlob_parse_negative_offset(struct kunit *test)
+{
+	char buf[] = "5000:-1:0x200:/usr/bin/myapp";
+	u64 thr; loff_t start, stop; char *path;
+
+	KUNIT_EXPECT_EQ(test,
+		tlob_parse_uprobe_line(buf, &thr, &path, &start, &stop),
+		-EINVAL);
+}
+
+static struct kunit_case tlob_parse_uprobe_cases[] = {
+	KUNIT_CASE(tlob_parse_decimal_offsets),
+	KUNIT_CASE(tlob_parse_hex_offsets),
+	KUNIT_CASE(tlob_parse_path_with_colon),
+	KUNIT_CASE(tlob_parse_zero_threshold),
+	KUNIT_CASE(tlob_parse_empty_path),
+	KUNIT_CASE(tlob_parse_too_few_fields),
+	KUNIT_CASE(tlob_parse_negative_offset),
+	{}
+};
+
+static struct kunit_suite tlob_parse_uprobe_suite = {
+	.name       = "tlob_parse_uprobe",
+	.test_cases = tlob_parse_uprobe_cases,
+};
+
+kunit_test_suites(&tlob_automaton_suite,
+		  &tlob_task_api_suite,
+		  &tlob_sched_integration_suite,
+		  &tlob_trace_output_suite,
+		  &tlob_event_buf_suite,
+		  &tlob_parse_uprobe_suite);
+
+MODULE_DESCRIPTION("KUnit tests for the tlob RV monitor");
+MODULE_LICENSE("GPL");
-- 
2.43.0


  parent reply	other threads:[~2026-04-12 19:28 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-12 19:27 [RFC PATCH 0/4] rv/tlob: Add task latency over budget RV monitor wen.yang
2026-04-12 19:27 ` [RFC PATCH 1/4] rv/tlob: Add tlob model DOT file wen.yang
2026-04-13  8:19   ` Gabriele Monaco
2026-04-12 19:27 ` [RFC PATCH 2/4] rv/tlob: Add tlob deterministic automaton monitor wen.yang
2026-04-13  8:19   ` Gabriele Monaco
2026-04-12 19:27 ` wen.yang [this message]
2026-04-12 19:27 ` [RFC PATCH 4/4] selftests/rv: Add selftest for the tlob monitor wen.yang

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=0a7f41ff8cb13f8601920ead2979db2ee5f2d442.1776020428.git.wen.yang@linux.dev \
    --to=wen.yang@linux.dev \
    --cc=gmonaco@redhat.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-trace-kernel@vger.kernel.org \
    --cc=mathieu.desnoyers@efficios.com \
    --cc=mhiramat@kernel.org \
    --cc=rostedt@goodmis.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