| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- use std::collections::BTreeMap;
- use std::env;
- use std::fs;
- use std::path::PathBuf;
- use blake3;
- use serde::{Deserialize, Serialize};
- #[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 = "default_runs")]
- runs: u32,
- }
- #[derive(Debug)]
- enum IssueKind {
- HashMismatch,
- NonDeterministic,
- AssemblerError,
- }
- #[derive(Debug)]
- struct Issue {
- kind: IssueKind,
- name: String,
- file: String,
- expected: Option<String>,
- actual: Option<String>,
- note: Option<String>,
- }
- fn default_runs() -> u32 {
- 10
- }
- 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()
- }
- #[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 source = read_source(&case.file);
- let mut first_hash: Option<String> = None;
- let mut nondeterministic = false;
- let mut assembler_failed = false;
- for _ in 0..case.runs.max(1) {
- let bytes = match sbpf_assembler::assemble(&source) {
- Ok(b) => b,
- Err(e) => {
- assembler_failed = true;
- any_missing_or_mismatch = true;
- issues.push(Issue {
- kind: IssueKind::AssemblerError,
- name: name.clone(),
- file: case.file.clone(),
- expected: None,
- actual: None,
- note: Some(format!("assembler failed: {:?}", e)),
- });
- break;
- }
- };
- let h = hash_bytes(&bytes);
- if let Some(prev) = &first_hash {
- if &h != prev {
- nondeterministic = true;
- issues.push(Issue {
- kind: IssueKind::NonDeterministic,
- name: name.clone(),
- file: case.file.clone(),
- expected: Some(prev.clone()),
- actual: Some(h.clone()),
- note: Some("bytecode hash varied across runs".to_string()),
- });
- break;
- }
- } else {
- first_hash = Some(h);
- }
- }
- if assembler_failed {
- // Already recorded. Skip to next case.
- continue;
- }
- let actual = match first_hash {
- Some(h) => h,
- None => {
- any_missing_or_mismatch = true;
- issues.push(Issue {
- kind: IssueKind::AssemblerError,
- name: name.clone(),
- file: case.file.clone(),
- expected: None,
- actual: None,
- note: Some("no hash computed".to_string()),
- });
- continue;
- }
- };
- if nondeterministic {
- any_missing_or_mismatch = true;
- continue;
- }
- if actual != case.hash {
- if update_hashes {
- case.hash = actual.clone();
- updated_entries += 1;
- } else {
- any_missing_or_mismatch = true;
- issues.push(Issue {
- kind: IssueKind::HashMismatch,
- name: name.clone(),
- file: case.file.clone(),
- expected: Some(case.hash.clone()),
- actual: Some(actual.clone()),
- note: None,
- });
- }
- }
- }
- if update_hashes && updated_entries > 0 {
- // Update the manifest.
- write_manifest(&manifest);
- }
- if any_missing_or_mismatch {
- // Print report.
- let mut mismatched = 0usize;
- let mut nondet = 0usize;
- let mut asmerr = 0usize;
- eprintln!("\n===== Regression Report =====");
- for issue in &issues {
- match issue.kind {
- IssueKind::HashMismatch => {
- mismatched += 1;
- eprintln!(
- "[Mismatch] case='{}' file='{}' expected={} actual={}",
- issue.name,
- issue.file,
- issue.expected.as_deref().unwrap_or("<none>"),
- issue.actual.as_deref().unwrap_or("<none>")
- );
- }
- IssueKind::NonDeterministic => {
- nondet += 1;
- eprintln!(
- "[Non-deterministic] case='{}' file='{}' note={}",
- issue.name,
- issue.file,
- issue.note.as_deref().unwrap_or("")
- );
- }
- IssueKind::AssemblerError => {
- asmerr += 1;
- eprintln!(
- "[Assembler Error] case='{}' file='{}' note={}",
- issue.name,
- issue.file,
- issue.note.as_deref().unwrap_or("")
- );
- }
- }
- }
- eprintln!(
- "===== Summary: total={} mismatched={} non-deterministic={} assembler-error={} =====\n",
- issues.len(),
- mismatched,
- nondet,
- asmerr
- );
- // Fail the test.
- panic!("regressions detected ({}).", issues.len());
- }
- }
|