mod support;
use std::{path::PathBuf, process::Command};
use crate::support::{analyse_fixture, fixture_dir};
const MACHINERY: &[&str] = &[
"core::panicking::panic_in_cleanup",
"core::panicking::panic_cannot_unwind",
"std::panicking::panic_count",
"std::panicking::catch_unwind",
"rust_eh_personality",
];
const ENTRIES: &[(&str, &[&str])] = &[
("core::panicking::panic_bounds_check", &["index"]),
("core::slice::index::", &["index"]),
("core::str::slice_error_fail", &["str-boundary"]),
("core::option::unwrap_failed", &["unwrap", "poison", "fmt"]),
("core::option::expect_failed", &["unwrap", "poison", "fmt"]),
("core::result::unwrap_failed", &["unwrap", "poison", "fmt"]),
("core::cell::panic_already", &["borrow"]),
("alloc::raw_vec::capacity_overflow", &["capacity-overflow"]),
(
"alloc::raw_vec::handle_error",
&["capacity-overflow", "alloc-failure"],
),
("handle_alloc_error", &["alloc-failure"]),
("resume_unwind", &["explicit"]),
("len_mismatch_fail", &["explicit"]),
("core::panicking::", &["explicit"]),
];
const SUSPICIOUS: &[&str] = &[
"core::panicking::",
"core::slice::index::slice_",
"core::str::slice_error",
"core::option::unwrap",
"core::option::expect",
"core::result::unwrap",
"core::cell::panic",
"alloc::raw_vec::capacity",
"alloc::raw_vec::handle",
"alloc::alloc::handle",
];
fn text_of(cmd: &mut Command) -> String {
let output = cmd.output().expect("the command should run");
assert!(
output.status.success(),
"the command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).expect("the output should be utf-8")
}
fn llvm_nm() -> PathBuf {
let sysroot = text_of(Command::new("rustc").arg("--print").arg("sysroot"));
let host = text_of(Command::new("rustc").arg("-vV"))
.lines()
.find_map(|line| line.strip_prefix("host: ").map(str::to_owned))
.expect("rustc should report a host triple");
let nm = PathBuf::from(sysroot.trim())
.join("lib")
.join("rustlib")
.join(host.trim())
.join("bin")
.join("llvm-nm");
assert!(
nm.exists(),
"llvm-nm is missing; install the llvm-tools component"
);
nm
}
#[test]
fn every_panic_entry_in_the_binary_is_dominated_by_a_finding() {
let reported = analyse_fixture("release", &[]);
let mut live: Vec<String> = Vec::new();
for (_, categories) in &reported {
for category in categories {
if !live.contains(category) {
live.push(category.clone());
}
}
}
let rlib = fixture_dir()
.join("target")
.join("panicgraph")
.join("build")
.join("release")
.join("libknown.rlib");
assert!(rlib.exists(), "the analysis build should leave {rlib:?}");
let listing = text_of(
Command::new(llvm_nm())
.arg("--demangle")
.arg("--undefined-only")
.arg(&rlib),
);
for line in listing.lines() {
let Some(symbol) = line.trim().strip_prefix("U ") else {
continue;
};
if MACHINERY.iter().any(|known| symbol.contains(known)) {
continue;
}
if let Some((_, categories)) =
ENTRIES.iter().find(|(name, _)| symbol.contains(name))
{
assert!(
categories.iter().any(|c| live.iter().any(|l| l == c)),
"the binary calls {symbol}, but no finding carries any of \
{categories:?}; the analysis lost sight of this entry point"
);
continue;
}
assert!(
!SUSPICIOUS.iter().any(|prefix| symbol.contains(prefix)),
"the binary calls {symbol}, which looks like a panic entry \
point this test does not know; the sink table and this sweep \
both need a row for it"
);
}
}