#![cfg(test)]
use super::*;
static DEFERRED_PROBE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn extract_probe_output_valid_json() {
use crate::probe::process::ProbeEvent;
let payload = ProbeBytes {
events: vec![ProbeEvent {
func_idx: 0,
task_ptr: 1,
ts: 100,
args: [0; 6],
fields: vec![("p:task_struct.pid".to_string(), 42)],
kstack: vec![],
str_val: None,
..Default::default()
}],
func_names: vec![(0, "schedule".to_string())],
bpf_source_locs: Default::default(),
diagnostics: None,
nr_cpus: None,
param_names: Default::default(),
render_hints: Default::default(),
};
let json = serde_json::to_string(&payload).unwrap();
let output = format!("noise\n{PROBE_OUTPUT_START}\n{json}\n{PROBE_OUTPUT_END}\nmore");
let parsed = extract_probe_output(&output, None, None);
assert!(parsed.is_some());
let formatted = parsed.unwrap();
assert!(
formatted.contains("schedule"),
"should contain func name: {formatted}"
);
assert!(
formatted.contains("pid"),
"should contain field name: {formatted}"
);
}
#[test]
fn extract_probe_output_missing() {
assert!(extract_probe_output("no markers", None, None).is_none());
}
#[test]
fn extract_probe_output_empty() {
let output = format!("{PROBE_OUTPUT_START}\n\n{PROBE_OUTPUT_END}");
assert!(extract_probe_output(&output, None, None).is_none());
}
#[test]
fn extract_probe_output_invalid_json() {
let output = format!("{PROBE_OUTPUT_START}\nnot valid json\n{PROBE_OUTPUT_END}");
assert!(extract_probe_output(&output, None, None).is_none());
}
#[test]
fn exit_code_for_result_pass_inconc_fail_skip_lattice() {
use crate::assert::{AssertDetail, AssertResult, DetailKind};
assert_eq!(exit_code_for_result(&AssertResult::pass()), 0);
let inc =
AssertResult::inconclusive(AssertDetail::new(DetailKind::Benchmark, "zero-denominator"));
assert_eq!(exit_code_for_result(&inc), 2);
let fail = AssertResult::fail(AssertDetail::new(DetailKind::Other, "real failure"));
assert_eq!(exit_code_for_result(&fail), 1);
assert_eq!(exit_code_for_result(&AssertResult::skip("no signal")), 1);
let mut fail_plus_inc =
AssertResult::fail(AssertDetail::new(DetailKind::Other, "real failure first"));
fail_plus_inc.record_inconclusive(AssertDetail::new(
DetailKind::Benchmark,
"and a zero-denom gate",
));
assert_eq!(exit_code_for_result(&fail_plus_inc), 1);
let mut inc_plus_pass =
AssertResult::inconclusive(AssertDetail::new(DetailKind::Benchmark, "zero-denom"));
inc_plus_pass.record_pass();
assert_eq!(exit_code_for_result(&inc_plus_pass), 2);
}
#[test]
fn extract_probe_output_enriched_fields() {
use crate::probe::process::ProbeEvent;
let payload = ProbeBytes {
events: vec![
ProbeEvent {
func_idx: 0,
task_ptr: 1,
ts: 100,
args: [0xDEAD, 0, 0, 0, 0, 0],
fields: vec![
("prev:task_struct.pid".to_string(), 42),
("prev:task_struct.scx_flags".to_string(), 0x1c),
],
kstack: vec![],
str_val: None,
..Default::default()
},
ProbeEvent {
func_idx: 1,
task_ptr: 1,
ts: 200,
args: [0; 6],
fields: vec![("rq:rq.cpu".to_string(), 3)],
kstack: vec![],
str_val: None,
..Default::default()
},
],
func_names: vec![
(0, "schedule".to_string()),
(1, "pick_task_scx".to_string()),
],
bpf_source_locs: Default::default(),
diagnostics: None,
nr_cpus: None,
param_names: Default::default(),
render_hints: Default::default(),
};
let json = serde_json::to_string(&payload).unwrap();
let output = format!("{PROBE_OUTPUT_START}\n{json}\n{PROBE_OUTPUT_END}");
let formatted = extract_probe_output(&output, None, None).unwrap();
assert!(formatted.contains("pid"), "pid field: {formatted}");
assert!(formatted.contains("42"), "pid value: {formatted}");
assert!(
formatted.contains("scx_flags"),
"scx_flags field: {formatted}"
);
assert!(formatted.contains("cpu"), "cpu field: {formatted}");
assert!(formatted.contains("3"), "cpu value: {formatted}");
assert!(
formatted.contains("task_struct *prev"),
"type header for task_struct: {formatted}"
);
assert!(
formatted.contains("rq *rq"),
"type header for rq: {formatted}"
);
assert!(
!formatted.contains("arg0"),
"raw args should not appear when fields exist: {formatted}"
);
assert!(formatted.contains("schedule"), "func schedule: {formatted}");
assert!(
formatted.contains("pick_task_scx"),
"func pick_task_scx: {formatted}"
);
}
fn truncate_probe_json(payload: &ProbeBytes, cut_after: usize) -> String {
let json = serde_json::to_string(payload).expect("serialize ProbeBytes");
json[..cut_after.min(json.len())].to_string()
}
#[test]
fn extract_probe_output_truncated_recovers_complete_events() {
use crate::probe::process::ProbeEvent;
let payload = ProbeBytes {
events: vec![
ProbeEvent {
func_idx: 0,
task_ptr: 0xa,
ts: 100,
args: [0; 6],
fields: vec![("p:task_struct.pid".to_string(), 11)],
kstack: vec![],
str_val: None,
..Default::default()
},
ProbeEvent {
func_idx: 1,
task_ptr: 0xb,
ts: 200,
args: [0; 6],
fields: vec![("p:task_struct.pid".to_string(), 22)],
kstack: vec![],
str_val: None,
..Default::default()
},
ProbeEvent {
func_idx: 2,
task_ptr: 0xc,
ts: 300,
args: [0; 6],
fields: vec![("p:task_struct.pid".to_string(), 33)],
kstack: vec![],
str_val: None,
..Default::default()
},
],
func_names: vec![
(0, "first".to_string()),
(1, "second".to_string()),
(2, "third".to_string()),
],
bpf_source_locs: Default::default(),
diagnostics: None,
nr_cpus: None,
param_names: Default::default(),
render_hints: Default::default(),
};
let full = serde_json::to_string(&payload).unwrap();
let events_start = full.find("\"events\":[").unwrap() + "\"events\":[".len();
let mut depth: u32 = 0;
let mut in_string = false;
let mut escape = false;
let mut event_starts: Vec<usize> = Vec::new();
for (i, b) in full.bytes().enumerate().skip(events_start) {
if in_string {
if escape {
escape = false;
} else if b == b'\\' {
escape = true;
} else if b == b'"' {
in_string = false;
}
continue;
}
match b {
b'"' => in_string = true,
b'{' => {
if depth == 0 {
event_starts.push(i);
}
depth += 1;
}
b'}' => depth = depth.saturating_sub(1),
b']' if depth == 0 => break,
_ => {}
}
}
assert_eq!(
event_starts.len(),
3,
"test fixture should produce 3 events"
);
let cut = event_starts[2] + 5;
let truncated = &full[..cut];
let output = format!("{PROBE_OUTPUT_START}\n{truncated}\n");
let dir = tempfile::tempdir().expect("tempdir");
let partial = dir.path().join("payload.partial.json");
let formatted = extract_probe_output(&output, None, Some(&partial))
.expect("recovery must surface partial events");
assert!(
formatted.contains("unknown"),
"recovered events should print under `unknown` func header (no func_names): {formatted}",
);
assert!(
formatted.contains("11"),
"first event's pid value should appear: {formatted}",
);
assert!(
formatted.contains("22"),
"second event's pid value should appear: {formatted}",
);
assert!(
!formatted.contains("33"),
"third (truncated) event's pid value must NOT appear: {formatted}",
);
assert!(
partial.exists(),
"raw truncated payload must be written to partial dump path",
);
let dumped = std::fs::read_to_string(&partial).expect("read partial dump");
assert_eq!(
dumped.trim(),
truncated.trim(),
"partial dump must contain the raw extracted JSON verbatim",
);
}
#[test]
fn extract_probe_output_truncated_no_complete_events_returns_none() {
use crate::probe::process::ProbeEvent;
let payload = ProbeBytes {
events: vec![ProbeEvent {
func_idx: 0,
task_ptr: 0xa,
ts: 100,
args: [0; 6],
fields: vec![("p:task_struct.pid".to_string(), 11)],
kstack: vec![],
str_val: None,
..Default::default()
}],
func_names: vec![(0, "first".to_string())],
bpf_source_locs: Default::default(),
diagnostics: None,
nr_cpus: None,
param_names: Default::default(),
render_hints: Default::default(),
};
let full = serde_json::to_string(&payload).unwrap();
let events_open = full.find("\"events\":[").unwrap() + "\"events\":[".len();
let cut = events_open + 5;
let truncated = &full[..cut];
let output = format!("{PROBE_OUTPUT_START}\n{truncated}\n");
let dir = tempfile::tempdir().expect("tempdir");
let partial = dir.path().join("payload.partial.json");
let result = extract_probe_output(&output, None, Some(&partial));
assert!(
result.is_none(),
"no complete events recoverable should yield None: {result:?}",
);
assert!(
partial.exists(),
"partial dump must be written even when recovery yields zero events",
);
let dumped = std::fs::read_to_string(&partial).expect("read partial dump");
assert_eq!(dumped.trim(), truncated.trim());
}
#[test]
fn extract_probe_output_truncated_string_value_recovers_prior() {
use crate::probe::process::ProbeEvent;
let payload = ProbeBytes {
events: vec![
ProbeEvent {
func_idx: 0,
task_ptr: 0xa,
ts: 100,
args: [0; 6],
fields: vec![("p:task_struct.pid".to_string(), 11)],
kstack: vec![],
str_val: None,
..Default::default()
},
ProbeEvent {
func_idx: 1,
task_ptr: 0xb,
ts: 200,
args: [0; 6],
fields: vec![],
kstack: vec![],
str_val: Some("a".repeat(200)),
..Default::default()
},
],
func_names: vec![(0, "first".to_string()), (1, "second".to_string())],
bpf_source_locs: Default::default(),
diagnostics: None,
nr_cpus: None,
param_names: Default::default(),
render_hints: Default::default(),
};
let full = serde_json::to_string(&payload).unwrap();
let needle = "aaaaaaaaaa"; let idx = full
.find(needle)
.expect("fixture must contain the long string");
let cut = idx + needle.len() + 50; let truncated = &full[..cut];
let output = format!("{PROBE_OUTPUT_START}\n{truncated}\n");
let formatted = extract_probe_output(&output, None, None)
.expect("recovery must surface the first complete event");
assert!(
formatted.contains("11"),
"first event's pid must survive truncation in the second event: {formatted}",
);
}
#[test]
fn extract_probe_output_truncated_partial_dump_path_none_skips_write() {
use crate::probe::process::ProbeEvent;
let payload = ProbeBytes {
events: vec![
ProbeEvent {
func_idx: 0,
task_ptr: 0xa,
ts: 100,
args: [0; 6],
fields: vec![("p:task_struct.pid".to_string(), 11)],
kstack: vec![],
str_val: None,
..Default::default()
},
ProbeEvent {
func_idx: 1,
task_ptr: 0xb,
ts: 200,
args: [0; 6],
fields: vec![],
kstack: vec![],
str_val: None,
..Default::default()
},
],
func_names: vec![(0, "f0".to_string()), (1, "f1".to_string())],
bpf_source_locs: Default::default(),
diagnostics: None,
nr_cpus: None,
param_names: Default::default(),
render_hints: Default::default(),
};
let full = serde_json::to_string(&payload).unwrap();
let first_close = full.find("},{").expect("two events present");
let cut = first_close + 3; let truncated = &full[..cut];
let output = format!("{PROBE_OUTPUT_START}\n{truncated}\n");
let formatted = extract_probe_output(&output, None, None)
.expect("recovery must yield the first event without a dump path");
assert!(
formatted.contains("11"),
"first event recovered: {formatted}"
);
}
#[test]
fn parse_probe_payload_strict_success() {
use crate::probe::process::ProbeEvent;
let payload = ProbeBytes {
events: vec![ProbeEvent {
func_idx: 0,
task_ptr: 0xa,
ts: 100,
args: [0; 6],
fields: vec![],
kstack: vec![],
str_val: None,
..Default::default()
}],
func_names: vec![(0, "schedule".to_string())],
bpf_source_locs: Default::default(),
diagnostics: None,
nr_cpus: None,
param_names: Default::default(),
render_hints: Default::default(),
};
let json = serde_json::to_string(&payload).unwrap();
let parsed = parse_probe_payload(&json, None).expect("strict parse must succeed");
assert_eq!(parsed.events.len(), 1);
assert_eq!(parsed.func_names.len(), 1);
}
#[test]
fn parse_probe_payload_eof_with_dump_path_writes_file_returns_recovered() {
use crate::probe::process::ProbeEvent;
let payload = ProbeBytes {
events: vec![ProbeEvent {
func_idx: 0,
task_ptr: 0xa,
ts: 100,
args: [0; 6],
fields: vec![],
kstack: vec![],
str_val: None,
..Default::default()
}],
func_names: vec![],
bpf_source_locs: Default::default(),
diagnostics: None,
nr_cpus: None,
param_names: Default::default(),
render_hints: Default::default(),
};
let full = serde_json::to_string(&payload).unwrap();
let first_close = full.find("}]").expect("single-event array ends with `}]`");
let truncated = &full[..first_close + 1]; let dir = tempfile::tempdir().expect("tempdir");
let partial = dir.path().join("dump.json");
let parsed = parse_probe_payload(truncated, Some(&partial))
.expect("EOF with one complete event must recover");
assert_eq!(parsed.events.len(), 1);
assert!(parsed.func_names.is_empty());
assert!(parsed.diagnostics.is_none());
assert!(partial.exists());
assert_eq!(std::fs::read_to_string(&partial).unwrap(), truncated);
}
#[test]
fn parse_probe_payload_non_eof_error_returns_none_and_dumps() {
let dir = tempfile::tempdir().expect("tempdir");
let partial = dir.path().join("dump.json");
let result = parse_probe_payload("not json", Some(&partial));
assert!(result.is_none());
assert!(partial.exists());
assert_eq!(std::fs::read_to_string(&partial).unwrap(), "not json");
}
#[test]
fn parse_probe_payload_truncated_no_dump_path_still_recovers() {
use crate::probe::process::ProbeEvent;
let payload = ProbeBytes {
events: vec![ProbeEvent {
func_idx: 0,
task_ptr: 1,
ts: 100,
args: [0; 6],
fields: vec![],
kstack: vec![],
str_val: None,
..Default::default()
}],
func_names: vec![],
bpf_source_locs: Default::default(),
diagnostics: None,
nr_cpus: None,
param_names: Default::default(),
render_hints: Default::default(),
};
let full = serde_json::to_string(&payload).unwrap();
let first_close = full.find("}]").unwrap();
let truncated = &full[..first_close + 1];
let parsed = parse_probe_payload(truncated, None).expect("recovery without dump path");
assert_eq!(parsed.events.len(), 1);
}
#[test]
fn recover_partial_events_no_events_key_returns_empty() {
assert!(recover_partial_events(r#"{"foo":1}"#).is_empty());
assert!(recover_partial_events("").is_empty());
}
#[test]
fn recover_partial_events_empty_array() {
assert!(recover_partial_events(r#"{"events":[]"#).is_empty());
assert!(recover_partial_events(r#"{"events":["#).is_empty());
}
#[test]
fn recover_partial_events_handles_braces_in_strings() {
use crate::probe::process::ProbeEvent;
let event = ProbeEvent {
func_idx: 0,
task_ptr: 1,
ts: 100,
args: [0; 6],
fields: vec![],
kstack: vec![],
str_val: Some("contains {nested} and \\\"quoted\\\"".to_string()),
..Default::default()
};
let event_json = serde_json::to_string(&event).unwrap();
let payload = format!(r#"{{"events":[{event_json}]}}"#);
let cut = payload.rfind(']').unwrap();
let truncated = &payload[..cut];
let recovered = recover_partial_events(truncated);
assert_eq!(
recovered.len(),
1,
"one event should recover: {recovered:?}"
);
assert_eq!(
recovered[0].str_val.as_deref(),
Some("contains {nested} and \\\"quoted\\\""),
);
}
#[test]
fn find_balanced_object_end_simple_object() {
assert_eq!(find_balanced_object_end("{}"), Some(2));
assert_eq!(find_balanced_object_end("{}rest"), Some(2));
}
#[test]
fn find_balanced_object_end_nested_objects() {
assert_eq!(find_balanced_object_end(r#"{"a":{"b":{}}}"#), Some(14));
}
#[test]
fn find_balanced_object_end_braces_in_strings_ignored() {
assert_eq!(find_balanced_object_end(r#"{"x":"{{}}"}"#), Some(12));
}
#[test]
fn find_balanced_object_end_escaped_quote_does_not_close_string() {
let s = r#"{"x":"\"}"}"#;
assert_eq!(find_balanced_object_end(s), Some(s.len()));
}
#[test]
fn find_balanced_object_end_truncated_returns_none() {
assert_eq!(find_balanced_object_end(r#"{"a":1"#), None);
}
#[test]
fn find_balanced_object_end_truncated_in_string_returns_none() {
assert_eq!(find_balanced_object_end(r#"{"a":"hello"#), None);
}
#[test]
fn find_balanced_object_end_non_object_returns_none() {
assert_eq!(find_balanced_object_end("[1,2]"), None);
assert_eq!(find_balanced_object_end(""), None);
assert_eq!(find_balanced_object_end("null"), None);
}
#[test]
fn truncate_probe_json_clamps_to_full_length() {
let payload = ProbeBytes {
events: vec![],
func_names: vec![],
bpf_source_locs: Default::default(),
diagnostics: None,
nr_cpus: None,
param_names: Default::default(),
render_hints: Default::default(),
};
let full = serde_json::to_string(&payload).unwrap();
let s = truncate_probe_json(&payload, full.len() + 1024);
assert_eq!(s, full);
assert_eq!(truncate_probe_json(&payload, 0), "");
}
#[test]
fn format_tail_empty_text_returns_none() {
assert_eq!(format_tail("", 5, "scheduler"), None);
}
#[test]
fn format_tail_fewer_lines_than_n_returns_all() {
let out = format_tail("one\ntwo\nthree", 10, "scheduler").unwrap();
assert_eq!(out, "--- scheduler ---\none\ntwo\nthree");
}
#[test]
fn format_tail_trims_to_last_n_lines() {
let out = format_tail("1\n2\n3\n4\n5", 3, "log").unwrap();
assert_eq!(out, "--- log ---\n3\n4\n5");
}
#[test]
fn format_tail_zero_n_returns_empty_body_under_header() {
let out = format_tail("a\nb", 0, "hdr").unwrap();
assert_eq!(out, "--- hdr ---\n");
}
#[test]
fn format_tail_preserves_trailing_blank_lines() {
let out = format_tail("a\n\nb", 3, "hdr").unwrap();
assert_eq!(out, "--- hdr ---\na\n\nb");
}
#[test]
fn parse_rust_env_empty_cmdline_is_empty() {
assert!(parse_rust_env_from_cmdline("").is_empty());
}
#[test]
fn parse_rust_env_no_matches() {
assert!(parse_rust_env_from_cmdline("console=ttyS0 ro quiet").is_empty());
}
#[test]
fn parse_rust_env_backtrace_only() {
let parsed = parse_rust_env_from_cmdline("console=ttyS0 RUST_BACKTRACE=1 ro");
assert_eq!(parsed, vec![("RUST_BACKTRACE", "1")]);
}
#[test]
fn parse_rust_env_log_only() {
let parsed = parse_rust_env_from_cmdline("RUST_LOG=debug other=x");
assert_eq!(parsed, vec![("RUST_LOG", "debug")]);
}
#[test]
fn parse_rust_env_both() {
let parsed = parse_rust_env_from_cmdline("RUST_BACKTRACE=full RUST_LOG=trace other=y");
assert_eq!(
parsed,
vec![("RUST_BACKTRACE", "full"), ("RUST_LOG", "trace")]
);
}
#[test]
fn parse_rust_env_preserves_token_order() {
let parsed = parse_rust_env_from_cmdline("RUST_LOG=info RUST_BACKTRACE=1");
assert_eq!(parsed, vec![("RUST_LOG", "info"), ("RUST_BACKTRACE", "1")]);
}
#[test]
fn parse_rust_env_empty_value() {
let parsed = parse_rust_env_from_cmdline("RUST_LOG=");
assert_eq!(parsed, vec![("RUST_LOG", "")]);
}
#[test]
fn parse_rust_env_ignores_prefix_mismatch() {
assert!(parse_rust_env_from_cmdline("xRUST_LOG=x").is_empty());
}
#[test]
fn parse_rust_env_sidecar_dir() {
let parsed = parse_rust_env_from_cmdline(
"console=ttyS0 KTSTR_SIDECAR_DIR=/host/target/ktstr/run-key ro",
);
assert_eq!(
parsed,
vec![(crate::KTSTR_SIDECAR_DIR_ENV, "/host/target/ktstr/run-key")]
);
}
#[test]
fn parse_rust_env_all_three_keys() {
let parsed =
parse_rust_env_from_cmdline("RUST_LOG=info KTSTR_SIDECAR_DIR=/dir RUST_BACKTRACE=1");
assert_eq!(
parsed,
vec![
("RUST_LOG", "info"),
(crate::KTSTR_SIDECAR_DIR_ENV, "/dir"),
("RUST_BACKTRACE", "1")
]
);
}
fn lifecycle_drain(
phase: crate::vmm::wire::LifecyclePhase,
reason: &str,
) -> crate::vmm::host_comms::BulkDrainResult {
let mut payload = vec![phase.wire_value()];
payload.extend_from_slice(reason.as_bytes());
crate::vmm::host_comms::BulkDrainResult {
entries: vec![crate::vmm::wire::ShmEntry {
msg_type: crate::vmm::wire::MSG_TYPE_LIFECYCLE,
payload,
crc_ok: true,
}],
}
}
#[test]
fn extract_not_attached_reason_timeout() {
let drain = lifecycle_drain(
crate::vmm::wire::LifecyclePhase::SchedulerNotAttached,
"timeout",
);
assert_eq!(
extract_not_attached_reason(Some(&drain)).as_deref(),
Some("timeout"),
);
}
#[test]
fn extract_not_attached_reason_sysfs_absent() {
let drain = lifecycle_drain(
crate::vmm::wire::LifecyclePhase::SchedulerNotAttached,
"sched_ext sysfs absent",
);
assert_eq!(
extract_not_attached_reason(Some(&drain)).as_deref(),
Some("sched_ext sysfs absent"),
);
}
#[test]
fn extract_not_attached_reason_trims_surrounding_whitespace() {
let drain = lifecycle_drain(
crate::vmm::wire::LifecyclePhase::SchedulerNotAttached,
" timeout ",
);
assert_eq!(
extract_not_attached_reason(Some(&drain)).as_deref(),
Some("timeout"),
);
}
#[test]
fn extract_not_attached_reason_absent_returns_none() {
assert_eq!(extract_not_attached_reason(None), None);
let died = lifecycle_drain(crate::vmm::wire::LifecyclePhase::SchedulerDied, "");
assert_eq!(extract_not_attached_reason(Some(&died)), None);
}
#[test]
fn extract_not_attached_reason_empty_suffix_returns_none() {
let drain = lifecycle_drain(crate::vmm::wire::LifecyclePhase::SchedulerNotAttached, "");
assert_eq!(extract_not_attached_reason(Some(&drain)), None);
let drain_ws = lifecycle_drain(
crate::vmm::wire::LifecyclePhase::SchedulerNotAttached,
" ",
);
assert_eq!(extract_not_attached_reason(Some(&drain_ws)), None);
}
#[test]
fn extract_not_attached_reason_first_match_wins() {
let drain = crate::vmm::host_comms::BulkDrainResult {
entries: vec![
lifecycle_drain(
crate::vmm::wire::LifecyclePhase::SchedulerNotAttached,
"timeout",
)
.entries
.pop()
.unwrap(),
lifecycle_drain(
crate::vmm::wire::LifecyclePhase::SchedulerNotAttached,
"sched_ext sysfs absent",
)
.entries
.pop()
.unwrap(),
],
};
assert_eq!(
extract_not_attached_reason(Some(&drain)).as_deref(),
Some("timeout"),
);
}
#[test]
fn extract_not_attached_reason_skips_crc_bad() {
let mut bad = lifecycle_drain(
crate::vmm::wire::LifecyclePhase::SchedulerNotAttached,
"timeout",
);
bad.entries[0].crc_ok = false;
assert_eq!(extract_not_attached_reason(Some(&bad)), None);
}
fn died_drain() -> crate::vmm::host_comms::BulkDrainResult {
lifecycle_drain(crate::vmm::wire::LifecyclePhase::SchedulerDied, "")
}
fn not_attached_drain(reason: &str) -> crate::vmm::host_comms::BulkDrainResult {
lifecycle_drain(
crate::vmm::wire::LifecyclePhase::SchedulerNotAttached,
reason,
)
}
#[test]
fn classify_repro_vm_status_timeout_wins_over_other_signals() {
let drain = not_attached_drain("timeout");
let status = classify_repro_vm_status(
true,
true,
137,
Some(&drain),
);
assert_eq!(status, "repro VM: timed out");
}
#[test]
fn classify_repro_vm_status_not_attached_with_reason() {
let drain = not_attached_drain("sched_ext sysfs absent");
let status = classify_repro_vm_status(false, false, 1, Some(&drain));
assert_eq!(
status,
"repro VM: scheduler did not attach (sched_ext sysfs absent) (exit code 1)",
);
}
#[test]
fn classify_repro_vm_status_not_attached_takes_precedence_over_crashed() {
let mut drain = died_drain();
drain
.entries
.push(not_attached_drain("timeout").entries.pop().unwrap());
let status = classify_repro_vm_status(false, true, 1, Some(&drain));
assert_eq!(
status,
"repro VM: scheduler did not attach (timeout) (exit code 1)",
);
}
#[test]
fn classify_repro_vm_status_crashed_from_sentinel() {
let drain = died_drain();
let status = classify_repro_vm_status(false, false, 139, Some(&drain));
assert_eq!(
status,
"repro VM: scheduler crashed — exited with non-zero status (139)",
);
}
#[test]
fn classify_repro_vm_status_crashed_from_crash_message() {
let status = classify_repro_vm_status(false, true, 134, None);
assert_eq!(
status,
"repro VM: scheduler crashed — exited with non-zero status (134)",
);
}
#[test]
fn classify_repro_vm_status_crashed_from_sentinel_qemu_clean_exit() {
let drain = died_drain();
let status = classify_repro_vm_status(false, false, 0, Some(&drain));
assert_eq!(status, "repro VM: scheduler crashed — exited cleanly");
}
#[test]
fn classify_repro_vm_status_crashed_from_sentinel_killed_by_signal() {
let drain = died_drain();
let status = classify_repro_vm_status(false, false, -9, Some(&drain));
assert_eq!(
status,
"repro VM: scheduler crashed — killed by signal (-9)",
);
}
#[test]
fn classify_repro_vm_status_crashed_from_sentinel_vmm_exit_code_unset() {
let drain = died_drain();
let status = classify_repro_vm_status(false, false, -1, Some(&drain));
assert_eq!(
status,
"repro VM: scheduler crashed — VM host reported no final exit \
status (the scheduler did not deliver an exit signal before \
the VM ended)",
);
assert!(
!status.contains("BSP"),
"user-facing status leaks BSP: {status}"
);
assert!(
!status.contains("VmResult::exit_code"),
"user-facing status leaks VmResult::exit_code: {status}",
);
assert!(
!status.contains("MSG_TYPE_EXIT"),
"user-facing status leaks MSG_TYPE_EXIT: {status}",
);
}
#[test]
fn classify_repro_vm_status_abnormal_exit() {
let status = classify_repro_vm_status(false, false, 2, None);
assert_eq!(status, "repro VM: exited abnormally (exit code 2)");
}
#[test]
fn classify_repro_vm_status_clean_run() {
let status = classify_repro_vm_status(false, false, 0, None);
assert_eq!(
status,
"repro VM: scheduler ran normally (crash did not reproduce)",
);
}
#[test]
fn classify_repro_vm_status_malformed_not_attached_falls_through() {
let drain = not_attached_drain("");
let status = classify_repro_vm_status(false, false, 1, Some(&drain));
assert_eq!(status, "repro VM: exited abnormally (exit code 1)");
}
#[test]
fn render_failure_dump_file_missing_returns_none() {
let nonexistent = std::env::temp_dir().join("ktstr-render-failure-dump-missing");
let _ = std::fs::remove_file(&nonexistent);
assert!(render_failure_dump_file(&nonexistent).is_none());
}
#[test]
fn render_failure_dump_file_single_schema() {
use crate::monitor::dump::{FailureDumpReport, SCHEMA_SINGLE};
let report = FailureDumpReport {
schema: SCHEMA_SINGLE.to_string(),
..Default::default()
};
let json = serde_json::to_string(&report).expect("serialize single");
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
std::fs::write(tmp.path(), json).expect("write tempfile");
let rendered = render_failure_dump_file(tmp.path()).expect("single-schema must render Some");
assert!(
rendered.starts_with("--- repro VM failure dump ---"),
"header missing: {rendered}"
);
assert!(
rendered.contains("(empty failure dump)"),
"single-schema body must come from FailureDumpReport Display: {rendered}"
);
}
#[test]
fn render_failure_dump_file_dual_schema() {
use crate::monitor::dump::{DualFailureDumpReport, FailureDumpReport, SCHEMA_DUAL};
let dual = DualFailureDumpReport {
schema: SCHEMA_DUAL.to_string(),
early: None,
late: FailureDumpReport::default(),
early_max_age_jiffies: 0,
early_threshold_jiffies: 0,
early_skipped_reason: None,
};
let json = serde_json::to_string(&dual).expect("serialize dual");
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
std::fs::write(tmp.path(), json).expect("write tempfile");
let rendered = render_failure_dump_file(tmp.path()).expect("dual-schema must render Some");
assert!(
rendered.starts_with("--- repro VM failure dump ---"),
"header missing: {rendered}"
);
assert!(
rendered.contains("DualFailureDumpReport:"),
"dual-schema body must come from DualFailureDumpReport Display: {rendered}"
);
}
#[test]
fn render_failure_dump_file_absent_schema_returns_none() {
let json = r#"{"maps":[],"vcpu_regs":[],"sdt_allocations":[]}"#;
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
std::fs::write(tmp.path(), json).expect("write tempfile");
assert!(
render_failure_dump_file(tmp.path()).is_none(),
"absent-schema JSON must return None to avoid silent mis-routing"
);
}
#[test]
fn render_failure_dump_file_unknown_schema_returns_none() {
let json = r#"{"schema":"triple","maps":[],"vcpu_regs":[],"sdt_allocations":[]}"#;
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
std::fs::write(tmp.path(), json).expect("write tempfile");
assert!(render_failure_dump_file(tmp.path()).is_none());
}
#[test]
fn render_failure_dump_file_invalid_json_returns_none() {
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
std::fs::write(tmp.path(), "not json").expect("write tempfile");
assert!(render_failure_dump_file(tmp.path()).is_none());
}
fn diag_with_events(
before: u32,
after: u32,
trigger_fires: u64,
exit_kind: u32,
) -> crate::probe::process::ProbeDiagnostics {
crate::probe::process::ProbeDiagnostics {
events_before_stitch: before,
events_after_stitch: after,
bpf_trigger_fires: trigger_fires,
bpf_exit_kind_snap: exit_kind,
..Default::default()
}
}
#[test]
fn stitch_drop_cause_trigger_never_fired() {
let diag = diag_with_events(146, 0, 0, 0);
let cause = stitch_drop_cause(&diag);
assert!(
cause.contains("trigger never fired"),
"expected 'trigger never fired' branch, got: {cause}"
);
}
#[test]
fn stitch_drop_cause_kind_stall() {
use crate::probe::scx_defs::EXIT_ERROR_STALL;
let diag = diag_with_events(146, 0, 1, EXIT_ERROR_STALL as u32);
let cause = stitch_drop_cause(&diag);
assert!(
cause.contains("kind=STALL"),
"expected 'kind=STALL' branch, got: {cause}"
);
}
#[test]
fn stitch_drop_cause_kind_error_generic() {
use crate::probe::scx_defs::EXIT_ERROR;
let diag = diag_with_events(146, 0, 1, EXIT_ERROR as u32);
let cause = stitch_drop_cause(&diag);
assert!(
cause.contains("kind=ERROR"),
"expected 'kind=ERROR' branch, got: {cause}"
);
assert!(
!cause.contains("kind=STALL"),
"kind=ERROR branch must not say STALL: {cause}"
);
assert!(
!cause.contains("kind=BPF_ERROR"),
"kind=ERROR branch must not say BPF_ERROR: {cause}"
);
}
#[test]
fn stitch_drop_cause_kind_bpf_error() {
use crate::probe::scx_defs::EXIT_ERROR_BPF;
let diag = diag_with_events(146, 0, 1, EXIT_ERROR_BPF as u32);
let cause = stitch_drop_cause(&diag);
assert!(
cause.contains("kind=BPF_ERROR"),
"expected 'kind=BPF_ERROR' branch, got: {cause}"
);
assert!(
cause.contains("file a ticket") || cause.contains("ID mismatch"),
"kind=BPF_ERROR branch must signal a suspected bug: {cause}"
);
}
#[test]
fn stitch_drop_cause_unrecognized_kind() {
let diag = diag_with_events(146, 0, 1, 9999);
let cause = stitch_drop_cause(&diag);
assert!(
cause.contains("unrecognized") || cause.contains("no causal"),
"unrecognized-kind branch must surface a diagnostic, got: {cause}"
);
}
#[test]
fn format_probe_diagnostics_appends_cause_when_zero_after_stitch() {
let pipeline = PipelineDiagnostics::default();
let skeleton = diag_with_events(146, 0, 0, 0);
let rendered = format_probe_diagnostics(&pipeline, &skeleton);
assert!(
rendered.contains("146 captured, 0 after stitch"),
"rendered output missing counter pair: {rendered}"
);
assert!(
rendered.contains("trigger never fired"),
"rendered output missing cause explanation: {rendered}"
);
}
#[test]
fn format_probe_diagnostics_appends_kind_stall_diagnostic() {
use crate::probe::scx_defs::EXIT_ERROR_STALL;
let pipeline = PipelineDiagnostics::default();
let skeleton = diag_with_events(146, 0, 1, EXIT_ERROR_STALL as u32);
let rendered = format_probe_diagnostics(&pipeline, &skeleton);
assert!(
rendered.contains("kind=STALL"),
"rendered output missing kind=STALL diagnostic: {rendered}"
);
}
#[test]
fn format_probe_diagnostics_no_cause_when_clean_run() {
let pipeline = PipelineDiagnostics::default();
let skeleton = diag_with_events(0, 0, 0, 0);
let rendered = format_probe_diagnostics(&pipeline, &skeleton);
assert!(
rendered.contains("0 captured, 0 after stitch"),
"missing zero-zero counter line: {rendered}"
);
assert!(
!rendered.contains("trigger never fired"),
"must not append cause for clean run: {rendered}"
);
assert!(
!rendered.contains("kind=STALL"),
"must not append cause for clean run: {rendered}"
);
}
#[test]
fn format_probe_diagnostics_no_cause_when_stitch_succeeded() {
let pipeline = PipelineDiagnostics::default();
let skeleton = diag_with_events(146, 100, 1, 1025);
let rendered = format_probe_diagnostics(&pipeline, &skeleton);
assert!(
rendered.contains("146 captured, 100 after stitch"),
"missing counter line: {rendered}"
);
assert!(
!rendered.contains("trigger never fired"),
"must not append cause when stitch succeeded: {rendered}"
);
assert!(
!rendered.contains("kind=STALL"),
"must not append cause when stitch succeeded: {rendered}"
);
}
#[test]
fn format_probe_diagnostics_appends_fallback_marker() {
let pipeline = PipelineDiagnostics::default();
let skeleton = crate::probe::process::ProbeDiagnostics {
events_before_stitch: 146,
events_after_stitch: 80,
stitch_fallback_used: true,
bpf_trigger_fires: 0,
bpf_exit_kind_snap: 0,
..Default::default()
};
let rendered = format_probe_diagnostics(&pipeline, &skeleton);
assert!(
rendered.contains("trigger absent") && rendered.contains("frequency"),
"fallback marker missing from rendered output: {rendered}"
);
}
#[test]
fn classify_dmesg_corruption_empty_text() {
let diag = classify_dmesg_corruption("");
assert!(diag.is_some());
assert!(diag.unwrap().contains("empty"));
}
#[test]
fn classify_dmesg_corruption_only_whitespace() {
let diag = classify_dmesg_corruption(" \n\n\t \n");
assert!(diag.is_some());
assert!(diag.unwrap().contains("empty"));
}
#[test]
fn classify_dmesg_corruption_only_replacement_chars() {
let diag = classify_dmesg_corruption("\u{fffd}\u{fffd}\u{fffd}");
assert!(diag.is_some());
assert!(diag.unwrap().contains("corrupt"));
}
#[test]
fn classify_dmesg_corruption_only_control_chars() {
let diag = classify_dmesg_corruption("\0\0\0");
assert!(diag.is_some());
assert!(diag.unwrap().contains("corrupt"));
let diag = classify_dmesg_corruption("\0\x01\x02\x07\x08");
assert!(diag.is_some());
assert!(diag.unwrap().contains("corrupt"));
let diag = classify_dmesg_corruption("\x7f\x7f");
assert!(diag.is_some());
assert!(diag.unwrap().contains("corrupt"));
}
#[test]
fn classify_dmesg_corruption_only_replacement_and_control_mix() {
let diag = classify_dmesg_corruption("\u{fffd}\0\u{fffd}");
assert!(diag.is_some());
assert!(diag.unwrap().contains("corrupt"));
}
#[test]
fn classify_dmesg_corruption_latin1_text_passes_through() {
for ch in ['\u{c0}', '\u{e9}', '\u{f1}', '\u{ff}'] {
let s: String = std::iter::repeat_n(ch, 5).collect();
let diag = classify_dmesg_corruption(&s);
assert!(
diag.is_none(),
"Latin-1 char U+{:04X} must NOT be classified as corrupt: {diag:?}",
ch as u32,
);
}
}
#[test]
fn classify_dmesg_corruption_real_kernel_text_passes_through() {
let diag = classify_dmesg_corruption("[ 0.000000] Linux version 6.16.0\n");
assert!(
diag.is_none(),
"real kernel text must not be classified as corrupt: {diag:?}"
);
}
#[test]
fn classify_dmesg_corruption_one_control_amid_text_passes_through() {
let diag = classify_dmesg_corruption("[0.1] Linux\u{1}version");
assert!(
diag.is_none(),
"one control char amid real text is not corruption: {diag:?}"
);
}
#[test]
fn classify_dmesg_corruption_mixed_garbage_and_text_passes_through() {
let diag = classify_dmesg_corruption("\u{fffd}A\u{fffd}");
assert!(diag.is_none());
}
#[test]
fn render_dmesg_tail_filter_empties_non_empty_stderr_emits_pointer_diag() {
let stderr = "[ 0.5] sched_ext_dump: header\n\
[ 0.6] sched_ext_dump: body line A\n\
[ 0.7] sched_ext_dump: body line B\n";
let tail = render_dmesg_tail(stderr, 40);
assert!(
tail.contains("--- repro VM dmesg ---"),
"tail must carry the section header: {tail}",
);
assert!(
tail.contains("no kernel printk other than sched_ext_dump"),
"tail must point operators at the sched_ext_dump section, \
not falsely report a crash: {tail}",
);
assert!(
!tail.contains("scheduler crashed"),
"filter-emptied real output must NOT surface the crash \
classifier's diagnostic: {tail}",
);
}
#[test]
fn render_dmesg_tail_truly_empty_stderr_emits_crash_diagnostic() {
let tail = render_dmesg_tail("", 40);
assert!(
tail.contains("scheduler crashed before kernel printk"),
"genuinely-empty stderr must surface the crash diagnostic: {tail}",
);
}
#[test]
fn render_dmesg_tail_real_kernel_text_passes_through_to_format_tail() {
let stderr = "[ 0.1] Linux version 6.16.0\n\
[ 0.5] sched_ext_dump: header\n\
[ 0.6] sched_ext_dump: body\n\
[ 0.9] systemd: starting\n";
let tail = render_dmesg_tail(stderr, 40);
assert!(
tail.contains("Linux version 6.16.0"),
"real kernel text must survive the filter: {tail}",
);
assert!(
tail.contains("systemd: starting"),
"non-dump lines must survive the filter: {tail}",
);
assert!(
!tail.contains("sched_ext_dump"),
"dump lines must be stripped (rendered separately): {tail}",
);
assert!(
!tail.contains("scheduler crashed"),
"real kernel text must NOT surface the crash diagnostic: {tail}",
);
}
#[test]
fn render_dmesg_tail_only_whitespace_emits_crash_diagnostic() {
let tail = render_dmesg_tail(" \n\n\t \n", 40);
assert!(
tail.contains("scheduler crashed before kernel printk"),
"whitespace-only stderr must surface the crash diagnostic: {tail}",
);
}
#[test]
fn render_dmesg_tail_dump_plus_replacement_noise_emits_pointer_diag() {
let stderr = "[1] sched_ext_dump: header\n\u{fffd}\u{fffd}\u{fffd}\n";
let tail = render_dmesg_tail(stderr, 40);
assert!(
tail.contains("no kernel printk other than sched_ext_dump"),
"filter-dropped-real-lines + U+FFFD residue must point at \
the dump section, not surface the crash diagnostic: {tail}",
);
assert!(
!tail.contains("scheduler crashed"),
"must NOT misclassify residue as a crash when real dump lines \
were filtered: {tail}",
);
}
#[test]
fn render_dmesg_tail_dump_plus_control_noise_emits_pointer_diag() {
let stderr = "[ 0.5] sched_ext_dump: header\n\0\0\0\n";
let tail = render_dmesg_tail(stderr, 40);
assert!(
tail.contains("no kernel printk other than sched_ext_dump"),
"control-char residue with dump lines must point at the dump \
section: {tail}",
);
}
#[test]
fn render_dmesg_tail_dump_plus_whitespace_emits_pointer_diag() {
let stderr = "[1.0] sched_ext_dump: dump\n \n\t\n";
let tail = render_dmesg_tail(stderr, 40);
assert!(
tail.contains("no kernel printk other than sched_ext_dump"),
"whitespace residue with dump lines must point at the dump \
section, not surface the crash diagnostic: {tail}",
);
assert!(
!tail.contains("scheduler crashed"),
"must NOT report a crash when real dump lines were filtered: {tail}",
);
}
#[test]
fn render_dmesg_tail_pure_corruption_no_dump_emits_corrupt_diag() {
let tail = render_dmesg_tail("\u{fffd}\u{fffd}\u{fffd}", 40);
assert!(
tail.contains("scheduler crashed") || tail.contains("UART buffer"),
"pure-corruption stderr must surface the corruption diagnostic: {tail}",
);
}
#[test]
fn render_dmesg_tail_pure_whitespace_no_dump_emits_empty_diag() {
let tail = render_dmesg_tail(" \n\t\n", 40);
assert!(
tail.contains("scheduler crashed before kernel printk"),
"whitespace-only stderr (no dump line) must surface the \
crash diagnostic: {tail}",
);
}
#[test]
fn render_dmesg_tail_latin1_residue_no_dump_passes_through() {
let stderr = "[0.1] hw vendor: foo\u{ff}bar\n";
let tail = render_dmesg_tail(stderr, 40);
assert!(
tail.contains("foo\u{ff}bar"),
"Latin-1 residue must format-tail-render unchanged: {tail}"
);
assert!(
!tail.contains("scheduler crashed"),
"Latin-1 residue must NOT trigger the corruption diag: {tail}"
);
}
#[test]
fn render_dmesg_tail_uses_tightened_marker() {
let stderr = "[ 0.1] BUG in sched_ext_dump_disable callback\n";
let tail = render_dmesg_tail(stderr, 40);
assert!(
tail.contains("BUG in sched_ext_dump_disable callback"),
"non-marker line (no colon after) must survive the filter: {tail}",
);
}
#[test]
fn render_dmesg_tail_bug_line_and_real_dump_split_correctly() {
let stderr = "[ 0.1] BUG in sched_ext_dump_disable callback\n\
ktstr-0 [001] 0.5: sched_ext_dump: scheduler state\n";
let tail = render_dmesg_tail(stderr, 40);
assert!(
tail.contains("BUG in sched_ext_dump_disable callback"),
"BUG line must survive the filter and land in dmesg: {tail}",
);
assert!(
!tail.contains("scheduler state"),
"real dump line must be stripped from dmesg (it goes to \
the dump section rendered separately): {tail}",
);
}
#[test]
fn extract_probe_output_emits_kind_stall_diagnostic_end_to_end() {
use crate::probe::process::ProbeDiagnostics;
use crate::probe::scx_defs::EXIT_ERROR_STALL;
let skeleton = ProbeDiagnostics {
events_before_stitch: 146,
events_after_stitch: 0,
bpf_trigger_fires: 1,
bpf_exit_kind_snap: EXIT_ERROR_STALL as u32,
..Default::default()
};
let payload = ProbeBytes {
events: Vec::new(),
func_names: Vec::new(),
bpf_source_locs: Default::default(),
diagnostics: Some(ProbeBytesDiagnostics {
pipeline: PipelineDiagnostics::default(),
skeleton,
}),
nr_cpus: None,
param_names: Default::default(),
render_hints: Default::default(),
};
let json = serde_json::to_string(&payload).unwrap();
let output = format!("noise\n{PROBE_OUTPUT_START}\n{json}\n{PROBE_OUTPUT_END}\n");
let formatted = extract_probe_output(&output, None, None)
.expect("ProbeBytes with diagnostics must produce some output");
assert!(
formatted.contains("--- probe pipeline ---"),
"missing pipeline header: {formatted}"
);
assert!(
formatted.contains("146 captured, 0 after stitch"),
"missing events counter line: {formatted}"
);
assert!(
formatted.contains("kind=STALL"),
"missing kind=STALL diagnostic in end-to-end output: {formatted}"
);
}
#[test]
fn extract_probe_output_emits_trigger_never_fired_end_to_end() {
use crate::probe::process::ProbeDiagnostics;
let skeleton = ProbeDiagnostics {
events_before_stitch: 146,
events_after_stitch: 0,
bpf_trigger_fires: 0,
bpf_exit_kind_snap: 0,
bpf_kprobe_fires: 16567,
bpf_meta_misses: 0,
..Default::default()
};
let payload = ProbeBytes {
events: Vec::new(),
func_names: Vec::new(),
bpf_source_locs: Default::default(),
diagnostics: Some(ProbeBytesDiagnostics {
pipeline: PipelineDiagnostics::default(),
skeleton,
}),
nr_cpus: None,
param_names: Default::default(),
render_hints: Default::default(),
};
let json = serde_json::to_string(&payload).unwrap();
let output = format!("{PROBE_OUTPUT_START}\n{json}\n{PROBE_OUTPUT_END}");
let formatted = extract_probe_output(&output, None, None)
.expect("ProbeBytes with diagnostics must produce some output");
assert!(
formatted.contains("trigger never fired"),
"missing 'trigger never fired' diagnostic: {formatted}"
);
assert!(
formatted.contains("16567 kprobe fires"),
"missing bpf_counts line: {formatted}"
);
}
#[test]
fn extract_probe_output_emits_fallback_marker_end_to_end() {
use crate::probe::process::{ProbeDiagnostics, ProbeEvent};
let skeleton = ProbeDiagnostics {
events_before_stitch: 146,
events_after_stitch: 80,
stitch_fallback_used: true,
bpf_trigger_fires: 0,
..Default::default()
};
let payload = ProbeBytes {
events: vec![ProbeEvent {
func_idx: 0,
task_ptr: 0xa,
ts: 100,
args: [0; 6],
fields: Vec::new(),
kstack: Vec::new(),
str_val: None,
..Default::default()
}],
func_names: vec![(0, "schedule".to_string())],
bpf_source_locs: Default::default(),
diagnostics: Some(ProbeBytesDiagnostics {
pipeline: PipelineDiagnostics::default(),
skeleton,
}),
nr_cpus: None,
param_names: Default::default(),
render_hints: Default::default(),
};
let json = serde_json::to_string(&payload).unwrap();
let output = format!("{PROBE_OUTPUT_START}\n{json}\n{PROBE_OUTPUT_END}");
let formatted = extract_probe_output(&output, None, None)
.expect("ProbeBytes with events must produce output");
assert!(
formatted.contains("trigger absent") && formatted.contains("frequency"),
"fallback marker missing from end-to-end output: {formatted}"
);
}
#[test]
fn deferred_probe_stash_take_invariants() {
let _guard = DEFERRED_PROBE_TEST_LOCK.lock().unwrap();
let _ = take_deferred_probe();
assert!(
take_deferred_probe().is_none(),
"empty stash must yield None"
);
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
stash_deferred_probe(stop.clone(), None);
let taken = take_deferred_probe().expect("stash must round-trip");
assert!(
!taken.stop.load(std::sync::atomic::Ordering::Relaxed),
"stop flag must round-trip with original value"
);
assert!(taken.handle.is_none(), "handle round-trip preserved None");
assert!(
take_deferred_probe().is_none(),
"drained: subsequent take must return None"
);
let stop_a = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let stop_b = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
stash_deferred_probe(stop_a, None);
stash_deferred_probe(stop_b, None);
let taken = take_deferred_probe().expect("stash present");
assert!(
taken.stop.load(std::sync::atomic::Ordering::Relaxed),
"second stash must win — got stop_a value instead of stop_b"
);
let _ = take_deferred_probe();
}
#[test]
fn label_repro_verdict_wraps_when_primary_did_not_reach_workload() {
use super::label_repro_verdict_when_workload_not_reached;
let verdict = "repro VM: scheduler ran normally (crash did not reproduce)";
let wrapped = label_repro_verdict_when_workload_not_reached(false, verdict);
let lines: Vec<&str> = wrapped.lines().collect();
assert!(
lines.len() >= 2,
"wrap must put the original verdict on a new line: {wrapped}",
);
assert!(
lines[0].starts_with("PRIMARY DID NOT REACH WORKLOAD"),
"first line must lead with the cautionary label: {wrapped}",
);
assert!(
wrapped.contains("not load-bearing"),
"wrap must call out that auto-repro is not load-bearing: {wrapped}",
);
assert_eq!(
*lines.last().unwrap(),
verdict,
"last line must be the unmodified original verdict: {wrapped}",
);
}
#[test]
fn label_repro_verdict_passthrough_when_primary_reached_workload() {
use super::label_repro_verdict_when_workload_not_reached;
let verdict = "repro VM: scheduler ran normally (crash did not reproduce)";
let wrapped = label_repro_verdict_when_workload_not_reached(true, verdict);
assert_eq!(
wrapped, verdict,
"primary reached workload; verdict must NOT be wrapped: got {wrapped}",
);
}
#[test]
fn primary_reached_workload_distinguishes_payload_starting_from_scheduler_not_attached() {
use crate::test_support::output::primary_reached_workload;
use crate::vmm::host_comms::BulkDrainResult;
use crate::vmm::wire::{LifecyclePhase, MSG_TYPE_LIFECYCLE, ShmEntry};
let phase_entry = |phase: LifecyclePhase| ShmEntry {
msg_type: MSG_TYPE_LIFECYCLE,
crc_ok: true,
payload: vec![phase.wire_value()],
};
let drain_only_not_attached = BulkDrainResult {
entries: vec![phase_entry(LifecyclePhase::SchedulerNotAttached)],
};
assert!(
!primary_reached_workload(Some(&drain_only_not_attached)),
"SchedulerNotAttached without PayloadStarting must NOT count as \
reached workload — the auto-repro gate must check the frame \
directly, not via any future stage-string bucketing",
);
let drain_with_payload = BulkDrainResult {
entries: vec![phase_entry(LifecyclePhase::PayloadStarting)],
};
assert!(
primary_reached_workload(Some(&drain_with_payload)),
"PayloadStarting frame must count as reached workload",
);
assert!(
!primary_reached_workload(None),
"missing drain must NOT count as reached workload",
);
let drain_only_init = BulkDrainResult {
entries: vec![phase_entry(LifecyclePhase::InitStarted)],
};
assert!(
!primary_reached_workload(Some(&drain_only_init)),
"InitStarted without PayloadStarting must NOT count as reached workload",
);
let empty_drain = BulkDrainResult { entries: vec![] };
assert!(
!primary_reached_workload(Some(&empty_drain)),
"empty drain (Some, but no entries) must NOT count as reached workload",
);
let mut crc_bad = BulkDrainResult {
entries: vec![phase_entry(LifecyclePhase::PayloadStarting)],
};
crc_bad.entries[0].crc_ok = false;
assert!(
!primary_reached_workload(Some(&crc_bad)),
"CRC-bad PayloadStarting frame must NOT count as reached workload",
);
}
fn vm_result_with_drain(entries: Vec<crate::vmm::wire::ShmEntry>) -> crate::vmm::result::VmResult {
crate::vmm::result::VmResult {
guest_messages: Some(crate::vmm::host_comms::BulkDrainResult { entries }),
..crate::vmm::result::VmResult::test_fixture()
}
}
fn wprof_frame(payload: &[u8], crc_ok: bool) -> crate::vmm::wire::ShmEntry {
crate::vmm::wire::ShmEntry {
msg_type: crate::vmm::wire::MsgType::WprofTrace.wire_value(),
payload: payload.to_vec(),
crc_ok,
}
}
#[test]
fn write_auto_repro_sidecar_artifacts_writes_wprof_pb() {
let _env_lock = crate::test_support::test_helpers::lock_env();
let tmp = tempfile::tempdir().expect("tempdir");
let _sidecar = crate::test_support::test_helpers::EnvVarGuard::set(
crate::KTSTR_SIDECAR_DIR_ENV,
tmp.path(),
);
let entry = crate::test_support::test_helpers::eevdf_entry("write_auto_repro_wprof_fixture");
let payload = b"\x0a\x02hi";
let result = vm_result_with_drain(vec![wprof_frame(payload, true)]);
write_auto_repro_sidecar_artifacts(&entry, &result);
let pb = tmp
.path()
.join("write_auto_repro_wprof_fixture.repro.wprof.pb");
assert!(pb.exists(), "expected wprof .pb at {}", pb.display());
assert_eq!(
std::fs::read(&pb).expect("read wprof .pb"),
payload,
"payload must round-trip byte-for-byte",
);
}
#[test]
fn write_auto_repro_sidecar_artifacts_skips_crc_bad_wprof() {
let _env_lock = crate::test_support::test_helpers::lock_env();
let tmp = tempfile::tempdir().expect("tempdir");
let _sidecar = crate::test_support::test_helpers::EnvVarGuard::set(
crate::KTSTR_SIDECAR_DIR_ENV,
tmp.path(),
);
let entry = crate::test_support::test_helpers::eevdf_entry("write_auto_repro_crc_bad_fixture");
let result = vm_result_with_drain(vec![wprof_frame(b"garbage", false)]);
write_auto_repro_sidecar_artifacts(&entry, &result);
let pb = tmp
.path()
.join("write_auto_repro_crc_bad_fixture.repro.wprof.pb");
assert!(
!pb.exists(),
"crc_ok=false WprofTrace must NOT produce a sidecar file at {}",
pb.display(),
);
}
#[test]
fn wait_for_sched_disabled_at_covers_all_arms() {
let tiny = std::time::Duration::from_millis(1);
assert!(
!wait_for_sched_disabled_at("/nonexistent/ktstr/sched_ext/state", tiny),
"unreadable state path must yield a non-spinning false",
);
let tmp = tempfile::tempdir().expect("tempdir");
let disabled = tmp.path().join("disabled_state");
std::fs::write(&disabled, "disabled\n").expect("write disabled state");
assert!(
wait_for_sched_disabled_at(disabled.to_str().expect("utf8 path"), tiny),
"state reading \"disabled\" must yield true",
);
let enabled = tmp.path().join("enabled_state");
std::fs::write(&enabled, "enabled\n").expect("write enabled state");
assert!(
!wait_for_sched_disabled_at(enabled.to_str().expect("utf8 path"), tiny),
"non-\"disabled\" state must time out to false within the deadline",
);
}
#[test]
fn finalize_probe_after_unwind_noop_when_nothing_stashed() {
let _guard = DEFERRED_PROBE_TEST_LOCK.lock().unwrap();
let _ = take_deferred_probe();
assert!(
take_deferred_probe().is_none(),
"precondition: stash must be empty before the no-op call",
);
finalize_probe_after_unwind();
assert!(
take_deferred_probe().is_none(),
"no-op path must leave DEFERRED_PROBE_COLLECT empty \
(the early-return in finalize_probe_after_unwind)",
);
}
#[test]
fn collect_and_print_probe_data_none_handle_leaves_stop_false() {
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
collect_and_print_probe_data(stop.clone(), None);
assert!(
!stop.load(std::sync::atomic::Ordering::Acquire),
"None-handle path must early-return before stop.store, \
leaving stop == false",
);
}
#[test]
fn publish_result_and_collect_host_arm_leaves_stop_false() {
let _g = crate::vmm::guest_comms::IsGuestOverrideGuard::new(false);
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
publish_result_and_collect(&AssertResult::pass(), stop.clone(), None);
assert!(
!stop.load(std::sync::atomic::Ordering::Acquire),
"host arm calls collect_and_print_probe_data(stop, None) which \
early-returns before stop.store; stop must stay false",
);
}
#[test]
fn publish_result_and_collect_stash_arm_stashes_deferred() {
let _guard = DEFERRED_PROBE_TEST_LOCK.lock().unwrap();
let _ = take_deferred_probe();
let _g = crate::vmm::guest_comms::IsGuestOverrideGuard::new(true);
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
publish_result_and_collect(&AssertResult::pass(), stop, None);
let stashed = take_deferred_probe();
assert!(
stashed.is_some(),
"guest arm must stash a DeferredProbe via stash_deferred_probe; \
take_deferred_probe must return Some",
);
assert!(
stashed.expect("stash present").handle.is_none(),
"stashed handle must round-trip the None passed in",
);
let _ = take_deferred_probe();
}
#[test]
fn emit_probe_payload_empty_events_round_trips_diagnostics_only() {
emit_probe_payload(
&[],
&[],
&PipelineDiagnostics::default(),
&crate::probe::process::ProbeDiagnostics::default(),
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
);
let payload = ProbeBytes {
events: vec![],
func_names: vec![],
bpf_source_locs: std::collections::HashMap::new(),
diagnostics: Some(ProbeBytesDiagnostics {
pipeline: PipelineDiagnostics::default(),
skeleton: crate::probe::process::ProbeDiagnostics::default(),
}),
nr_cpus: crate::probe::output::get_nr_cpus(),
param_names: std::collections::HashMap::new(),
render_hints: std::collections::HashMap::new(),
};
let json = serde_json::to_string(&payload).expect("serialize empty-events payload");
let output = format!("{PROBE_OUTPUT_START}\n{json}\n{PROBE_OUTPUT_END}");
let formatted = extract_probe_output(&output, None, None)
.expect("empty-events payload with default diagnostics must render the pipeline header");
assert!(
formatted.contains("--- probe pipeline ---"),
"diagnostics header missing (format_probe_diagnostics's `--- probe pipeline ---` push): {formatted}",
);
assert!(
formatted.contains("0 captured, 0 after stitch"),
"empty-events stitch counters missing (format_probe_diagnostics's \
`{{}} captured, {{}} after stitch` line, no cause \
appended since events_before_stitch == 0): {formatted}",
);
}