From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.129.124]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id CAF2437C907 for ; Thu, 16 Apr 2026 12:09:54 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=170.10.129.124 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776341397; cv=none; b=O3iiOWN7SVqhp0rCdwVD1eR+wsRsefbHtFZ8n23x0vMaYKWqGsLV8NqTm8C+Z920YefbQODas0mTkvLBNHuOmeFfy7oryVbTnGKpzozpxqMnUSAi3jBm+0atwh2BD2PTdZCGmtBzIz+XtK1sIob57FpIkCuyXjsJjEFLI6jHj98= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776341397; c=relaxed/simple; bh=lhaG5oYF192NoyYnUT9brn1zo0slsoiNd9McGPeCt9o=; h=Message-ID:Subject:From:To:Cc:Date:In-Reply-To:References: Content-Type:MIME-Version; b=oFRYPqVFDqBlbs60DYEmMYpMZarZgwA3ifLJkqZB6GKRJyeaKVcQvKjiTw6Hr5VMXAotkn7idfKtC+CENoG/OpSJbvDth003xQtGOKTEGygtkj7I2tTOYCwYMZ3McHs3uYwyMjH3k3BcipIJktWoAyA2XXDCFgbpAl4Hm51lw7E= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=redhat.com; spf=pass smtp.mailfrom=redhat.com; dkim=pass (1024-bit key) header.d=redhat.com header.i=@redhat.com header.b=SdatXcQf; dkim=pass (2048-bit key) header.d=redhat.com header.i=@redhat.com header.b=WJmSKLFy; arc=none smtp.client-ip=170.10.129.124 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=redhat.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=redhat.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=redhat.com header.i=@redhat.com header.b="SdatXcQf"; dkim=pass (2048-bit key) header.d=redhat.com header.i=@redhat.com header.b="WJmSKLFy" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1776341394; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references:autocrypt:autocrypt; bh=QBeTPIdOmLFHeL5xdgYC5Nuop26QGWWrfcP79AQppPI=; b=SdatXcQfCxY259LT0RAYRWrWf52wMgDT0GWhrHe+D73EJHjw51sF/Fr+66A95yDrim+jPw mc/ufUveZJNmK5o2zZTIlDutsUtTokSchZ480TlvYCjPxMdjs6u2/cWqQS9+DgajZi7HBq Urd3gx1RbBStkrM1tHiPIkLo2offU40= Received: from mail-wm1-f70.google.com (mail-wm1-f70.google.com [209.85.128.70]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-553-Ux34jFeQNRqM-28DOBnhLg-1; Thu, 16 Apr 2026 08:09:52 -0400 X-MC-Unique: Ux34jFeQNRqM-28DOBnhLg-1 X-Mimecast-MFC-AGG-ID: Ux34jFeQNRqM-28DOBnhLg_1776341392 Received: by mail-wm1-f70.google.com with SMTP id 5b1f17b1804b1-488d8deb75fso47416035e9.3 for ; Thu, 16 Apr 2026 05:09:52 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=google; t=1776341391; x=1776946191; darn=vger.kernel.org; h=mime-version:user-agent:content-transfer-encoding:autocrypt :references:in-reply-to:date:cc:to:from:subject:message-id:from:to :cc:subject:date:message-id:reply-to; bh=QBeTPIdOmLFHeL5xdgYC5Nuop26QGWWrfcP79AQppPI=; b=WJmSKLFyUJlY79JfDKxs1PpD2myhOxPC3Lequ+p29+9Pt5q0TitUQXcsLf49dOy+5L mRqi8imY8s7duKkzkK7Tz7XdA0I3bzmo2Y5cKG22786fpnMOJj9b6cAhoUV2IXE+75Fr f0cXYiN35jTHT+bhmc6zKBHbeCNnow/t4/AJ27+BtYyGfa6cB1pwc6cV16f/zavMTAo4 E88JDJq5F4oAPn7yOB6TIuEca9pJAAhkuVzqdibUQ8iYOSwEk24zhz4LtLWf+E8gqQPf jQVKhnuTIZrL3xTHiROYS21wci7a2x//zA+pH0eII9Mgv/ogtEEgxA6RB6aPn9U+CbNe kXYQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1776341391; x=1776946191; h=mime-version:user-agent:content-transfer-encoding:autocrypt :references:in-reply-to:date:cc:to:from:subject:message-id:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=QBeTPIdOmLFHeL5xdgYC5Nuop26QGWWrfcP79AQppPI=; b=TfJ/28L0IbjDv2yjOno6aJTSZd/hz/ohWAejR9C2EU5OPhQA0eAjkCD5az5liIv0eR zsMsYYAySP3ajS4PnZaNBtQvNiFBM5C4ftUcf1f6NuSIOlEUQGRachjjj3MvlPo7UHuP tVQnJ3gzFtFcvyG8G3071UbcGCmTnilgxmtIk8sIo/Poie8CqVcDE6ZmqayydhqCZnXr M6JD/vJk+Mcpj9PV0Ot+D/RLCovfyaH3cKn2T8nv4B3t9v2Ait8YUENlmLy4qiXVQfp/ wTeCxDNZFjbDrw6zLEJyiFokk9je/JKrBMYyxc+5GJ6WHwLiVNg6+BOmTrKIpw6HuE8k xJuw== X-Forwarded-Encrypted: i=1; AFNElJ9KvFqjYMcciP5UbQlcH1Oo4F+V+80cHfdltY5SWpeOom2b79NFDXaF2azxESvOvbitcPeqABs+j8HCZ6w=@vger.kernel.org X-Gm-Message-State: AOJu0YyGeC9euXypW5lIPJpDdWC2wwNb7DtyM5m3XzH91IrFiHKuNzRI 7wT6XgQdMGtWuzsRl5LVc0M9O/2iJO1Y6ONSr9peH4UyPqgVyXUL7KIhAZQeS55/KGk5lkJtHqK cuElAPWMA4/sEbsjDzhbg6jTlb3B1ycju4UKNPRY8dztynBuDJoEeVmQIhSVqAhAp9Q== X-Gm-Gg: AeBDieuMG9pJIuMVXStBDO19DcRNoddq9eYcuRuug48PpE4FUMCKAc91CLOxg6n8X03 IeJg+Av+wVOXt4RYqrWDlDYquNlfef5PKTjxmmmfWcCLUHDGCQb3uzv0r1I3fBSEb2LYWhKEcgk vhv6HTqWscQq0kKnfP4FGqfrFOwdWvbbXlX1Qy1QwoZfXHSNMzsA0999Yb6wK+Ztkg7cL+he2By 1mByPta0iDgKccjkgwTAGe1aAfgPulUnjQAv1ZOD/HtPXwzHpenWidY3O6vnx8Yjj8fDbJXb34Q 25wLJeThAX4RJr0qRldrZBhFWyQolY3rLn60smxSN3rm56/AfaAbdftxJ3GwRidcjZCpxFMnhau i5DAznu5yimmQgoVWWbUTb29kJNuAfwrxcstPt6RZQx1Uv0BVqyN3MsMMGBM8+VQXoK21c4+Swx wyZwwbaKhRK7Oiplm9su0M1Tesow== X-Received: by 2002:a05:600c:8585:b0:488:ba19:da25 with SMTP id 5b1f17b1804b1-488d6822a0bmr256272635e9.12.1776341390500; Thu, 16 Apr 2026 05:09:50 -0700 (PDT) X-Received: by 2002:a05:600c:8585:b0:488:ba19:da25 with SMTP id 5b1f17b1804b1-488d6822a0bmr256272095e9.12.1776341389745; Thu, 16 Apr 2026 05:09:49 -0700 (PDT) Received: from gmonaco-thinkpadt14gen3.rmtit.csb (212-8-243-115.hosted-by-worldstream.net. [212.8.243.115]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-488f58163cbsm49018055e9.1.2026.04.16.05.09.48 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 16 Apr 2026 05:09:49 -0700 (PDT) Message-ID: Subject: Re: [RFC PATCH 3/4] rv/tlob: Add KUnit tests for the tlob monitor From: Gabriele Monaco To: wen.yang@linux.dev Cc: linux-trace-kernel@vger.kernel.org, linux-kernel@vger.kernel.org, Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers Date: Thu, 16 Apr 2026 14:09:47 +0200 In-Reply-To: <0a7f41ff8cb13f8601920ead2979db2ee5f2d442.1776020428.git.wen.yang@linux.dev> References: <0a7f41ff8cb13f8601920ead2979db2ee5f2d442.1776020428.git.wen.yang@linux.dev> Autocrypt: addr=gmonaco@redhat.com; prefer-encrypt=mutual; keydata=mDMEZuK5YxYJKwYBBAHaRw8BAQdAmJ3dM9Sz6/Hodu33Qrf8QH2bNeNbOikqYtxWFLVm0 1a0JEdhYnJpZWxlIE1vbmFjbyA8Z21vbmFjb0BrZXJuZWwub3JnPoiZBBMWCgBBFiEEysoR+AuB3R Zwp6j270psSVh4TfIFAmjKX2MCGwMFCQWjmoAFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgk Q70psSVh4TfIQuAD+JulczTN6l7oJjyroySU55Fbjdvo52xiYYlMjPG7dCTsBAMFI7dSL5zg98I+8 cXY1J7kyNsY6/dcipqBM4RMaxXsOtCRHYWJyaWVsZSBNb25hY28gPGdtb25hY29AcmVkaGF0LmNvb T6InAQTFgoARAIbAwUJBaOagAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgBYhBMrKEfgLgd0WcK eo9u9KbElYeE3yBQJoymCyAhkBAAoJEO9KbElYeE3yjX4BAJ/ETNnlHn8OjZPT77xGmal9kbT1bC1 7DfrYVISWV2Y1AP9HdAMhWNAvtCtN2S1beYjNybuK6IzWYcFfeOV+OBWRDQ== Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable User-Agent: Evolution 3.58.3 (3.58.3-1.fc43) Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 On Mon, 2026-04-13 at 03:27 +0800, wen.yang@linux.dev wrote: > From: Wen Yang >=20 > 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. >=20 > 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. >=20 > Signed-off-by: Wen Yang I was considering adding Kunit tests and thought to have them a bit more integrated ([1] if you want to have a peek before I submit it for RFC, mind= it's a bit raw). The problem with reimplementing the da_handle_event() is that you are in fa= ct validating only the model matrix, several other things could go wrong befor= e you get there (whether the monitor was started properly, other things you might= be doing from the tracepoint handler before you handle events, etc.). Also, I believe it's a bit of an overkill to validate every single transiti= on like this, especially considering the work once you update the model for whatever reason. One meaningful thing to validate is that a certain sequence of events with = a certain timing causes a violation (or if you want, that a good sequence doe= s not), for instance. But that's just my opinion, of course. Thanks, Gabriele > --- > =C2=A0kernel/trace/rv/Makefile=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 |=C2=A0= =C2=A0=C2=A0 1 + > =C2=A0kernel/trace/rv/monitors/tlob/.kunitconfig |=C2=A0=C2=A0=C2=A0 5 + > =C2=A0kernel/trace/rv/monitors/tlob/Kconfig=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= |=C2=A0=C2=A0 12 + > =C2=A0kernel/trace/rv/monitors/tlob/tlob.c=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0 |=C2=A0=C2=A0=C2=A0 1 + > =C2=A0kernel/trace/rv/monitors/tlob/tlob_kunit.c | 1194 +++++++++++++++++= +++ > =C2=A05 files changed, 1213 insertions(+) > =C2=A0create mode 100644 kernel/trace/rv/monitors/tlob/.kunitconfig > =C2=A0create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.c >=20 > 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) +=3D monitors/nrp/nrp.o > =C2=A0obj-$(CONFIG_RV_MON_SSSW) +=3D monitors/sssw/sssw.o > =C2=A0obj-$(CONFIG_RV_MON_OPID) +=3D monitors/opid/opid.o > =C2=A0obj-$(CONFIG_RV_MON_TLOB) +=3D monitors/tlob/tlob.o > +obj-$(CONFIG_TLOB_KUNIT_TEST) +=3D monitors/tlob/tlob_kunit.o > =C2=A0# Add new monitors here > =C2=A0obj-$(CONFIG_RV_REACTORS) +=3D rv_reactors.o > =C2=A0obj-$(CONFIG_RV_REACT_PRINTK) +=3D 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=3Dy > +CONFIG_KUNIT=3Dy > +CONFIG_RV=3Dy > +CONFIG_RV_MON_TLOB=3Dy > +CONFIG_TLOB_KUNIT_TEST=3Dy > 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 > =C2=A0 =C2=A0 For further information, see: > =C2=A0 =C2=A0=C2=A0=C2=A0 Documentation/trace/rv/monitor_tlob.rst > =C2=A0 > +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 > + =C2=A0 Enable KUnit in-kernel unit tests for the tlob RV monitor. > + > + =C2=A0 Tests cover automaton state transitions, the hash table helpers, > + =C2=A0 the start/stop task interface, and the event ring buffer includi= ng > + =C2=A0 overflow handling and wakeup behaviour. > + > + =C2=A0 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, > =C2=A0 *path_out=C2=A0 =3D buf + n; > =C2=A0 return 0; > =C2=A0} > +EXPORT_SYMBOL_IF_KUNIT(tlob_parse_uprobe_line); > =C2=A0 > =C2=A0static ssize_t tlob_monitor_write(struct file *file, > =C2=A0 =C2=A0 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:=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 DA tr= ansition table coverage. > + * tlob_task_api:=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 = tlob_start_task()/tlob_stop_task() lifecycle and > errors. > + * tlob_sched_integration: on/off-CPU accounting across real context > switches. > + * tlob_trace_output:=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 tlob_budget_exceeded= tracepoint field > verification. > + * tlob_event_buf:=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ring = buffer push, overflow, and wakeup. > + * tlob_parse_uprobe:=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 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 r= eal > + * filesystem, which is outside the scope of unit tests. It is covered b= y the > + * uprobe_duplicate_offset case in tools/testing/selftests/rv/test_tlob.= sh. > + */ > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > + > +/* > + * Pull in the rv tracepoint declarations so that > + * register_trace_tlob_budget_exceeded() is available. > + * No CREATE_TRACE_POINTS here=C2=A0 --=C2=A0 the tracepoint implementat= ion lives in > rv.c. > + */ > +#include > + > +#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 touch= ing > + * 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 =3D (enum states_tlob)da_mon->curr_state; > + enum states_tlob next_state =3D > + (enum states_tlob)automaton_tlob.function[curr_state][event]; > + > + if (next_state !=3D INVALID_STATE) > + da_mon->curr_state =3D 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 =3D { .curr_state =3D 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 =3D { .curr_state =3D 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 =3D { .curr_state =3D 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 =3D { .curr_state =3D 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 =3D { .curr_state =3D 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 =3D { .curr_state =3D 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 =3D { .curr_state =3D 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 =3D { .curr_state =3D 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[] =3D { > + switch_out_tlob, sched_wakeup_tlob, > + }; > + unsigned int i; > + > + for (i =3D 0; i < ARRAY_SIZE(events); i++) { > + struct da_monitor mon =3D { .curr_state =3D off_cpu_tlob }; > + > + da_handle_event_tlob(&mon, events[i]); > + KUNIT_EXPECT_EQ_MSG(test, (int)mon.curr_state, > + =C2=A0=C2=A0=C2=A0 (int)off_cpu_tlob, > + =C2=A0=C2=A0=C2=A0 "event %u should self-loop in off_cpu", > + =C2=A0=C2=A0=C2=A0 events[i]); > + } > +} > + > +/* on_cpu self-loops on sched_wakeup. */ > +static void tlob_on_cpu_self_loops(struct kunit *test) > +{ > + struct da_monitor mon =3D { .curr_state =3D on_cpu_tlob }; > + > + da_handle_event_tlob(&mon, sched_wakeup_tlob); > + KUNIT_EXPECT_EQ_MSG(test, (int)mon.curr_state, (int)on_cpu_tlob, > + =C2=A0=C2=A0=C2=A0 "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[] =3D { > + switch_in_tlob, switch_out_tlob, sched_wakeup_tlob, > + }; > + unsigned int i; > + > + for (i =3D 0; i < ARRAY_SIZE(events); i++) { > + struct da_monitor mon =3D { .curr_state =3D unmonitored_tlob }; > + > + da_handle_event_tlob(&mon, events[i]); > + KUNIT_EXPECT_EQ_MSG(test, (int)mon.curr_state, > + =C2=A0=C2=A0=C2=A0 (int)unmonitored_tlob, > + =C2=A0=C2=A0=C2=A0 "event %u should self-loop in > unmonitored", > + =C2=A0=C2=A0=C2=A0 events[i]); > + } > +} > + > +static void tlob_full_happy_path(struct kunit *test) > +{ > + struct da_monitor mon =3D { .curr_state =3D 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 =3D { .curr_state =3D 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 =3D 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[] =3D { > + 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 =3D { > + .name=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D "tlob_automaton", > + .test_cases =3D tlob_automaton_cases, > +}; > + > +/* > + * Suite 2: task registration API > + */ > + > +/* Basic start/stop cycle */ > +static void tlob_start_stop_ok(struct kunit *test) > +{ > + int ret; > + > + ret =3D 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);=C2=A0 /* 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 =3D 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 =3D tlob_stop_task(current); > + KUNIT_EXPECT_TRUE(test, ret =3D=3D 0 || ret =3D=3D -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 =3D 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 =3D kunit_kcalloc(test, TLOB_MAX_MONITORED, > + =C2=A0=C2=A0=C2=A0=C2=A0 sizeof(*ctxs), GFP_KERNEL); > + KUNIT_ASSERT_NOT_NULL(test, ctxs); > + > + threads =3D 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 =3D 0; i < TLOB_MAX_MONITORED; i++) { > + init_completion(&ctxs[i].start); > + init_completion(&ctxs[i].done); > + > + threads[i] =3D kthread_run(tlob_waiter_fn, &ctxs[i], > + "tlob_waiter_%d", i); > + if (IS_ERR(threads[i])) { > + KUNIT_FAIL(test, "kthread_run failed at i=3D%d", i); > + threads[i] =3D NULL; > + goto cleanup; > + } > + get_task_struct(threads[i]); > + > + ret =3D tlob_start_task(threads[i], 10000000, NULL, 0); > + if (ret !=3D 0) { > + KUNIT_FAIL(test, "tlob_start_task failed at i=3D%d: > %d", > + =C2=A0=C2=A0 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 =3D 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 =3D 0; i < TLOB_MAX_MONITORED; i++) { > + if (!threads[i]) > + break; > + tlob_stop_task(threads[i]); > + complete(&ctxs[i].start); > + } > + for (i =3D 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 =3D 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 =3D { .hold_ms =3D 80 }; > + struct task_struct *holder; > + ktime_t t0; > + int ret; > + > + mutex_init(&ctx.lock); > + init_completion(&ctx.ready); > + > + holder =3D 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 =3D 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 =3D 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 =3D tlob_start_task(current, 1000 /* 1 ms */, NULL, 0); > + KUNIT_ASSERT_EQ(test, ret, 0); > + > + /* Busy-spin 50 ms - 50x the budget */ > + t0 =3D 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 =3D kthread_run(tlob_dummy_fn, &done1, "tlob_dummy1"); > + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, t1); > + get_task_struct(t1); > + > + t2 =3D 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 =3D 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 =3D (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[] =3D { > + 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 =3D { > + .name=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D "tlob_task_api", > + .suite_init =3D tlob_task_api_suite_init, > + .suite_exit =3D tlob_task_api_suite_exit, > + .test_cases =3D 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 =3D 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 =3D 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 =3D 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 =3D 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 =3D 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 =3D 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 =3D 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 =3D tlob_stop_task(target); > + KUNIT_EXPECT_TRUE(test, ret =3D=3D 0 || ret =3D=3D -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[] =3D { > + KUNIT_CASE(tlob_sched_switch_accounting), > + KUNIT_CASE(tlob_monitor_other_task), > + {} > +}; > + > +static struct kunit_suite tlob_sched_integration_suite =3D { > + .name=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D "tlob_sched_integration", > + .suite_init =3D tlob_sched_suite_init, > + .suite_exit =3D tlob_sched_suite_exit, > + .test_cases =3D 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, > + =C2=A0=C2=A0 struct task_struct *task, u64 threshold_us, > + =C2=A0=C2=A0 u64 on_cpu_us, u64 off_cpu_us, > + =C2=A0=C2=A0 u32 switches, bool state_is_on_cpu, u64 tag) > +{ > + struct tlob_exceeded_capture *cap =3D data; > + > + /* Only capture the first event to avoid races. */ > + if (atomic_cmpxchg(&cap->fired, 0, 1) !=3D 0) > + return; > + > + cap->pid =3D task->pid; > + cap->threshold_us =3D threshold_us; > + cap->on_cpu_us =3D on_cpu_us; > + cap->off_cpu_us =3D off_cpu_us; > + cap->switches =3D switches; > + cap->state_is_on_cpu =3D state_is_on_cpu; > + cap->tag =3D tag; > +} > + > +/* > + * Arm a 2 ms budget and busy-spin for 60 ms. Verify the tracepoint fire= s > + * once with matching threshold, correct pid, and total time >=3D 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 =3D {}; > + const u64 threshold_us =3D 2000; /* 2 ms */ > + ktime_t t0; > + int ret; > + > + atomic_set(&cap.fired, 0); > + > + ret =3D register_trace_tlob_budget_exceeded(probe_tlob_budget_exceeded, > + =C2=A0 &cap); > + KUNIT_ASSERT_EQ(test, ret, 0); > + > + ret =3D tlob_start_task(current, threshold_us, NULL, 0); > + KUNIT_ASSERT_EQ(test, ret, 0); > + > + /* Busy-spin 60 ms=C2=A0 --=C2=A0 30x the budget */ > + t0 =3D 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 =3D=3D false, switches >=3D 1, off_cpu_us > 0. > + */ > +static void tlob_trace_budget_exceeded_off_cpu(struct kunit *test) > +{ > + struct tlob_exceeded_capture cap =3D {}; > + struct tlob_holder_ctx ctx =3D { .hold_ms =3D 80 }; > + struct task_struct *holder; > + const u64 threshold_us =3D 10000; /* 10 ms */ > + ktime_t t0; > + int ret; > + > + atomic_set(&cap.fired, 0); > + > + mutex_init(&ctx.lock); > + init_completion(&ctx.ready); > + > + holder =3D kthread_run(tlob_holder_fn, &ctx, "tlob_holder2_kunit"); > + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, holder); > + wait_for_completion(&ctx.ready); > + > + ret =3D register_trace_tlob_budget_exceeded(probe_tlob_budget_exceeded, > + =C2=A0 &cap); > + KUNIT_ASSERT_EQ(test, ret, 0); > + > + ret =3D tlob_start_task(current, threshold_us, NULL, 0); > + KUNIT_ASSERT_EQ(test, ret, 0); > + > + /* Phase 1: ~1 ms on-CPU */ > + t0 =3D 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[] =3D { 500, 1000, 3000 }; > + unsigned int i; > + > + for (i =3D 0; i < ARRAY_SIZE(thresholds); i++) { > + struct tlob_exceeded_capture cap =3D {}; > + ktime_t t0; > + int ret; > + > + atomic_set(&cap.fired, 0); > + > + ret =3D register_trace_tlob_budget_exceeded( > + probe_tlob_budget_exceeded, &cap); > + KUNIT_ASSERT_EQ(test, ret, 0); > + > + ret =3D tlob_start_task(current, thresholds[i], NULL, 0); > + KUNIT_ASSERT_EQ(test, ret, 0); > + > + /* Spin for 20x the threshold to ensure timer fires */ > + t0 =3D ktime_get(); > + while (ktime_us_delta(ktime_get(), t0) < > + =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (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], > + =C2=A0=C2=A0=C2=A0 "threshold mismatch for entry %u", i); > + } > +} > + > +static int tlob_trace_suite_init(struct kunit_suite *suite) > +{ > + int ret; > + > + ret =3D 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[] =3D { > + 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 =3D { > + .name=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D "tlob_trace_output", > + .suite_init =3D tlob_trace_suite_init, > + .suite_exit =3D tlob_trace_suite_exit, > + .test_cases =3D 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 =3D kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); > + if (!priv) > + return NULL; > + > + ring =3D &priv->ring; > + > + ring->page =3D kunit_kzalloc(test, sizeof(struct tlob_mmap_page), > + =C2=A0=C2=A0 GFP_KERNEL); > + if (!ring->page) > + return NULL; > + > + ring->data =3D kunit_kzalloc(test, cap * sizeof(struct tlob_event), > + =C2=A0=C2=A0 GFP_KERNEL); > + if (!ring->data) > + return NULL; > + > + ring->mask=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0 =3D cap - 1; > + ring->page->capacity=C2=A0 =3D cap; > + ring->page->version=C2=A0=C2=A0 =3D 1; > + ring->page->data_offset =3D PAGE_SIZE; /* nominal; not used in tests */ > + ring->page->record_size =3D 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 =3D { > + .tid =3D 1234, > + .threshold_us =3D 5000, > + .on_cpu_us =3D 3000, > + .off_cpu_us =3D 2000, > + .switches =3D 3, > + .state =3D 1, > + }; > + struct tlob_event out =3D {}; > + u32 tail; > + > + priv =3D alloc_priv_kunit(test, TLOB_RING_DEFAULT_CAP); > + KUNIT_ASSERT_NOT_NULL(test, priv); > + > + ring =3D &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,=C2=A0=C2=A0 0ull); > + > + /* Dequeue manually */ > + tail =3D ring->page->data_tail; > + out=C2=A0 =3D ring->data[tail & ring->mask]; > + ring->page->data_tail =3D tail + 1; > + > + KUNIT_EXPECT_EQ(test, out.tid,=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0 in.tid); > + KUNIT_EXPECT_EQ(test, out.threshold_us, in.threshold_us); > + KUNIT_EXPECT_EQ(test, out.on_cpu_us,=C2=A0=C2=A0=C2=A0 in.on_cpu_us); > + KUNIT_EXPECT_EQ(test, out.off_cpu_us,=C2=A0=C2=A0 in.off_cpu_us); > + KUNIT_EXPECT_EQ(test, out.switches,=C2=A0=C2=A0=C2=A0=C2=A0 in.switches= ); > + KUNIT_EXPECT_EQ(test, out.state,=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0 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 =3D=3D 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 =3D {}; > + struct tlob_event out =3D {}; > + const u32 cap =3D TLOB_RING_MIN_CAP; > + u32 i; > + > + priv =3D alloc_priv_kunit(test, cap); > + KUNIT_ASSERT_NOT_NULL(test, priv); > + > + ring =3D &priv->ring; > + > + /* Push cap + 1 records; tid encodes the sequence */ > + for (i =3D 0; i <=3D cap; i++) { > + ntf.tid=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D i; > + ntf.threshold_us =3D (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,=C2=A0=C2=A0 1ull); > + > + /* Oldest surviving record must be the first one pushed (tid =3D=3D 0) = */ > + out =3D ring->data[ring->page->data_tail & ring->mask]; > + KUNIT_EXPECT_EQ(test, out.tid, 0u); > + > + /* Drain the ring; the last record must have tid =3D=3D cap - 1 */ > + for (i =3D 0; i < cap; i++) { > + u32 tail =3D ring->page->data_tail; > + > + out =3D ring->data[tail & ring->mask]; > + ring->page->data_tail =3D 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 =3D alloc_priv_kunit(test, TLOB_RING_DEFAULT_CAP); > + KUNIT_ASSERT_NOT_NULL(test, priv); > + > + ring =3D &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,=C2=A0=C2=A0 0ull); > +} > + > +/* A kthread blocks on wait_event_interruptible(); pushing one record mu= st > + * 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 =3D arg; > + struct tlob_ring *ring =3D &ctx->priv->ring; > + > + complete(&ctx->ready); > + > + wait_event_interruptible(ctx->priv->waitq, > + smp_load_acquire(&ring->page->data_head) !=3D > + READ_ONCE(ring->page->data_tail) || > + kthread_should_stop()); > + > + if (smp_load_acquire(&ring->page->data_head) !=3D > + =C2=A0=C2=A0=C2=A0 READ_ONCE(ring->page->data_tail)) > + ctx->woke =3D 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 =3D { .tid =3D 99 }; > + long timeout; > + > + priv =3D alloc_priv_kunit(test, TLOB_RING_DEFAULT_CAP); > + KUNIT_ASSERT_NOT_NULL(test, priv); > + > + init_completion(&ctx.ready); > + init_completion(&ctx.done); > + ctx.priv =3D priv; > + ctx.woke =3D 0; > + > + t =3D 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=C2=A0 --=C2=A0 must wake the waiter */ > + tlob_event_push_kunit(priv, &ev); > + > + timeout =3D 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[] =3D { > + 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 =3D { > + .name=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D "tlob_event_buf", > + .test_cases =3D 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[] =3D "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,=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (u64)5000); > + KUNIT_EXPECT_EQ(test, start,=C2=A0=C2=A0=C2=A0 (loff_t)4768); > + KUNIT_EXPECT_EQ(test, stop,=C2=A0=C2=A0=C2=A0=C2=A0 (loff_t)4848); > + KUNIT_EXPECT_STREQ(test, path,=C2=A0 "/usr/bin/myapp"); > +} > + > +/* Happy path: 0x-prefixed hex offsets. */ > +static void tlob_parse_hex_offsets(struct kunit *test) > +{ > + char buf[] =3D "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,=C2=A0=C2=A0 (loff_t)0x12a0); > + KUNIT_EXPECT_EQ(test, stop,=C2=A0=C2=A0=C2=A0 (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[] =3D "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[] =3D "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[] =3D "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[] =3D "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[] =3D "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[] =3D { > + 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 =3D { > + .name=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D "tlob_parse_uprobe", > + .test_cases =3D tlob_parse_uprobe_cases, > +}; > + > +kunit_test_suites(&tlob_automaton_suite, > + =C2=A0 &tlob_task_api_suite, > + =C2=A0 &tlob_sched_integration_suite, > + =C2=A0 &tlob_trace_output_suite, > + =C2=A0 &tlob_event_buf_suite, > + =C2=A0 &tlob_parse_uprobe_suite); > + > +MODULE_DESCRIPTION("KUnit tests for the tlob RV monitor"); > +MODULE_LICENSE("GPL");