Rust · 4548 bytes Raw Blame History
1 //! Determinism sweep — compile every test program twice at -O2 and
2 //! verify byte-identical assembly output. Catches non-deterministic
3 //! codegen (HashMap iteration order, unstable sorts, stale regalloc
4 //! state) across the entire test corpus, not just the subset with
5 //! explicit REPRO_CHECK annotations.
6 //!
7 //! Multi-file tests (those containing `!--- file:` markers) and
8 //! error-expected tests are skipped — they either need special
9 //! handling or don't produce assembly.
10
11 use std::fs;
12 use std::path::{Path, PathBuf};
13 use std::process::Command;
14 use std::sync::atomic::{AtomicUsize, Ordering};
15
16 static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
17
18 fn find_compiler() -> PathBuf {
19 for c in &["target/release/armfortas", "target/debug/armfortas"] {
20 let p = PathBuf::from(c);
21 if p.exists() {
22 return fs::canonicalize(&p).unwrap();
23 }
24 }
25 panic!("armfortas binary not found — run `cargo build` first");
26 }
27
28 fn find_test_programs() -> PathBuf {
29 for c in &["test_programs", "../test_programs"] {
30 let p = PathBuf::from(c);
31 if p.is_dir() {
32 return p;
33 }
34 }
35 panic!("cannot find test_programs/ directory");
36 }
37
38 fn unique_path(prefix: &str, ext: &str) -> PathBuf {
39 let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
40 std::env::temp_dir().join(format!(
41 "afs_det_{}_{}_{}{}",
42 prefix,
43 std::process::id(),
44 id,
45 ext
46 ))
47 }
48
49 /// Compile a source to assembly, returning the bytes.
50 /// Returns None if compilation fails (e.g., error-expected tests).
51 fn compile_to_asm(compiler: &Path, source: &Path, opt: &str) -> Option<Vec<u8>> {
52 let out = unique_path("sweep", ".s");
53 let result = Command::new(compiler)
54 .args([
55 source.to_str().unwrap(),
56 opt,
57 "-S",
58 "-o",
59 out.to_str().unwrap(),
60 ])
61 .output()
62 .expect("compiler launch failed");
63 if !result.status.success() {
64 let _ = fs::remove_file(&out);
65 return None;
66 }
67 let bytes = fs::read(&out).expect("cannot read .s output");
68 let _ = fs::remove_file(&out);
69 Some(bytes)
70 }
71
72 fn should_skip(source_text: &str) -> bool {
73 // Skip multi-file tests.
74 if source_text.contains("!--- file:") {
75 return true;
76 }
77 // Skip error-expected tests (they're supposed to fail compilation).
78 if source_text.contains("! ERROR_EXPECTED:") {
79 return true;
80 }
81 // Skip XFAIL tests (they have known failures).
82 if source_text.contains("! XFAIL:") {
83 return true;
84 }
85 false
86 }
87
88 #[test]
89 fn all_programs_deterministic_at_o2() {
90 let compiler = find_compiler();
91 let test_dir = find_test_programs();
92
93 let mut sources: Vec<PathBuf> = fs::read_dir(&test_dir)
94 .expect("cannot read test_programs/")
95 .filter_map(|e| e.ok())
96 .map(|e| e.path())
97 .filter(|p| p.extension().map(|e| e == "f90").unwrap_or(false))
98 .collect();
99 sources.sort();
100
101 let mut checked = 0;
102 let mut skipped = 0;
103 let mut failures = Vec::new();
104
105 for source in &sources {
106 let name = source.file_name().unwrap().to_str().unwrap();
107 let text = fs::read_to_string(source).unwrap();
108 if should_skip(&text) {
109 skipped += 1;
110 continue;
111 }
112
113 let first = match compile_to_asm(&compiler, source, "-O2") {
114 Some(bytes) => bytes,
115 None => {
116 // Compilation failed — this test has other issues; skip.
117 skipped += 1;
118 continue;
119 }
120 };
121 let second = compile_to_asm(&compiler, source, "-O2")
122 .expect("second compile failed but first succeeded — flaky");
123
124 if first != second {
125 failures.push(format!(
126 "{}: assembly differs between two -O2 compilations ({} vs {} bytes)",
127 name,
128 first.len(),
129 second.len(),
130 ));
131 } else {
132 checked += 1;
133 }
134 }
135
136 eprintln!(
137 "\nDeterminism sweep: {} checked, {} skipped, {} failures out of {} programs",
138 checked,
139 skipped,
140 failures.len(),
141 sources.len(),
142 );
143
144 if !failures.is_empty() {
145 panic!("Determinism failures:\n{}", failures.join("\n"),);
146 }
147
148 // Sanity: we should have checked a substantial portion.
149 assert!(
150 checked >= 150,
151 "only {} programs checked — expected at least 150",
152 checked,
153 );
154 }
155