pub struct Assert {Show 22 fields
pub not_starved: Option<bool>,
pub isolation: Option<bool>,
pub max_gap_ms: Option<u64>,
pub max_spread_pct: Option<f64>,
pub max_throughput_cv: Option<f64>,
pub min_work_rate: Option<f64>,
pub max_p99_wake_latency_ns: Option<u64>,
pub max_wake_latency_cv: Option<f64>,
pub min_iteration_rate: Option<f64>,
pub max_migration_ratio: Option<f64>,
pub max_imbalance_ratio: Option<f64>,
pub max_local_dsq_depth: Option<u32>,
pub fail_on_stall: Option<bool>,
pub sustained_samples: Option<usize>,
pub max_fallback_rate: Option<f64>,
pub max_keep_last_rate: Option<f64>,
pub enforce_monitor_thresholds: bool,
pub min_page_locality: Option<f64>,
pub max_cross_node_migration_ratio: Option<f64>,
pub max_slow_tier_ratio: Option<f64>,
pub expect_scx_bpf_error_contains: Option<&'static str>,
pub expect_scx_bpf_error_matches: Option<&'static str>,
}Expand description
Unified assertion configuration. Carries both worker checks and
monitor thresholds as a single composable type. Each Option field
acts as an override — None means “inherit from parent layer”.
Construct via Assert::NO_OVERRIDES (preferred const baseline)
or Assert::default_checks (currently aliases NO_OVERRIDES);
chain builder methods on either base (all builders are const fn
except Assert::expect_scx_bpf_error_matches, which compiles a
regex at construction). Use the resulting Assert value as the
assert field of a Scheduler
declared via declare_scheduler! — the
macro accepts assert = Assert::NO_OVERRIDES.foo()-style chains
at the scheduler level. The #[ktstr_test] proc macro does NOT
accept an assert = … attribute on test entries; per-field
attribute shortcuts (max_gap_ms = N, not_starved = true, …)
compose into the equivalent struct literal at expansion time.
Merge order: Assert::default_checks() -> Scheduler.assert -> per-test assert.
default_checks() is NO_OVERRIDES — all assertions are opt-in.
// Scheduler opts into imbalance checking.
let sched_assert = Assert::NO_OVERRIDES.max_imbalance_ratio(5.0);
// Merge: defaults <- scheduler <- test.
let merged = Assert::default_checks()
.merge(&sched_assert)
.merge(&Assert::NO_OVERRIDES.max_gap_ms(5000));
assert_eq!(merged.not_starved, None); // not opted in
assert_eq!(merged.max_imbalance_ratio, Some(5.0)); // from sched
assert_eq!(merged.max_gap_ms, Some(5000)); // from test§Serde roundtrip — covers the 20 threshold/check fields only
The serde derive at the struct level covers the 20 threshold +
flag fields (every Option<bool/u64/f64/u32/usize> plus the bare
enforce_monitor_thresholds: bool). The two reproducer-matcher
fields (Self::expect_scx_bpf_error_contains and
Self::expect_scx_bpf_error_matches) carry #[serde(skip)]
because their &'static str shape cannot round-trip through a
borrowed deserializer — see each field’s doc for the rationale.
Sidecar consumers comparing
threshold config across runs treat reproducer matcher strings as
part of the test identity (encoded by name in the sidecar key),
not part of the threshold payload, so the skip is operationally
transparent today.
Fields§
§not_starved: Option<bool>Enable starvation, fairness spread, and gap checks across
worker reports. Some(true) enables, Some(false) explicitly
disables (overriding any enabling merge from a lower layer),
None inherits from the merge parent.
isolation: Option<bool>Enable per-worker CPU isolation checks (ensure workers remain
within their assigned cpuset). Same tri-state semantics as
not_starved.
max_gap_ms: Option<u64>Max per-worker scheduling gap in milliseconds. Fails the assertion if any worker’s longest off-CPU stretch exceeds this.
max_spread_pct: Option<f64>Max per-cgroup fairness spread as a percentage. Fails if the range between the most- and least-served workers exceeds this fraction of their mean.
max_throughput_cv: Option<f64>Max coefficient of variation for work_units/cpu_time across workers. Catches placement unfairness where some workers get less CPU than others.
min_work_rate: Option<f64>Minimum work_units per CPU-second. Catches cases where all workers are equally slow (CV passes but absolute throughput is too low).
max_p99_wake_latency_ns: Option<u64>Max p99 wake latency in NANOSECONDS. Fails if the pooled
p99 across every worker’s wake_latencies_ns exceeds this.
§Unit-name gotcha
The threshold is _ns, but the paired reporting field on
CgroupStats::p99_wake_latency_us and the roll-up
ScenarioStats::worst_p99_wake_latency_us are
MICROSECONDS. The two surfaces are intentionally split:
- the threshold uses NS for precision (typical scheduler wake latencies are single-digit µs, so sub-µs resolution matters for regression gates);
- the reporting fields use US for readability in
stats compare/ dashboard output.
Both are computed from the same underlying
WorkerReport::wake_latencies_ns samples — see
assert_benchmarks for the threshold path and
assert_not_starved for the reporting path. A bare
comparison of max_p99_wake_latency_ns against
CgroupStats::p99_wake_latency_us is a unit-mismatch bug;
assert_benchmarks never does this — it consumes the raw
wake_latencies_ns directly — and
assert_p99_ns_threshold_compares_against_ns_latencies pins
that contract.
max_wake_latency_cv: Option<f64>Max wake latency coefficient of variation. Fails if CV exceeds this.
min_iteration_rate: Option<f64>Minimum iterations per wall-clock second. Fails if any worker is below.
max_migration_ratio: Option<f64>Max migration ratio (migrations/iterations). Fails if any cgroup exceeds this.
max_imbalance_ratio: Option<f64>Max nr_running / LLC imbalance ratio observed by the monitor.
Fails if the worst sample’s imbalance exceeds this.
max_local_dsq_depth: Option<u32>Max local DSQ depth observed by the monitor. Fails if any sampled CPU’s local DSQ grew beyond this.
fail_on_stall: Option<bool>Treat a stall verdict from the monitor as a hard failure. Same
tri-state semantics as not_starved.
sustained_samples: Option<usize>Minimum number of consecutive samples that must exceed the monitor threshold before a verdict is raised. Smooths out single-sample spikes.
max_fallback_rate: Option<f64>Max select_cpu_fallback rate (events/sec). Fails if the
scx event counter delta over the run exceeds this rate.
max_keep_last_rate: Option<f64>Max keep_last rate (events/sec). Fails if the scx event
counter delta over the run exceeds this rate.
enforce_monitor_thresholds: boolPromote monitor threshold violations from report-only to
pass/fail. When false (the default), the monitor still walks
every sample and records every violation in the verdict’s
details, but the verdict’s passed stays true. Tests that
want monitor violations to fail the run call
Self::with_monitor_defaults, which populates each monitor
threshold from MonitorThresholds::new()
and sets this flag to true.
min_page_locality: Option<f64>Minimum fraction of pages on the expected NUMA node(s) (0.0-1.0).
Expected nodes are derived from the worker’s
MemPolicy at evaluation time.
Fails if the observed locality fraction falls below this.
max_cross_node_migration_ratio: Option<f64>Maximum ratio of NUMA-node-migrated pages to total allocated
pages (0.0-1.0). Distinct from max_migration_ratio
which measures CPU migrations per iteration. Fails if the
observed migration ratio exceeds this.
max_slow_tier_ratio: Option<f64>Maximum fraction of pages on slow-tier (memory-only) NUMA nodes
(0.0-1.0). For CXL memory tiering tests: fails if more than
this fraction of pages land on memory-only nodes. Requires
slow_tier_nodes to be set at evaluation time.
expect_scx_bpf_error_contains: Option<&'static str>Reproducer-mode literal-substring matcher for the captured
scx_bpf_error text. When Some(literal), the eval pipeline
scans the combined scheduler log + sched_ext dump for a
substring match against literal and fails the test if the
substring is absent.
Use this for the common case of pinning an exact error
fragment like apply_cell_config returned -EINVAL without
having to escape regex metacharacters. For pattern matching
with anchors / character classes / wildcards, use
Self::expect_scx_bpf_error_matches instead — the two
fields are orthogonal and can both be set (both must match).
Requires the entry’s expect_err = true — a reproducer
matcher only fires on expected-error tests; setting this on
a passing test would assert “the test passed AND contained
this error text,” which is contradictory. The eval-time
validation rejects that combination with a clear diagnostic.
Stored as &'static str so the const-fn Self::merge
composes without cloning. Empty strings are rejected at
evaluation (an empty literal would silently match every
message and turn this assertion into a no-op).
#[serde(skip)] because the field’s &'static str shape
cannot round-trip through a borrowed deserializer (no source-
string lifetime to bind to). Reproducer matcher strings are
test-author static literals carried in the test definition
itself, not per-run data the sidecar needs to roundtrip — so
skipping them on the wire keeps the JSON shape clean without
losing any sidecar-consumer functionality. Skipped fields
default to None on deserialize per Option::default().
The Option<Cow<'static, str>> alternative that WOULD
roundtrip is rejected because it cascade-breaks
Scheduler::assert’s const-fn (Cow has a heap destructor,
which breaks the const-fn assignment path used by
declare_scheduler! macro statics). A future decomposition
into a ReproducerMatchers sub-config could revisit this if
sidecar-loaded test definitions ever need the strings to
survive end-to-end.
expect_scx_bpf_error_matches: Option<&'static str>Reproducer-mode regex matcher for the captured scx_bpf_error
text. When Some(pattern), the eval pipeline compiles the
pattern via the regex crate, scans the combined scheduler
log + sched_ext dump, and fails the test if the regex does
not match anywhere in the corpus.
The pattern is a full regex — special characters
(. * + ? ( ) [ ] { } | ^ $ \) carry their regex meaning.
For literal-substring matching, prefer
Self::expect_scx_bpf_error_contains to avoid escape
footguns. The captured corpus is the multi-line concatenation
of the scheduler log and --- sched_ext dump ---; the regex
crate’s default flags apply: ^ and $ anchor to the start /
end of the WHOLE corpus (not individual lines), and . does
NOT cross \n. Opt in to line-level anchoring with (?m)
(e.g. (?m)^apply_cell_config$) and to newline-spanning
. with (?s). A bare apply_cell_config matches the
token anywhere in the corpus.
Requires the entry’s expect_err = true — same rationale
as Self::expect_scx_bpf_error_contains. Patterns are
validated at construction: empty literals, invalid regex
syntax, and any pattern satisfying is_match("") all
panic via the Self::expect_scx_bpf_error_matches
builder. The is_match("") predicate catches two
no-op classes with one check: patterns that match every
position (e.g. a?, .*, (?:)) trivially pass against
any corpus, and patterns that match only the empty string
(e.g. ^$) trivially fail against any non-empty corpus —
real captured scheduler-output corpora are non-empty, so
both classes are equally no-op pins. Bare \b (word
boundary) slips the gate because the empty string
contains no word characters; see the builder docstring
for the operator-direction.
#[serde(skip)] for the same reason as
Self::expect_scx_bpf_error_contains above: &'static str
doesn’t roundtrip + the matcher pattern is test-definition
data, not sidecar-roundtrip data. Skipped fields default to
None on deserialize.
Implementations§
Source§impl Assert
impl Assert
Sourcepub const NO_OVERRIDES: Assert
pub const NO_OVERRIDES: Assert
Identity element for Assert::merge: every field is None,
so neither side of a merge with NO_OVERRIDES is altered.
Identical to the value returned by Self::default_checks —
the const is for spread-into-struct-literal composition, the
const fn is the method-style entry point.
Sourcepub fn format_human(&self) -> String
pub fn format_human(&self) -> String
Human-readable multi-line dump of every threshold field. Each
field renders as name: value (none when the option is
None, i.e. inherited or unset). Used by
cargo ktstr show-thresholds <test> to expose the resolved
merged Assert (default_checks().merge(&entry.scheduler.assert). merge(&entry.assert)) without forcing the operator to read
the Debug impl or source. Output is a sequence of indented
row lines ending with a newline; the caller owns any
outer section header (the show-thresholds CLI already
prints Test: ... / Scheduler: ... lines above the
threshold block, which together establish context — an
additional Resolved assertion thresholds: banner here
would be a redundant third header).
Sourcepub const fn default_checks() -> Assert
pub const fn default_checks() -> Assert
Baseline of the runtime merge chain
default_checks().merge(&scheduler.assert).merge(&entry.assert).
All checks are off by default — tests opt in to the assertions
they care about via scheduler-level or per-test Assert
overrides.
For spread-into-struct-literal composition
(Assert { not_starved: Some(true), ..Assert::NO_OVERRIDES })
use the equivalent const Self::NO_OVERRIDES; this const fn
is the method-style entry point that pairs with .verdict()
and the builder setters.
Sourcepub fn verdict(self) -> Verdict
pub fn verdict(self) -> Verdict
Build a fresh Verdict under this Assert’s threshold
config. The returned accumulator carries no claim records; call
the typed claim_<field> methods generated by
#[derive(Claim)] on stats structs as
stats.claim_<field>(&mut verdict), or use the
claim! macro on a local/expression, then
call Verdict::into_result to produce the final
AssertResult.
This is the entry point of the pointwise-claim API. The
Assert itself remains pure threshold config and stays
Copy; per-test claims accumulate on the returned Verdict,
which owns its own buffers (details, stats).
let r = Assert::default_checks().verdict().into_result();
assert!(r.is_pass(), "no claims means passing verdict");pub const fn check_not_starved(self) -> Self
pub const fn check_isolation(self) -> Self
pub const fn max_gap_ms(self, ms: u64) -> Self
pub const fn max_spread_pct(self, pct: f64) -> Self
pub const fn max_throughput_cv(self, v: f64) -> Self
pub const fn min_work_rate(self, v: f64) -> Self
pub const fn max_p99_wake_latency_ns(self, v: u64) -> Self
pub const fn max_wake_latency_cv(self, v: f64) -> Self
pub const fn min_iteration_rate(self, v: f64) -> Self
pub const fn max_migration_ratio(self, v: f64) -> Self
pub const fn max_imbalance_ratio(self, v: f64) -> Self
pub const fn max_local_dsq_depth(self, v: u32) -> Self
Sourcepub const fn fail_on_stall(self, v: bool) -> Self
pub const fn fail_on_stall(self, v: bool) -> Self
Control whether a monitor stall verdict fails the assertion.
Sourcepub const fn sustained_samples(self, v: usize) -> Self
pub const fn sustained_samples(self, v: usize) -> Self
Set the number of consecutive over-threshold samples required before the monitor raises a verdict.
pub const fn max_fallback_rate(self, v: f64) -> Self
pub const fn max_keep_last_rate(self, v: f64) -> Self
pub const fn min_page_locality(self, v: f64) -> Self
pub const fn max_cross_node_migration_ratio(self, v: f64) -> Self
pub const fn max_slow_tier_ratio(self, v: f64) -> Self
Sourcepub const fn has_worker_checks(&self) -> bool
pub const fn has_worker_checks(&self) -> bool
True when any worker-level check field is Some. A pure query on
the configured checks — it no longer gates per-cgroup telemetry
(telemetry is built unconditionally per handle in
crate::scenario’s collect path; worker-check assertions are the
only opt-in part). Retained as public API for callers composing an
Assert who need to know whether any worker check is set.
Sourcepub const fn merge(&self, other: &Assert) -> Assert
pub const fn merge(&self, other: &Assert) -> Assert
Merge other on top of self. Each Some field in other
overrides the corresponding field in self; None fields
inherit from self.
Assert::NO_OVERRIDES is the two-sided identity:
x.merge(&NO_OVERRIDES) and NO_OVERRIDES.merge(&x) both yield
x. The runtime composes scheduler- and test-level overrides as
Assert::default_checks().merge(&scheduler.assert).merge(&test.assert),
so a NO_OVERRIDES at either override layer leaves the defaults
untouched – which means “no override,” not “no checks.”
Sourcepub fn assert_cgroup(
&self,
reports: &[WorkerReport],
cpuset: Option<&BTreeSet<usize>>,
) -> AssertResult
pub fn assert_cgroup( &self, reports: &[WorkerReport], cpuset: Option<&BTreeSet<usize>>, ) -> AssertResult
Run the configured worker checks against one cgroup’s reports.
cpuset is the CPU set for isolation checks. numa_nodes is
the NUMA node IDs covered by the cpuset (for page locality and
slow-tier checks). Derive via
TestTopology::numa_nodes_for_cpuset.
Sourcepub fn assert_cgroup_with_numa(
&self,
reports: &[WorkerReport],
cpuset: Option<&BTreeSet<usize>>,
numa_nodes: Option<&BTreeSet<usize>>,
) -> AssertResult
pub fn assert_cgroup_with_numa( &self, reports: &[WorkerReport], cpuset: Option<&BTreeSet<usize>>, numa_nodes: Option<&BTreeSet<usize>>, ) -> AssertResult
Run worker checks with explicit NUMA node set for page locality.
Sourcepub fn assert_page_locality(
&self,
observed: f64,
total_pages: u64,
local_pages: u64,
) -> AssertResult
pub fn assert_page_locality( &self, observed: f64, total_pages: u64, local_pages: u64, ) -> AssertResult
Run NUMA page locality check.
observed is the fraction of pages on expected nodes (0.0-1.0).
total_pages and local_pages are for diagnostics.
Sourcepub fn assert_cross_node_migration(
&self,
migrated_pages: u64,
total_pages: u64,
) -> AssertResult
pub fn assert_cross_node_migration( &self, migrated_pages: u64, total_pages: u64, ) -> AssertResult
Run cross-node migration ratio check.
migrated_pages is the /proc/vmstat numa_pages_migrated delta.
total_pages is total allocated pages from numa_maps.
Sourcepub const fn with_monitor_defaults(self) -> Self
pub const fn with_monitor_defaults(self) -> Self
Opt into pass/fail enforcement for monitor thresholds. Without
this call, monitor violations are reported in the verdict’s
details but do not fail the test. With it, any monitor
threshold violation fails the test.
Also populates any unset monitor-threshold field with the
canonical default from MonitorThresholds::new()
— so a test that only cares about max_keep_last_rate can chain
.max_keep_last_rate(N).with_monitor_defaults() and get the
other four enforced at their canonical defaults.
Sourcepub const fn expect_scx_bpf_error_contains(self, literal: &'static str) -> Self
pub const fn expect_scx_bpf_error_contains(self, literal: &'static str) -> Self
Const-fn builder for Self::expect_scx_bpf_error_contains.
Chains with the other const-fn setters so a scheduler-def or
per-test assertion block can compose
Assert::NO_OVERRIDES.expect_scx_bpf_error_contains(...).check_not_starved().
Empty strings panic at construction (an empty literal would
silently match every message and turn this assertion into a
no-op); pass a non-empty fragment that should appear in the
expected scx_bpf_error message.
§Panics
When literal is empty.
Sourcepub fn expect_scx_bpf_error_matches(self, pattern: &'static str) -> Self
pub fn expect_scx_bpf_error_matches(self, pattern: &'static str) -> Self
Builder for Self::expect_scx_bpf_error_matches. The
pattern is a regex; special characters retain their regex
meaning. For literal-substring matching, prefer
Self::expect_scx_bpf_error_contains to avoid escape
footguns.
Validates the pattern at construction: rejects empty
patterns, rejects invalid regex syntax, and rejects any
pattern that satisfies is_match(""). The empty-string
match predicate catches two related no-op classes:
patterns that match every position (e.g. a?, .*,
(?:)) trivially pass against any corpus, and patterns
that match only the empty string (e.g. ^$) trivially
fail against any non-empty corpus — every real captured
scheduler-output corpus is non-empty, so the latter is
equally a no-op pin in practice. Both are useless;
is_match("") catches both with one check.
Bare \b (word boundary) slips this gate because the
empty string contains no word characters, so \b finds
no transition and is_match("") returns false; yet \b
matches the first word boundary in any real corpus,
turning a bare-\b pin into a vacuous “any non-empty
log passes” assertion. Use a substring of the expected
error text rather than a standalone boundary assertion.
All other documented assertions (\A, \z, ^, $,
\B) match the empty string at position 0 and ARE
caught by the gate.
Unlike the sibling Self::expect_scx_bpf_error_contains
(which is const fn), this builder is non-const because
the construction-time regex compilation requires heap
allocation. Callers needing a const builder for a regex
matcher must build the Assert via struct literal —
the evaluator’s defense-in-depth catches invalid syntax
reached via that bypass at first evaluation, but the
vacuous-pattern gate only fires on the builder path.
§Panics
When pattern is empty, is invalid regex syntax, or
matches the empty string.
Sourcepub fn evaluate_scx_bpf_error_match(
&self,
captured_text: &str,
expect_err: bool,
) -> Vec<AssertDetail>
pub fn evaluate_scx_bpf_error_match( &self, captured_text: &str, expect_err: bool, ) -> Vec<AssertDetail>
Evaluate the reproducer-mode scx_bpf_error matchers against
the captured text corpus. Returns an empty Vec when no matcher
is configured or when every configured matcher matches; returns
a non-empty Vec of AssertDetail entries on failure.
Each configured matcher contributes at most one detail. Both
fields can be set simultaneously (AND semantics — both must
match).
Preconditions enforced by the evaluator:
expect_err = truemust be set when either matcher is configured. Setting the matcher on a passing-test contract is a misuse — surfaced with anexpect_err = truereminder diagnostic.- The regex pattern in
Self::expect_scx_bpf_error_matchesmust compile via theregexcrate. Invalid syntax surfaces as a diagnostic naming the pattern and the compile error.
captured_text is the concatenation of the raw scheduler-log
stream (the bulk-port merged SchedLog frames, or the test
process output fallback when no frames arrived) and the
--- sched_ext dump --- extract — both surfaces where
scx_bpf_error printk lands. The matcher sees the WHOLE
stream, not the marker-extracted section; lines outside the
SCHED_OUTPUT_START..SCHED_OUTPUT_END markers are included.
Trait Implementations§
Source§impl<'de> Deserialize<'de> for Assert
impl<'de> Deserialize<'de> for Assert
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
impl Copy for Assert
Auto Trait Implementations§
impl Freeze for Assert
impl RefUnwindSafe for Assert
impl Send for Assert
impl Sync for Assert
impl Unpin for Assert
impl UnwindSafe for Assert
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more