use std::path::PathBuf;
use std::process::Command;
fn locate_objdump() -> Option<PathBuf> {
for candidate in ["llvm-objdump", "objdump"] {
let probe = Command::new(candidate).arg("--version").output();
if probe.ok().filter(|o| o.status.success()).is_some() {
return Some(PathBuf::from(candidate));
}
}
None
}
fn current_test_binary() -> std::io::Result<PathBuf> {
std::env::current_exe()
}
pub fn disassemble_self() -> Result<String, AsmCheckError> {
let tool = locate_objdump().ok_or(AsmCheckError::ToolMissing)?;
let bin = current_test_binary().map_err(|e| AsmCheckError::IoError(e.to_string()))?;
let out = Command::new(&tool)
.arg("-d")
.arg("--no-show-raw-insn")
.arg(&bin)
.output()
.map_err(|e| AsmCheckError::IoError(e.to_string()))?;
if !out.status.success() {
return Err(AsmCheckError::ToolFailed {
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
});
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
pub fn function_section<'a>(disasm: &'a str, name_substr: &str) -> Option<&'a str> {
let mut start = None;
for (i, line) in disasm.lines().enumerate() {
if line.contains(name_substr) && line.trim_end().ends_with(':') {
start = Some(i);
break;
}
}
let start = start?;
let lines: Vec<&str> = disasm.lines().collect();
let mut end = lines.len();
for j in (start + 1)..lines.len() {
if lines[j].ends_with(':')
&& !lines[j].trim_start().starts_with('0') && !lines[j].is_empty()
{
end = j;
break;
}
}
let from = lines[..start].iter().map(|s| s.len() + 1).sum::<usize>();
let to = lines[..end]
.iter()
.map(|s| s.len() + 1)
.sum::<usize>()
.min(disasm.len());
Some(&disasm[from..to])
}
pub fn assert_function_contains(name_substr: &str, expected: &[&str]) -> Result<(), AsmCheckError> {
let disasm = disassemble_self()?;
let body = function_section(&disasm, name_substr).ok_or(AsmCheckError::FunctionNotFound {
name: name_substr.into(),
})?;
for pat in expected {
if !body.contains(pat) {
return Err(AsmCheckError::PatternMissing {
function: name_substr.into(),
pattern: (*pat).into(),
});
}
}
Ok(())
}
#[derive(Debug)]
pub enum AsmCheckError {
ToolMissing,
ToolFailed { stderr: String },
IoError(String),
FunctionNotFound { name: String },
PatternMissing { function: String, pattern: String },
}
impl std::fmt::Display for AsmCheckError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ToolMissing => write!(f, "neither llvm-objdump nor objdump found in PATH"),
Self::ToolFailed { stderr } => write!(f, "objdump failed: {stderr}"),
Self::IoError(s) => write!(f, "io error: {s}"),
Self::FunctionNotFound { name } => {
write!(f, "function matching `{name}` not found in disassembly")
}
Self::PatternMissing { function, pattern } => write!(
f,
"function `{function}` is missing expected pattern `{pattern}`"
),
}
}
}
impl std::error::Error for AsmCheckError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore]
fn disassemble_self_succeeds() {
match disassemble_self() {
Ok(d) => assert!(d.len() > 1024, "disassembly suspiciously small"),
Err(AsmCheckError::ToolMissing) => {
eprintln!("[asm-check] skipping: objdump not in PATH");
}
Err(e) => panic!("disassembly failed: {e}"),
}
}
#[test]
#[ignore]
fn cumsum_kernel_keeps_simd_on_aarch64() {
if !cfg!(target_arch = "aarch64") {
eprintln!("[asm-check] skipping: not aarch64");
return;
}
match assert_function_contains("Cumsum", &["fadd"]) {
Ok(()) => {}
Err(AsmCheckError::ToolMissing | AsmCheckError::FunctionNotFound { .. }) => {
eprintln!("[asm-check] skipping: tool or symbol missing");
}
Err(e) => panic!("Cumsum kernel asm check failed: {e}"),
}
}
}