1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
//! Test-only macros shared across the crate.
//!
//! Hoisted to crate root via `#[macro_use]` on the module declaration
//! in `lib.rs`, so `skip!` and `skip_on_contention!` are reachable from
//! any `#[cfg(test)]` code without an explicit `use`.
/// Emit a canonical `ktstr: SKIP: ...` message and return from the
/// caller. Routes through [`crate::report::test_skip`] so the
/// prefix lives in one place — the alternative (15+ open-coded
/// `eprintln!` sites) drifts into inconsistent casings that break
/// every grep-based test-summary tool.
///
/// Only callable from functions returning `()` — the macro expands to
/// an early `return;` with no value. Production code that returns a
/// non-unit type (dispatcher fns returning `i32`, helpers returning
/// `Option<T>`, loop bodies that `continue`) calls
/// [`crate::report::test_skip`] directly and drives its own control
/// flow.
macro_rules! skip {
// Zero-args arm: `skip!()` emits the banner with an empty
// reason. `format_args!()` itself requires at least a format
// string, so the variadic arm below cannot handle this case
// — a dedicated rule routes it to an empty literal.
() => {{
$crate::report::test_skip(format_args!(""));
return;
}};
($($arg:tt)*) => {{
$crate::report::test_skip(format_args!($($arg)*));
return;
}};
}
/// Evaluate a `Result`-returning builder (or any `anyhow::Result`
/// expression) and either unwrap the value or skip gracefully on
/// [`crate::vmm::host_topology::ResourceContention`]. Any other error
/// panics with `{e:#}`.
///
/// Replaces the recurring `match ... { Ok => v, Err(e) if
/// ResourceContention => skip!(...), Err(e) => panic!(...) }`
/// boilerplate. Inherits `skip!`'s early-return behavior, so callers
/// must return `()`.
macro_rules! skip_on_contention {
($expr:expr) => {
match $expr {
Ok(v) => v,
Err(e)
if e.chain().any(|cause| {
cause
.downcast_ref::<$crate::vmm::host_topology::ResourceContention>()
.is_some()
}) =>
{
skip!("resource contention: {e:#}");
}
Err(e) => panic!("{e:#}"),
}
};
}
/// Skip the calling test when the current process lacks a Linux
/// capability. Uses `prctl(PR_CAPBSET_READ, cap)` to probe the
/// capability bounding set — returns 1 when the cap is present, 0
/// when absent, -1 on EINVAL (unknown cap number).
///
/// Typical use: `require_capability!(libc::CAP_SYS_RESOURCE);` at the
/// top of a test that calls `setrlimit` to raise a hard limit.
#[allow(unused_macros)]
macro_rules! require_capability {
($cap:expr) => {{
let ret = unsafe { libc::prctl(libc::PR_CAPBSET_READ, $cap, 0, 0, 0) };
if ret != 1 {
skip!(
"missing capability {} (prctl PR_CAPBSET_READ returned {})",
stringify!($cap),
ret
);
}
}};
}
#[cfg(test)]
mod tests {
use crate::vmm::host_topology::ResourceContention;
/// Regression for the error-chain fix: a ResourceContention wrapped
/// in `.context(...)` must still be recognized by the macro and
/// trigger the `skip!` branch instead of the `panic!` branch.
///
/// `#[cfg(panic = "unwind")]`: this test uses `std::panic::catch_unwind`
/// to assert the macro does NOT panic. Under `panic = "abort"` (the
/// release profile's setting — see `Cargo.toml [profile.release]`)
/// panics cannot be caught; the panic aborts the whole test binary
/// instead of returning an `Err` from `catch_unwind`. Gating the
/// test on the panic strategy lets `cargo ktstr test --release`
/// skip it without false-failing the binary.
#[test]
#[cfg(panic = "unwind")]
fn skip_on_contention_walks_context_chain() {
let result = std::panic::catch_unwind(|| {
fn skip_fn() {
let err: anyhow::Error = anyhow::Error::new(ResourceContention {
reason: "simulated contention".into(),
})
.context("wrapping context layer 1")
.context("wrapping context layer 2");
let _: () = skip_on_contention!(Err::<(), _>(err));
unreachable!("skip_on_contention! should have early-returned");
}
skip_fn();
});
assert!(
result.is_ok(),
"context-wrapped ResourceContention must skip, not panic"
);
}
/// Unwrapped ResourceContention keeps working (no regression on the
/// simple path).
///
/// `#[cfg(panic = "unwind")]`: same rationale as the sibling
/// context-chain test — `catch_unwind` is unusable under
/// `panic = "abort"`.
#[test]
#[cfg(panic = "unwind")]
fn skip_on_contention_recognizes_direct_error() {
let result = std::panic::catch_unwind(|| {
fn skip_fn() {
let err: anyhow::Error = anyhow::Error::new(ResourceContention {
reason: "direct contention".into(),
});
let _: () = skip_on_contention!(Err::<(), _>(err));
unreachable!("skip_on_contention! should have early-returned");
}
skip_fn();
});
assert!(
result.is_ok(),
"direct ResourceContention must skip, not panic"
);
}
/// Non-contention errors still panic (negative case).
#[test]
#[should_panic(expected = "unrelated error")]
fn skip_on_contention_panics_on_non_contention_error() {
fn skip_fn() {
let err = anyhow::anyhow!("unrelated error");
let _: () = skip_on_contention!(Err::<(), _>(err));
}
skip_fn();
}
/// The `skip!` macro must emit the canonical `ktstr: SKIP:
/// <reason>` banner to stderr AND early-return from the calling
/// function. Prior tests exercise `test_skip` (the lower-level
/// emitter) and `skip_on_contention!` (the wrapper macro) but
/// the bare `skip!` macro was left uncovered — a regression that
/// silently broke the format_args expansion or the `return;`
/// tail would slip through until a downstream consumer
/// parsed the wrong line.
///
/// This test uses the crate-shared stderr-capture helper and
/// verifies BOTH invariants: the captured bytes carry the
/// canonical banner, and a post-`skip!` line in the helper fn
/// is never reached (pinned via a sentinel flag).
#[test]
fn skip_macro_emits_banner_and_early_returns() {
use crate::test_support::test_helpers::capture_stderr;
use std::sync::atomic::{AtomicBool, Ordering};
let reached_tail = AtomicBool::new(false);
let (_, bytes) = capture_stderr(|| {
// Helper fn returning `()` so `skip!` can emit its
// `return;` tail. The AtomicBool is set only if the
// line AFTER `skip!` executes — a regression that
// dropped the `return;` tail would trip it. The two
// `#[allow(...)]` attributes are load-bearing: when
// `skip!` correctly returns, `reached.store` is dead
// code AND `reached` falls out of the live set —
// which is exactly what this test is designed to
// pin. Without the allows, compilation warns about
// the very invariant the test verifies.
#[allow(unused_variables, unreachable_code)]
fn helper(reached: &AtomicBool) {
skip!("macro-level reason with {} substitution", "format-args");
reached.store(true, Ordering::SeqCst);
}
helper(&reached_tail);
});
let text = std::str::from_utf8(&bytes).expect("stderr is UTF-8");
assert_eq!(
text, "ktstr: SKIP: macro-level reason with format-args substitution\n",
"expected canonical banner with format-args substitution",
);
assert!(
!reached_tail.load(Ordering::SeqCst),
"skip! must early-return; lines after the macro must not execute",
);
}
/// `skip!` with a literal (no format args) still emits the
/// banner. Pairs with the substitution test above to cover the
/// no-args branch of the `format_args!($($arg)*)` expansion.
#[test]
fn skip_macro_literal_reason_emits_banner() {
use crate::test_support::test_helpers::capture_stderr;
let (_, bytes) = capture_stderr(|| {
fn helper() {
skip!("literal skip reason");
}
helper();
});
let text = std::str::from_utf8(&bytes).unwrap();
assert_eq!(text, "ktstr: SKIP: literal skip reason\n");
}
/// `skip!()` with ZERO arguments expands to
/// `format_args!()` — an empty reason. The banner still fires
/// with the canonical prefix + colon + empty tail + newline.
/// Pins the degenerate-input behavior so a regression that
/// rejected zero-argument expansion (e.g. a macro arm
/// requiring at least one token tree) fails here instead of at
/// some downstream call site that happens to call `skip!()`
/// for "I don't care why, just skip" semantics.
#[test]
fn skip_macro_zero_args_emits_banner_with_empty_reason() {
use crate::test_support::test_helpers::capture_stderr;
let (_, bytes) = capture_stderr(|| {
fn helper() {
skip!();
}
helper();
});
let text = std::str::from_utf8(&bytes).unwrap();
assert_eq!(text, "ktstr: SKIP: \n");
}
/// Pin the contract that the `#[ktstr_test]` macro's generated
/// body relies on: when `run_ktstr_test` returns
/// `Err(ResourceContention)` (possibly wrapped in
/// `.context(...)`), the macro must NOT panic — it must emit
/// the canonical `ktstr: SKIP: resource contention: ...`
/// banner and return. The macro lives in `ktstr-macros` and
/// expands to a literal `match` block that depends on
/// [`crate::test_support::is_resource_contention`] walking the
/// full error chain plus an `eprintln! + return` arm. We can't
/// invoke the proc-macro from a unit test, but we CAN simulate
/// its expansion shape and assert the same observable
/// behaviour: the banner is emitted, the post-arm sentinel
/// never executes, and the generated function never panics.
///
/// `#[cfg(panic = "unwind")]`: same rationale as the sibling
/// `skip_on_contention_walks_context_chain` test —
/// `catch_unwind` is unusable under `panic = "abort"`.
#[test]
#[cfg(panic = "unwind")]
fn ktstr_test_macro_body_skips_on_resource_contention() {
use crate::test_support::test_helpers::capture_stderr;
use crate::vmm::host_topology::ResourceContention;
use std::sync::atomic::{AtomicBool, Ordering};
let reached_tail = AtomicBool::new(false);
let result = std::panic::catch_unwind(|| {
let (_, bytes) = capture_stderr(|| {
// Simulates the body that
// `ktstr-macros::ktstr_test` expands into for a
// non-`expect_err` test. The contention arm must
// emit the SKIP banner and return; the trailing
// store must not execute.
#[allow(unused_variables, unreachable_code)]
fn helper(reached: &AtomicBool) {
let result: Result<(), anyhow::Error> =
Err(anyhow::Error::new(ResourceContention {
reason: "all 3 LLC slots busy".into(),
})
.context("build ktstr_test VM"));
match result {
Ok(_) => {}
Err(e) if crate::test_support::is_resource_contention(&e) => {
eprintln!("ktstr: SKIP: resource contention: {e:#}");
return;
}
Err(e) => panic!("{e:#}"),
}
reached.store(true, Ordering::SeqCst);
}
helper(&reached_tail);
});
let text = std::str::from_utf8(&bytes).expect("stderr is UTF-8");
assert!(
text.starts_with("ktstr: SKIP: resource contention: "),
"expected SKIP banner, got: {text:?}"
);
assert!(
text.contains("build ktstr_test VM"),
"banner must include the wrapping context layer; got: {text:?}"
);
assert!(
text.contains("all 3 LLC slots busy"),
"banner must include the inner ResourceContention reason; got: {text:?}"
);
});
assert!(
result.is_ok(),
"macro body must NOT panic on ResourceContention",
);
assert!(
!reached_tail.load(Ordering::SeqCst),
"macro body must early-return after emitting the SKIP banner",
);
}
}