regression.rs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. use std::collections::BTreeMap;
  2. use std::env;
  3. use std::fs;
  4. use std::path::PathBuf;
  5. use blake3;
  6. use serde::{Deserialize, Serialize};
  7. #[derive(Debug, Deserialize, Serialize)]
  8. struct Manifest {
  9. #[serde(default)]
  10. cases: BTreeMap<String, Case>,
  11. }
  12. #[derive(Debug, Deserialize, Serialize)]
  13. struct Case {
  14. file: String,
  15. #[serde(default)]
  16. hash: String,
  17. #[serde(default = "default_runs")]
  18. runs: u32,
  19. }
  20. #[derive(Debug)]
  21. enum IssueKind {
  22. HashMismatch,
  23. NonDeterministic,
  24. AssemblerError,
  25. }
  26. #[derive(Debug)]
  27. struct Issue {
  28. kind: IssueKind,
  29. name: String,
  30. file: String,
  31. expected: Option<String>,
  32. actual: Option<String>,
  33. note: Option<String>,
  34. }
  35. fn default_runs() -> u32 {
  36. 10
  37. }
  38. fn fixtures_dir() -> PathBuf {
  39. PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
  40. }
  41. fn read_manifest() -> Manifest {
  42. let manifest_path = fixtures_dir().join("index.toml");
  43. let content = fs::read_to_string(&manifest_path)
  44. .unwrap_or_else(|e| panic!("failed to read {}: {}", manifest_path.display(), e));
  45. toml::from_str(&content).expect("failed to parse fixtures/index.toml")
  46. }
  47. fn read_source(case_file: &str) -> String {
  48. let path = fixtures_dir().join(case_file);
  49. fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {}: {}", path.display(), e))
  50. }
  51. fn write_manifest(manifest: &Manifest) {
  52. let manifest_path = fixtures_dir().join("index.toml");
  53. let content = toml::to_string_pretty(manifest)
  54. .unwrap_or_else(|e| panic!("failed to serialize manifest: {:?}", e));
  55. fs::write(&manifest_path, content)
  56. .unwrap_or_else(|e| panic!("failed to write {}: {}", manifest_path.display(), e));
  57. }
  58. fn hash_bytes(bytes: &[u8]) -> String {
  59. blake3::hash(bytes).to_hex().to_string()
  60. }
  61. #[test]
  62. fn test_regression() {
  63. let mut manifest = read_manifest();
  64. assert!(
  65. !manifest.cases.is_empty(),
  66. "fixtures/index.toml must define at least one case"
  67. );
  68. let update_hashes = env::var("UPDATE_HASHES")
  69. .ok()
  70. .filter(|v| v == "1")
  71. .is_some();
  72. let mut any_missing_or_mismatch = false;
  73. let mut issues: Vec<Issue> = Vec::new();
  74. let mut updated_entries = 0usize;
  75. for (name, case) in manifest.cases.iter_mut() {
  76. let source = read_source(&case.file);
  77. let mut first_hash: Option<String> = None;
  78. let mut nondeterministic = false;
  79. let mut assembler_failed = false;
  80. for _ in 0..case.runs.max(1) {
  81. let bytes = match sbpf_assembler::assemble(&source) {
  82. Ok(b) => b,
  83. Err(e) => {
  84. assembler_failed = true;
  85. any_missing_or_mismatch = true;
  86. issues.push(Issue {
  87. kind: IssueKind::AssemblerError,
  88. name: name.clone(),
  89. file: case.file.clone(),
  90. expected: None,
  91. actual: None,
  92. note: Some(format!("assembler failed: {:?}", e)),
  93. });
  94. break;
  95. }
  96. };
  97. let h = hash_bytes(&bytes);
  98. if let Some(prev) = &first_hash {
  99. if &h != prev {
  100. nondeterministic = true;
  101. issues.push(Issue {
  102. kind: IssueKind::NonDeterministic,
  103. name: name.clone(),
  104. file: case.file.clone(),
  105. expected: Some(prev.clone()),
  106. actual: Some(h.clone()),
  107. note: Some("bytecode hash varied across runs".to_string()),
  108. });
  109. break;
  110. }
  111. } else {
  112. first_hash = Some(h);
  113. }
  114. }
  115. if assembler_failed {
  116. // Already recorded. Skip to next case.
  117. continue;
  118. }
  119. let actual = match first_hash {
  120. Some(h) => h,
  121. None => {
  122. any_missing_or_mismatch = true;
  123. issues.push(Issue {
  124. kind: IssueKind::AssemblerError,
  125. name: name.clone(),
  126. file: case.file.clone(),
  127. expected: None,
  128. actual: None,
  129. note: Some("no hash computed".to_string()),
  130. });
  131. continue;
  132. }
  133. };
  134. if nondeterministic {
  135. any_missing_or_mismatch = true;
  136. continue;
  137. }
  138. if actual != case.hash {
  139. if update_hashes {
  140. case.hash = actual.clone();
  141. updated_entries += 1;
  142. } else {
  143. any_missing_or_mismatch = true;
  144. issues.push(Issue {
  145. kind: IssueKind::HashMismatch,
  146. name: name.clone(),
  147. file: case.file.clone(),
  148. expected: Some(case.hash.clone()),
  149. actual: Some(actual.clone()),
  150. note: None,
  151. });
  152. }
  153. }
  154. }
  155. if update_hashes && updated_entries > 0 {
  156. // Update the manifest.
  157. write_manifest(&manifest);
  158. }
  159. if any_missing_or_mismatch {
  160. // Print report.
  161. let mut mismatched = 0usize;
  162. let mut nondet = 0usize;
  163. let mut asmerr = 0usize;
  164. eprintln!("\n===== Regression Report =====");
  165. for issue in &issues {
  166. match issue.kind {
  167. IssueKind::HashMismatch => {
  168. mismatched += 1;
  169. eprintln!(
  170. "[Mismatch] case='{}' file='{}' expected={} actual={}",
  171. issue.name,
  172. issue.file,
  173. issue.expected.as_deref().unwrap_or("<none>"),
  174. issue.actual.as_deref().unwrap_or("<none>")
  175. );
  176. }
  177. IssueKind::NonDeterministic => {
  178. nondet += 1;
  179. eprintln!(
  180. "[Non-deterministic] case='{}' file='{}' note={}",
  181. issue.name,
  182. issue.file,
  183. issue.note.as_deref().unwrap_or("")
  184. );
  185. }
  186. IssueKind::AssemblerError => {
  187. asmerr += 1;
  188. eprintln!(
  189. "[Assembler Error] case='{}' file='{}' note={}",
  190. issue.name,
  191. issue.file,
  192. issue.note.as_deref().unwrap_or("")
  193. );
  194. }
  195. }
  196. }
  197. eprintln!(
  198. "===== Summary: total={} mismatched={} non-deterministic={} assembler-error={} =====\n",
  199. issues.len(),
  200. mismatched,
  201. nondet,
  202. asmerr
  203. );
  204. // Fail the test.
  205. panic!("regressions detected ({}).", issues.len());
  206. }
  207. }