regression.rs 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. use {
  2. serde::{Deserialize, Serialize},
  3. std::{collections::BTreeMap, env, fs, path::PathBuf},
  4. };
  5. #[derive(Debug, Deserialize, Serialize)]
  6. struct Manifest {
  7. #[serde(default)]
  8. cases: BTreeMap<String, Case>,
  9. }
  10. #[derive(Debug, Deserialize, Serialize)]
  11. struct Case {
  12. file: String,
  13. #[serde(default)]
  14. hash: String,
  15. }
  16. #[derive(Debug)]
  17. enum IssueKind {
  18. HashMismatch,
  19. AssemblerError,
  20. }
  21. #[derive(Debug)]
  22. struct Issue {
  23. kind: IssueKind,
  24. name: String,
  25. file: String,
  26. expected: Option<String>,
  27. actual: Option<String>,
  28. note: Option<String>,
  29. }
  30. fn fixtures_dir() -> PathBuf {
  31. PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
  32. }
  33. fn read_manifest() -> Manifest {
  34. let manifest_path = fixtures_dir().join("index.toml");
  35. let content = fs::read_to_string(&manifest_path)
  36. .unwrap_or_else(|e| panic!("failed to read {}: {}", manifest_path.display(), e));
  37. toml::from_str(&content).expect("failed to parse fixtures/index.toml")
  38. }
  39. fn read_source(case_file: &str) -> String {
  40. let path = fixtures_dir().join(case_file);
  41. fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {}: {}", path.display(), e))
  42. }
  43. fn write_manifest(manifest: &Manifest) {
  44. let manifest_path = fixtures_dir().join("index.toml");
  45. let content = toml::to_string_pretty(manifest)
  46. .unwrap_or_else(|e| panic!("failed to serialize manifest: {:?}", e));
  47. fs::write(&manifest_path, content)
  48. .unwrap_or_else(|e| panic!("failed to write {}: {}", manifest_path.display(), e));
  49. }
  50. fn hash_bytes(bytes: &[u8]) -> String {
  51. blake3::hash(bytes).to_hex().to_string()
  52. }
  53. #[test]
  54. fn test_regression() {
  55. let mut manifest = read_manifest();
  56. assert!(
  57. !manifest.cases.is_empty(),
  58. "fixtures/index.toml must define at least one case"
  59. );
  60. let update_hashes = env::var("UPDATE_HASHES")
  61. .ok()
  62. .filter(|v| v == "1")
  63. .is_some();
  64. let mut any_missing_or_mismatch = false;
  65. let mut issues: Vec<Issue> = Vec::new();
  66. let mut updated_entries = 0usize;
  67. for (name, case) in manifest.cases.iter_mut() {
  68. let source = read_source(&case.file);
  69. let actual = match sbpf_assembler::assemble(&source) {
  70. Ok(bytes) => hash_bytes(&bytes),
  71. Err(e) => {
  72. any_missing_or_mismatch = true;
  73. issues.push(Issue {
  74. kind: IssueKind::AssemblerError,
  75. name: name.clone(),
  76. file: case.file.clone(),
  77. expected: None,
  78. actual: None,
  79. note: Some(format!("assembler failed: {:?}", e)),
  80. });
  81. continue;
  82. }
  83. };
  84. if actual != case.hash {
  85. if update_hashes && case.hash.is_empty() {
  86. // Only update the hash if it's empty.
  87. case.hash = actual.clone();
  88. updated_entries += 1;
  89. } else {
  90. any_missing_or_mismatch = true;
  91. issues.push(Issue {
  92. kind: IssueKind::HashMismatch,
  93. name: name.clone(),
  94. file: case.file.clone(),
  95. expected: Some(case.hash.clone()),
  96. actual: Some(actual.clone()),
  97. note: None,
  98. });
  99. }
  100. }
  101. }
  102. if update_hashes && updated_entries > 0 {
  103. // Update the manifest.
  104. write_manifest(&manifest);
  105. }
  106. if any_missing_or_mismatch {
  107. // Print report.
  108. let mut mismatched = 0usize;
  109. let mut asmerr = 0usize;
  110. eprintln!("\n===== Regression Report =====");
  111. for issue in &issues {
  112. match issue.kind {
  113. IssueKind::HashMismatch => {
  114. mismatched += 1;
  115. eprintln!(
  116. "[Mismatch] case='{}' file='{}' expected={} actual={}",
  117. issue.name,
  118. issue.file,
  119. issue.expected.as_deref().unwrap_or("<none>"),
  120. issue.actual.as_deref().unwrap_or("<none>")
  121. );
  122. }
  123. IssueKind::AssemblerError => {
  124. asmerr += 1;
  125. eprintln!(
  126. "[Assembler Error] case='{}' file='{}' note={}",
  127. issue.name,
  128. issue.file,
  129. issue.note.as_deref().unwrap_or("")
  130. );
  131. }
  132. }
  133. }
  134. eprintln!(
  135. "===== Summary: total={} mismatched={} assembler-error={} =====\n",
  136. issues.len(),
  137. mismatched,
  138. asmerr
  139. );
  140. // Fail the test.
  141. panic!("regressions detected ({}).", issues.len());
  142. }
  143. }