use super::super::output::STAGE_INIT_STARTED_NO_PAYLOAD;
use super::super::test_helpers::lifecycle_drain;
#[cfg(feature = "wprof")]
use super::super::test_helpers::{EnvVarGuard, lock_env, sched_entry};
use super::*;
use tempfile::TempDir;
#[test]
fn placeholder_dump_writes_when_path_missing() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test_writes.failure-dump.json");
let result = vmm::VmResult {
success: false,
..vmm::VmResult::test_fixture()
};
write_placeholder_failure_dump_if_missing(&path, &result);
assert!(path.exists(), "placeholder must land at the canonical path");
let body = std::fs::read_to_string(&path).expect("readable");
let report: crate::monitor::dump::FailureDumpReport =
serde_json::from_str(&body).expect("valid FailureDumpReport JSON");
assert!(report.is_placeholder, "stub must carry is_placeholder=true",);
let reason = report
.sdt_alloc_unavailable
.as_deref()
.expect("placeholder sets sdt_alloc_unavailable");
assert!(
reason.contains("no BPF state captured"),
"reason must explain why no real dump exists: {reason}",
);
}
#[test]
fn placeholder_dump_skipped_when_file_exists() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test_exists.failure-dump.json");
let sentinel: &[u8] = br#"{"real":true,"is_placeholder":false}"#;
std::fs::write(&path, sentinel).unwrap();
let result = vmm::VmResult {
success: false,
..vmm::VmResult::test_fixture()
};
write_placeholder_failure_dump_if_missing(&path, &result);
let after = std::fs::read(&path).unwrap();
assert_eq!(
after, sentinel,
"real dump must not be overwritten by placeholder",
);
}
#[test]
fn placeholder_dump_atomic_publish_no_tmp_orphan() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test_atomic.failure-dump.json");
let result = vmm::VmResult {
success: false,
..vmm::VmResult::test_fixture()
};
write_placeholder_failure_dump_if_missing(&path, &result);
assert!(path.exists());
let tmp = path.with_extension("json.tmp");
assert!(
!tmp.exists(),
"atomic rename(2) must consume the .tmp file; orphan at: {}",
tmp.display(),
);
}
#[test]
fn placeholder_dump_reason_includes_lifecycle_stage_label() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test_stage.failure-dump.json");
let drain = lifecycle_drain(&[crate::vmm::wire::LifecyclePhase::InitStarted]);
let result = vmm::VmResult {
success: false,
guest_messages: Some(drain),
..vmm::VmResult::test_fixture()
};
write_placeholder_failure_dump_if_missing(&path, &result);
let body = std::fs::read_to_string(&path).unwrap();
let report: crate::monitor::dump::FailureDumpReport = serde_json::from_str(&body).unwrap();
let reason = report.sdt_alloc_unavailable.as_deref().unwrap();
assert!(
reason.contains(STAGE_INIT_STARTED_NO_PAYLOAD),
"reason must include the lifecycle stage label `{}`: {reason}",
STAGE_INIT_STARTED_NO_PAYLOAD,
);
}
#[test]
fn placeholder_dump_reason_includes_bug_summary_from_sched_log() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test_bug.failure-dump.json");
let result = vmm::VmResult {
success: false,
output: "scx_bpf_error: apply_cell_config returned -EINVAL\n".to_string(),
..vmm::VmResult::test_fixture()
};
write_placeholder_failure_dump_if_missing(&path, &result);
let body = std::fs::read_to_string(&path).unwrap();
let report: crate::monitor::dump::FailureDumpReport = serde_json::from_str(&body).unwrap();
let reason = report.sdt_alloc_unavailable.as_deref().unwrap();
assert!(
reason.contains("BUG SUMMARY:"),
"reason must fold the BUG SUMMARY extraction: {reason}",
);
assert!(
reason.contains("apply_cell_config returned -EINVAL"),
"BUG SUMMARY text must surface the actionable scx_bpf_error: {reason}",
);
}
#[test]
fn placeholder_dump_reason_omits_bug_summary_when_none() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test_no_bug.failure-dump.json");
let result = vmm::VmResult {
success: false,
..vmm::VmResult::test_fixture()
};
write_placeholder_failure_dump_if_missing(&path, &result);
let body = std::fs::read_to_string(&path).unwrap();
let report: crate::monitor::dump::FailureDumpReport = serde_json::from_str(&body).unwrap();
let reason = report.sdt_alloc_unavailable.as_deref().unwrap();
assert!(
!reason.contains("BUG SUMMARY"),
"reason must not mention BUG SUMMARY when no actionable text was extracted: {reason}",
);
}
#[test]
fn placeholder_dump_production_call_sites_are_failure_gated() {
let src = include_str!("mod.rs");
let lines: Vec<&str> = src.lines().collect();
let scan_end = lines.len();
let call_lines: Vec<usize> = lines
.iter()
.enumerate()
.take(scan_end)
.filter_map(|(i, l)| {
l.contains("write_placeholder_failure_dump_if_missing(&primary_dump_path")
.then_some(i)
})
.collect();
let display_lines: Vec<usize> = call_lines.iter().map(|i| i + 1).collect();
assert_eq!(
call_lines.len(),
2,
"expected exactly 2 production call sites (matched on &primary_dump_path \
to exclude helper definition and unit-test call sites which use &path); \
found {} at lines {:?}",
call_lines.len(),
display_lines,
);
let success_gated = call_lines.iter().copied().find(|&i| {
lines[i.saturating_sub(10)..=i]
.join("\n")
.contains("if !result.success")
});
let post_vm_gated = call_lines.iter().copied().find(|&i| {
let window = lines[i.saturating_sub(20)..=i].join("\n");
window.contains("run_post_vm_callbacks(entry, &result, guest_already_failed)")
&& window.contains("post_vm_err.is_some()")
});
let success_gated = success_gated.unwrap_or_else(|| {
panic!(
"no production call site is gated by `if !result.success` (post-`vm.run` \
placeholder emission); production sites at lines: {display_lines:?}",
)
});
let post_vm_gated = post_vm_gated.unwrap_or_else(|| {
panic!(
"no production call site is gated by both \
`run_post_vm_callbacks(entry, &result, guest_already_failed)` \
(the post_vm_err binding source) AND `post_vm_err.is_some()` \
(the gate); production sites at lines: {display_lines:?}",
)
});
assert_ne!(
success_gated,
post_vm_gated,
"the same call site (line {}) satisfies both gate patterns; \
each gate should guard a different call site (sites: {display_lines:?})",
success_gated + 1,
);
}
fn strip_named_arg_prefix(s: &str) -> String {
let rest = match s.strip_prefix('"') {
Some(r) => r,
None => return s.to_string(),
};
let rest = match rest.strip_prefix('{') {
Some(r) => r,
None => return s.to_string(),
};
let end = match rest.find('}') {
Some(e) => e,
None => return s.to_string(),
};
let name = &rest[..end];
let is_named_arg = !name.is_empty()
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
&& name.chars().next().is_some_and(|c| !c.is_ascii_digit());
if !is_named_arg {
return s.to_string();
}
format!("\"{}", &rest[end + 1..])
}
#[test]
fn bug_summary_line_immediately_follows_fingerprint_line_in_all_failure_messages() {
let src = include_str!("mod.rs");
let lines: Vec<&str> = src.lines().collect();
let scan_end = lines.len();
let fingerprint_arg_lines: Vec<usize> = lines
.iter()
.enumerate()
.take(scan_end)
.filter_map(|(i, l)| (l.trim() == "fingerprint_line,").then_some(i))
.collect();
let display_lines: Vec<usize> = fingerprint_arg_lines.iter().map(|i| i + 1).collect();
assert_eq!(
fingerprint_arg_lines.len(),
4,
"expected exactly 4 failure-message format!() sites passing \
`fingerprint_line,` as a positional argument (assert-fail, \
monitor-fail, timeout, no-result paths); found {} at lines {:?}. \
If a 5th failure path was added, extend this test; if a path was \
removed, update the expected count.",
fingerprint_arg_lines.len(),
display_lines,
);
for &i in &fingerprint_arg_lines {
let next = lines
.get(i + 1)
.unwrap_or_else(|| panic!("no line after fingerprint_line, at {}", i + 1));
assert_eq!(
next.trim(),
"bug_summary_line(),",
"failure-message format!() at eval.rs:{} passes `fingerprint_line,` \
but the next positional argument is `{}` (trimmed), not \
`bug_summary_line(),`. The BUG SUMMARY must render at the top \
of every failure message so it surfaces above the test-name / \
topology line in CI logs.",
i + 1,
next.trim(),
);
let fmt_line_idx = (0..i)
.rev()
.find(|&k| !lines[k].trim().is_empty())
.unwrap_or(0);
let trimmed = lines[fmt_line_idx].trim();
let stripped = strip_named_arg_prefix(trimmed);
assert!(
stripped.starts_with("\"{}{}"),
"failure-message format!() at eval.rs:{} passes `fingerprint_line,` \
then `bug_summary_line(),` as args 0+1, but the preceding format \
string literal at eval.rs:{} (`{}`) does NOT start with `\"{{}}{{}}` \
(default positional indices for args 0+1 in order, optionally \
preceded by a single named-arg span like `{{post_vm_prefix}}`). \
A regression that reordered the format-string indices (e.g. \
`\"{{2}}{{0}}{{1}}...\"`) would render the BUG SUMMARY below \
the test-name / topology line even though the args list still \
threads `bug_summary_line()` as the second positional.",
i + 1,
fmt_line_idx + 1,
trimmed,
);
}
}
#[test]
fn bug_summary_line_renders_bug_summary_prefix_in_both_arms() {
let src = include_str!("mod.rs");
let lines: Vec<&str> = src.lines().collect();
let opener_idx = lines
.iter()
.position(|l| l.contains("let bug_summary_line = || -> String {"))
.expect(
"bug_summary_line closure opener `let bug_summary_line = || -> String {` \
must exist in eval.rs",
);
let closer_idx = lines
.iter()
.enumerate()
.skip(opener_idx + 1)
.find(|(_, l)| l.trim() == "};")
.map(|(i, _)| i)
.expect("bug_summary_line closure must close with `};` on its own line");
let body = &lines[opener_idx..=closer_idx];
let prefix_lines: Vec<usize> = body
.iter()
.enumerate()
.filter_map(|(off, l)| l.contains("BUG SUMMARY:").then_some(opener_idx + off + 1))
.collect();
assert_eq!(
prefix_lines.len(),
2,
"expected the literal `BUG SUMMARY:` prefix to appear EXACTLY twice in \
the `bug_summary_line` closure body (eval.rs:{}-{}): once in the \
ANSI-colored arm and once in the plain arm. Found {} occurrence(s) \
at lines {:?}. A regression that renamed either arm's prefix (e.g. \
`BPF ERROR:`, `BUG_SUMMARY:`, dropped the colon) would break the \
post-ANSI-strip byte sequence that downstream CI log parsers grep for.",
opener_idx + 1,
closer_idx + 1,
prefix_lines.len(),
prefix_lines,
);
}
#[test]
fn matcher_dispatch_and_mismatch_marker_wiring_pinned() {
let src = include_str!("mod.rs");
let lines: Vec<&str> = src.lines().collect();
let scan_end = lines.len();
let find_sites = |needle: &str| -> Vec<usize> {
lines
.iter()
.enumerate()
.take(scan_end)
.filter_map(|(i, l)| {
if l.trim_start().starts_with("//") {
return None;
}
l.contains(needle).then_some(i)
})
.collect()
};
let assert_gated = |site: usize, label: &str, gate: &str| {
let window = lines[site.saturating_sub(8)..=site].join("\n");
assert!(
window.contains(gate),
"{label} (line {}) must be gated by `{gate}`; \
8-line lookback window:\n{window}",
site + 1,
);
};
let assert_unique = |sites: &[usize], label: &str| -> usize {
assert_eq!(
sites.len(),
1,
"expected exactly 1 {label} site; found {} at lines {:?}",
sites.len(),
sites.iter().map(|i| i + 1).collect::<Vec<_>>(),
);
sites[0]
};
let dispatch_site = assert_unique(
&find_sites("merged_assert.evaluate_scx_bpf_error_match("),
"matcher dispatch",
);
assert_gated(dispatch_site, "matcher dispatch", "if matcher_configured");
assert!(
lines[dispatch_site].contains("entry.expect_err"),
"matcher dispatch (line {}) must forward `entry.expect_err` to drive \
the inversion check; line: {}",
dispatch_site + 1,
lines[dispatch_site],
);
let marker_site = assert_unique(
&find_sites("err.context(ScxBpfErrorMatcherMismatch)"),
"marker attach",
);
assert_gated(marker_site, "marker attach", "if matcher_mismatch");
let configured_site = assert_unique(
&find_sites("let matcher_configured ="),
"`let matcher_configured =` assignment",
);
let configured_window =
lines[configured_site..=configured_site.saturating_add(2).min(scan_end - 1)].join("\n");
assert!(
configured_window.contains("expect_scx_bpf_error_contains.is_some()")
&& configured_window.contains("expect_scx_bpf_error_matches.is_some()"),
"matcher_configured must derive from the matcher fields' \
`.is_some()` checks, not a hardcoded literal; assignment window:\n\
{configured_window}",
);
let mismatch_site = assert_unique(
&find_sites("let matcher_mismatch ="),
"`let matcher_mismatch =` assignment",
);
let mismatch_line = lines[mismatch_site];
assert!(
mismatch_line.contains("matcher_details.is_empty()"),
"matcher_mismatch must derive from `matcher_details.is_empty()`, \
not a hardcoded literal; assignment line: {mismatch_line}",
);
}
#[test]
fn resolve_staged_schedulers_strict_preserves_entry_iteration_order() {
use crate::test_support::Scheduler;
static FIRST: Scheduler = Scheduler::named("scx_alpha").binary_discover("scx_alpha_bin");
static SECOND: Scheduler = Scheduler::named("scx_beta").binary_discover("scx_beta_bin");
static THIRD: Scheduler = Scheduler::named("scx_gamma").binary_discover("scx_gamma_bin");
static SCHEDS: &[&Scheduler] = &[&FIRST, &SECOND, &THIRD];
let entry = crate::test_support::entry::KtstrTestEntry {
name: "order_pin",
staged_schedulers: SCHEDS,
..crate::test_support::entry::KtstrTestEntry::DEFAULT
};
let resolved = resolve_staged_schedulers_strict(&entry, |spec| {
let key = match spec {
SchedulerSpec::Discover(s) => s.to_string(),
_ => "unexpected_variant".to_string(),
};
Ok(Some(PathBuf::from(format!("/synthetic/{key}"))))
})
.expect("strict resolver succeeds on synthetic happy path");
let names: Vec<&str> = resolved.iter().map(|(n, _, _)| n.as_str()).collect();
assert_eq!(
names,
vec!["scx_alpha", "scx_beta", "scx_gamma"],
"resolution MUST preserve entry.staged_schedulers declaration order; \
a future refactor that collects via HashMap would silently scramble \
initramfs staging layout"
);
let paths: Vec<String> = resolved
.iter()
.map(|(_, p, _)| p.display().to_string())
.collect();
assert_eq!(
paths,
vec![
"/synthetic/scx_alpha_bin",
"/synthetic/scx_beta_bin",
"/synthetic/scx_gamma_bin",
],
"synthetic resolver paths must align with iteration order — \
confirms the per-entry resolver call happens in declaration order"
);
}
#[test]
fn resolve_staged_schedulers_strict_skips_resolver_none() {
use crate::test_support::Scheduler;
static BINARY: Scheduler = Scheduler::named("scx_real").binary_discover("scx_real_bin");
static BUILTIN: Scheduler = Scheduler::named("scx_builtin").binary_discover("scx_skip");
static SCHEDS: &[&Scheduler] = &[&BINARY, &BUILTIN];
let entry = crate::test_support::entry::KtstrTestEntry {
name: "none_skip",
staged_schedulers: SCHEDS,
..crate::test_support::entry::KtstrTestEntry::DEFAULT
};
let resolved = resolve_staged_schedulers_strict(&entry, |spec| match spec {
SchedulerSpec::Discover("scx_skip") => Ok(None),
SchedulerSpec::Discover(s) => Ok(Some(PathBuf::from(format!("/synthetic/{s}")))),
_ => Ok(None),
})
.expect("strict resolver succeeds; None entries are dropped not errored");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].0, "scx_real");
}
#[test]
fn resolve_staged_schedulers_strict_propagates_resolver_error() {
use crate::test_support::Scheduler;
static SCHED: Scheduler = Scheduler::named("scx_fail").binary_discover("scx_fail_bin");
static SCHEDS: &[&Scheduler] = &[&SCHED];
let entry = crate::test_support::entry::KtstrTestEntry {
name: "err_propagate",
staged_schedulers: SCHEDS,
..crate::test_support::entry::KtstrTestEntry::DEFAULT
};
let err = resolve_staged_schedulers_strict(&entry, |_spec| {
Err::<Option<PathBuf>, _>(anyhow::anyhow!(
"synthetic resolver error — staged binary not found on host"
))
})
.expect_err("strict resolver must propagate error, not swallow");
assert!(
err.to_string().contains("synthetic resolver error"),
"error chain must preserve resolver's message, got: {err:#}"
);
}
#[cfg(feature = "wprof")]
fn expect_auto_repro_entry(name: &'static str) -> KtstrTestEntry {
KtstrTestEntry {
expect_auto_repro: true,
..sched_entry(name)
}
}
#[cfg(feature = "wprof")]
fn failing_vm_result_with_name(name: &'static str) -> crate::vmm::VmResult {
crate::vmm::VmResult {
success: false,
entry_name: Some(name),
..crate::vmm::VmResult::test_fixture()
}
}
#[cfg(feature = "wprof")]
fn write_valid_repro_artifact(sidecar_dir: &std::path::Path, name: &str) {
use crate::test_support::wprof::{PERFETTO_TRACE_PACKETS_TAG, WPROF_PB_MIN_BYTES};
let mut bytes = vec![PERFETTO_TRACE_PACKETS_TAG];
bytes.resize(WPROF_PB_MIN_BYTES, 0);
let path = sidecar_dir.join(format!("{name}.repro.wprof.pb"));
std::fs::write(&path, &bytes).expect("write valid repro artifact");
}
#[cfg(feature = "wprof")]
#[test]
fn apply_expect_auto_repro_inversion_no_op_when_attr_unset() {
let _lock = lock_env();
let dir = TempDir::new().expect("tempdir");
let _env = EnvVarGuard::set(
crate::KTSTR_SIDECAR_DIR_ENV,
dir.path().to_str().expect("utf8 tempdir"),
);
let entry = sched_entry("attr_unset");
assert!(!entry.expect_auto_repro, "fixture must leave attr false");
write_valid_repro_artifact(dir.path(), "attr_unset");
let mut result = failing_vm_result_with_name("attr_unset");
apply_expect_auto_repro_inversion(&entry, &mut result);
assert!(
!result.expect_auto_repro_satisfied,
"attr-unset run must leave field false even when artifact is shape-valid"
);
}
#[cfg(feature = "wprof")]
#[test]
fn apply_expect_auto_repro_inversion_no_op_when_success_true() {
let _lock = lock_env();
let dir = TempDir::new().expect("tempdir");
let _env = EnvVarGuard::set(
crate::KTSTR_SIDECAR_DIR_ENV,
dir.path().to_str().expect("utf8 tempdir"),
);
let entry = expect_auto_repro_entry("success_true");
write_valid_repro_artifact(dir.path(), "success_true");
let mut result = crate::vmm::VmResult {
success: true,
entry_name: Some("success_true"),
..crate::vmm::VmResult::test_fixture()
};
apply_expect_auto_repro_inversion(&entry, &mut result);
assert!(
!result.expect_auto_repro_satisfied,
"success=true run must leave field false even when artifact is shape-valid"
);
}
#[cfg(feature = "wprof")]
#[test]
fn apply_expect_auto_repro_inversion_no_op_when_entry_name_none() {
let _lock = lock_env();
let dir = TempDir::new().expect("tempdir");
let _env = EnvVarGuard::set(
crate::KTSTR_SIDECAR_DIR_ENV,
dir.path().to_str().expect("utf8 tempdir"),
);
let entry = expect_auto_repro_entry("entry_name_none");
let mut result = crate::vmm::VmResult {
success: false,
entry_name: None,
..crate::vmm::VmResult::test_fixture()
};
apply_expect_auto_repro_inversion(&entry, &mut result);
assert!(
!result.expect_auto_repro_satisfied,
"entry_name=None must trip the path-resolve bail and leave field false"
);
}
#[cfg(feature = "wprof")]
#[test]
fn apply_expect_auto_repro_inversion_no_op_when_artifact_missing() {
let _lock = lock_env();
let dir = TempDir::new().expect("tempdir");
let _env = EnvVarGuard::set(
crate::KTSTR_SIDECAR_DIR_ENV,
dir.path().to_str().expect("utf8 tempdir"),
);
let entry = expect_auto_repro_entry("artifact_missing");
let mut result = failing_vm_result_with_name("artifact_missing");
apply_expect_auto_repro_inversion(&entry, &mut result);
assert!(
!result.expect_auto_repro_satisfied,
"missing artifact must trip the shape-check bail and leave field false"
);
}
#[cfg(feature = "wprof")]
#[test]
fn apply_expect_auto_repro_inversion_no_op_when_artifact_truncated() {
use crate::test_support::wprof::{PERFETTO_TRACE_PACKETS_TAG, WPROF_PB_MIN_BYTES};
let _lock = lock_env();
let dir = TempDir::new().expect("tempdir");
let _env = EnvVarGuard::set(
crate::KTSTR_SIDECAR_DIR_ENV,
dir.path().to_str().expect("utf8 tempdir"),
);
let entry = expect_auto_repro_entry("artifact_truncated");
let mut bytes = vec![PERFETTO_TRACE_PACKETS_TAG];
bytes.resize(WPROF_PB_MIN_BYTES - 1, 0);
std::fs::write(dir.path().join("artifact_truncated.repro.wprof.pb"), &bytes)
.expect("write truncated artifact");
let mut result = failing_vm_result_with_name("artifact_truncated");
apply_expect_auto_repro_inversion(&entry, &mut result);
assert!(
!result.expect_auto_repro_satisfied,
"truncated artifact must trip the size gate and leave field false"
);
}
#[cfg(feature = "wprof")]
#[test]
fn apply_expect_auto_repro_inversion_no_op_when_artifact_wrong_tag() {
use crate::test_support::wprof::WPROF_PB_MIN_BYTES;
let _lock = lock_env();
let dir = TempDir::new().expect("tempdir");
let _env = EnvVarGuard::set(
crate::KTSTR_SIDECAR_DIR_ENV,
dir.path().to_str().expect("utf8 tempdir"),
);
let entry = expect_auto_repro_entry("artifact_wrong_tag");
let mut bytes = vec![0xff]; bytes.resize(WPROF_PB_MIN_BYTES, 0);
std::fs::write(dir.path().join("artifact_wrong_tag.repro.wprof.pb"), &bytes)
.expect("write wrong-tag artifact");
let mut result = failing_vm_result_with_name("artifact_wrong_tag");
apply_expect_auto_repro_inversion(&entry, &mut result);
assert!(
!result.expect_auto_repro_satisfied,
"wrong-tag artifact must trip the tag gate and leave field false"
);
}
#[cfg(feature = "wprof")]
#[test]
fn apply_expect_auto_repro_inversion_sets_field_on_valid_artifact() {
let _lock = lock_env();
let dir = TempDir::new().expect("tempdir");
let _env = EnvVarGuard::set(
crate::KTSTR_SIDECAR_DIR_ENV,
dir.path().to_str().expect("utf8 tempdir"),
);
let entry = expect_auto_repro_entry("valid_artifact");
write_valid_repro_artifact(dir.path(), "valid_artifact");
let mut result = failing_vm_result_with_name("valid_artifact");
apply_expect_auto_repro_inversion(&entry, &mut result);
assert!(
result.expect_auto_repro_satisfied,
"shape-valid artifact + every upstream gate satisfied must set field true"
);
}