use {
serde::{Deserialize, Serialize},
std::{collections::BTreeMap, env, fs, path::PathBuf},
};
#[derive(Debug, Deserialize, Serialize)]
struct Manifest {
#[serde(default)]
cases: BTreeMap<String, Case>,
}
#[derive(Debug, Deserialize, Serialize)]
struct Case {
file: String,
#[serde(default)]
hash: String,
#[serde(default)]
debug_hash: String,
#[serde(default)]
hash_v0: String,
#[serde(default)]
debug_hash_v0: String,
}
#[derive(Debug)]
enum IssueKind {
HashMismatch,
DebugHashMismatch,
AssemblerError,
}
#[derive(Debug)]
struct Issue {
kind: IssueKind,
name: String,
file: String,
arch: sbpf_assembler::SbpfArch,
expected: Option<String>,
actual: Option<String>,
note: Option<String>,
}
fn fixtures_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
}
fn read_manifest() -> Manifest {
let manifest_path = fixtures_dir().join("index.toml");
let content = fs::read_to_string(&manifest_path)
.unwrap_or_else(|e| panic!("failed to read {}: {}", manifest_path.display(), e));
toml::from_str(&content).expect("failed to parse fixtures/index.toml")
}
fn read_source(case_file: &str) -> String {
let path = fixtures_dir().join(case_file);
fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {}: {}", path.display(), e))
}
fn write_manifest(manifest: &Manifest) {
let manifest_path = fixtures_dir().join("index.toml");
let content = toml::to_string_pretty(manifest)
.unwrap_or_else(|e| panic!("failed to serialize manifest: {:?}", e));
fs::write(&manifest_path, content)
.unwrap_or_else(|e| panic!("failed to write {}: {}", manifest_path.display(), e));
}
fn hash_bytes(bytes: &[u8]) -> String {
blake3::hash(bytes).to_hex().to_string()
}
fn expected_hash_mut(case: &mut Case, arch: sbpf_assembler::SbpfArch) -> &mut String {
match arch {
sbpf_assembler::SbpfArch::V0 => &mut case.hash_v0,
sbpf_assembler::SbpfArch::V3 => &mut case.hash,
}
}
fn expected_debug_hash_mut(case: &mut Case, arch: sbpf_assembler::SbpfArch) -> &mut String {
match arch {
sbpf_assembler::SbpfArch::V0 => &mut case.debug_hash_v0,
sbpf_assembler::SbpfArch::V3 => &mut case.debug_hash,
}
}
#[test]
fn test_regression() {
let mut manifest = read_manifest();
assert!(
!manifest.cases.is_empty(),
"fixtures/index.toml must define at least one case"
);
let update_hashes = env::var("UPDATE_HASHES")
.ok()
.filter(|v| v == "1")
.is_some();
let mut any_missing_or_mismatch = false;
let mut issues: Vec<Issue> = Vec::new();
let mut updated_entries = 0usize;
for (name, case) in manifest.cases.iter_mut() {
let case_file = case.file.clone();
let source = read_source(&case_file);
for arch in [sbpf_assembler::SbpfArch::V0, sbpf_assembler::SbpfArch::V3] {
let is_v0_jump32_case =
matches!(arch, sbpf_assembler::SbpfArch::V0) && case_file.starts_with("jump32_");
let assembler = sbpf_assembler::Assembler::new(
sbpf_assembler::AssemblerOption::default().with_arch(arch),
);
let assemble_result = assembler.assemble(&source);
if is_v0_jump32_case {
assert!(
assemble_result.is_err(),
"expected fixture '{}' to fail assembly",
case_file
);
} else {
let actual = match assemble_result {
Ok(bytes) => hash_bytes(&bytes),
Err(e) => {
any_missing_or_mismatch = true;
issues.push(Issue {
kind: IssueKind::AssemblerError,
name: name.clone(),
file: case_file.clone(),
arch,
expected: None,
actual: None,
note: Some(format!("assembler failed: {:?}", e)),
});
continue;
}
};
let expected = expected_hash_mut(case, arch);
if actual != *expected {
if update_hashes && expected.is_empty() {
*expected = actual.clone();
updated_entries += 1;
} else {
any_missing_or_mismatch = true;
issues.push(Issue {
kind: IssueKind::HashMismatch,
name: name.clone(),
file: case_file.clone(),
arch,
expected: Some(expected.clone()),
actual: Some(actual.clone()),
note: None,
});
}
}
}
let debug_assembler = sbpf_assembler::Assembler::new(
sbpf_assembler::AssemblerOption::default()
.with_arch(arch)
.with_debug_mode(sbpf_assembler::DebugMode {
filename: case_file.clone(),
directory: "/test".to_string(),
}),
);
let debug_result = debug_assembler.assemble(&source);
if is_v0_jump32_case {
assert!(
debug_result.is_err(),
"expected fixture '{}' to fail debug assembly",
case_file
);
} else {
let debug_actual = match debug_result {
Ok(bytes) => hash_bytes(&bytes),
Err(e) => {
any_missing_or_mismatch = true;
issues.push(Issue {
kind: IssueKind::AssemblerError,
name: name.clone(),
file: case_file.clone(),
arch,
expected: None,
actual: None,
note: Some(format!("debug assembler failed: {:?}", e)),
});
continue;
}
};
let expected_debug = expected_debug_hash_mut(case, arch);
if debug_actual != *expected_debug {
if update_hashes && expected_debug.is_empty() {
*expected_debug = debug_actual.clone();
updated_entries += 1;
} else {
any_missing_or_mismatch = true;
issues.push(Issue {
kind: IssueKind::DebugHashMismatch,
name: name.clone(),
file: case_file.clone(),
arch,
expected: Some(expected_debug.clone()),
actual: Some(debug_actual.clone()),
note: None,
});
}
}
}
}
}
if update_hashes && updated_entries > 0 {
write_manifest(&manifest);
}
if any_missing_or_mismatch {
let mut mismatched = 0usize;
let mut debug_mismatched = 0usize;
let mut asmerr = 0usize;
eprintln!("\n===== Regression Report =====");
for issue in &issues {
match issue.kind {
IssueKind::HashMismatch => {
mismatched += 1;
eprintln!(
"[Mismatch] arch='{:?}' case='{}' file='{}' expected={} actual={}",
issue.arch,
issue.name,
issue.file,
issue.expected.as_deref().unwrap_or("<none>"),
issue.actual.as_deref().unwrap_or("<none>")
);
}
IssueKind::DebugHashMismatch => {
debug_mismatched += 1;
eprintln!(
"[Debug Mismatch] arch='{:?}' case='{}' file='{}' expected={} actual={}",
issue.arch,
issue.name,
issue.file,
issue.expected.as_deref().unwrap_or("<none>"),
issue.actual.as_deref().unwrap_or("<none>")
);
}
IssueKind::AssemblerError => {
asmerr += 1;
eprintln!(
"[Assembler Error] arch='{:?}' case='{}' file='{}' note={}",
issue.arch,
issue.name,
issue.file,
issue.note.as_deref().unwrap_or("")
);
}
}
}
eprintln!(
"===== Summary: total={} mismatched={} debug_mismatched={} assembler-error={} =====\n",
issues.len(),
mismatched,
debug_mismatched,
asmerr
);
panic!("regressions detected ({}).", issues.len());
}
}