#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::arithmetic_side_effects,
clippy::indexing_slicing
)]
use std::path::{Path, PathBuf};
use nimrod::{EntrypointKind, NimBinary, NimVersionHint, rtti::symbols::RttiVersion};
fn fixture_path(rel: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(rel)
}
fn parse_fixture(rel: &str) -> Option<Vec<u8>> {
std::fs::read(fixture_path(rel)).ok()
}
const NIGHTLY_ELF: &str = "nightly/linux_x64/nim-2.3.1/bin/nim";
#[test]
fn elf_v2_type_graph_is_fully_readable() {
let Some(data) = parse_fixture(NIGHTLY_ELF) else {
eprintln!("skip: {NIGHTLY_ELF} not present");
return;
};
let bin = NimBinary::from_bytes(&data).unwrap();
let types = bin.types();
assert!(
types.len() >= 80,
"expected many V2 types, got {}",
types.len()
);
assert_eq!(types.len(), bin.rtti_symbols().len());
for t in types {
assert_eq!(t.version, RttiVersion::V2);
assert!(
t.is_readable(),
"V2 type {} should be file-backed",
t.symbol_name
);
assert!(
t.align > 0,
"align should be populated for {}",
t.symbol_name
);
assert!(t.depth.is_some(), "V2 depth should be populated");
}
assert!(
types.iter().any(|t| !t.display_tokens.is_empty()),
"expected V2 inheritance display tokens"
);
let with_dtor = types.iter().filter(|t| t.destructor.is_some()).count();
assert!(with_dtor > 0, "expected resolved destructors");
}
#[test]
fn elf_v2_display_tokens_track_depth() {
let Some(data) = parse_fixture(NIGHTLY_ELF) else {
return;
};
let bin = NimBinary::from_bytes(&data).unwrap();
let mut checked = 0;
for t in bin.types() {
if t.display_tokens.is_empty() {
continue;
}
let depth = t.depth.expect("V2 type has depth");
let expected = usize::try_from(depth).unwrap() + 1;
assert_eq!(
t.display_tokens.len(),
expected,
"{}: display token count should be depth+1",
t.symbol_name
);
checked += 1;
}
assert!(
checked > 0,
"expected at least one type with display tokens"
);
}
#[test]
fn macho_v1_types_degrade_to_name_only() {
let Some(data) = parse_fixture("native/exceptions_refc_debug") else {
eprintln!("skip: native/exceptions_refc_debug not present");
return;
};
let bin = NimBinary::from_bytes(&data).unwrap();
let types = bin.types();
assert!(!types.is_empty(), "expected V1 RTTI symbols");
for t in types {
assert_eq!(t.version, RttiVersion::V1);
assert!(!t.is_readable(), "Mach-O V1 should be name-only");
assert!(t.type_fragment.is_some());
}
let frags: Vec<&str> = types
.iter()
.filter_map(|t| t.type_fragment.as_deref())
.collect();
assert!(
frags.iter().any(|f| f.contains("myerror")),
"expected MyError fragment, got {frags:?}"
);
}
#[test]
fn compiler_version_distinguishes_modes() {
let cases = [
("native/exceptions_arc_release", NimVersionHint::Nim2xArc),
("native/exceptions_orc_release", NimVersionHint::Nim2xOrc),
("native/exceptions_refc_debug", NimVersionHint::Nim1xRefc),
];
for (rel, expected) in cases {
let Some(data) = parse_fixture(rel) else {
eprintln!("skip: {rel} not present");
continue;
};
let bin = NimBinary::from_bytes(&data).unwrap();
assert_eq!(bin.nim_version(), expected, "{rel}: version mismatch");
}
}
#[test]
fn code_entrypoints_aggregate_sources() {
let Some(data) = parse_fixture(NIGHTLY_ELF) else {
return;
};
let bin = NimBinary::from_bytes(&data).unwrap();
let eps = bin.code_entrypoints();
assert!(!eps.is_empty());
assert!(
eps.windows(2).all(|w| w[0].va < w[1].va),
"must be VA-sorted & deduped"
);
let has = |k: EntrypointKind| eps.iter().any(|e| e.kind == k);
assert!(has(EntrypointKind::EntryShim), "expected entry shims");
assert!(has(EntrypointKind::ModuleInit), "expected module inits");
assert!(has(EntrypointKind::ProcSymbol), "expected proc symbols");
}
#[test]
fn format_and_bitness_predicates() {
if let Some(data) = parse_fixture(NIGHTLY_ELF) {
let bin = NimBinary::from_bytes(&data).unwrap();
assert!(bin.format().is_elf());
assert!(!bin.format().is_pe());
assert!(bin.is_64bit());
assert_eq!(bin.bitness(), Some(64));
}
}