Rust · 8577 bytes Raw Blame History
1 use std::collections::BTreeSet;
2 use std::fs;
3 use std::path::{Path, PathBuf};
4 use std::process::Command;
5
6 use armfortas::driver::OptLevel;
7 use armfortas::testing::{capture_from_path, CaptureRequest, CapturedStage, Stage};
8
9 fn fixture(name: &str) -> PathBuf {
10 let path = PathBuf::from("test_programs").join(name);
11 assert!(path.exists(), "missing test fixture {}", path.display());
12 path
13 }
14
15 fn capture_text(request: CaptureRequest, stage: Stage) -> String {
16 let result = capture_from_path(&request).expect("capture should succeed");
17 match result.get(stage) {
18 Some(CapturedStage::Text(text)) => text.clone(),
19 Some(CapturedStage::Run(_)) => panic!("expected text stage for {}", stage.as_str()),
20 None => panic!("missing requested stage {}", stage.as_str()),
21 }
22 }
23
24 fn candidate_target_dirs() -> Vec<PathBuf> {
25 let mut dirs = Vec::new();
26 if let Ok(exe) = std::env::current_exe() {
27 for ancestor in exe.ancestors() {
28 let Some(name) = ancestor.file_name().and_then(|n| n.to_str()) else {
29 continue;
30 };
31 if matches!(name, "debug" | "release") {
32 dirs.push(ancestor.to_path_buf());
33 break;
34 }
35 }
36 }
37 for dir in ["target/release", "target/debug"] {
38 let candidate = PathBuf::from(dir);
39 if !dirs.iter().any(|existing| existing == &candidate) {
40 dirs.push(candidate);
41 }
42 }
43 dirs
44 }
45
46 fn find_compiler() -> PathBuf {
47 for dir in candidate_target_dirs() {
48 let path = dir.join("armfortas");
49 if path.exists() {
50 return path;
51 }
52 }
53 panic!("cannot find armfortas binary — run `cargo build` first");
54 }
55
56 fn compile_binary(compiler: &Path, source: &Path, opt_flag: &str, output: &Path) {
57 let status = Command::new(compiler)
58 .args([
59 source.to_str().unwrap(),
60 opt_flag,
61 "-o",
62 output.to_str().unwrap(),
63 ])
64 .status()
65 .expect("compiler launch failed");
66 assert!(
67 status.success(),
68 "binary compile failed for {} at {}",
69 source.display(),
70 opt_flag
71 );
72 }
73
74 fn tool_output(tool: &str, args: &[&str]) -> String {
75 let output = Command::new(tool)
76 .args(args)
77 .output()
78 .unwrap_or_else(|e| panic!("cannot run {}: {}", tool, e));
79 assert!(
80 output.status.success(),
81 "{} failed:\n{}",
82 tool,
83 String::from_utf8_lossy(&output.stderr)
84 );
85 String::from_utf8_lossy(&output.stdout).into_owned()
86 }
87
88 fn normalize_lc_uuid(mut bytes: Vec<u8>) -> Vec<u8> {
89 const MH_MAGIC_64: u32 = 0xfeedfacf;
90 const LC_UUID: u32 = 0x1b;
91 const MACH_HEADER_64_SIZE: usize = 32;
92
93 if bytes.len() < MACH_HEADER_64_SIZE {
94 return bytes;
95 }
96 let magic = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
97 if magic != MH_MAGIC_64 {
98 return bytes;
99 }
100 let ncmds = u32::from_le_bytes(bytes[16..20].try_into().unwrap()) as usize;
101 let mut offset = MACH_HEADER_64_SIZE;
102 for _ in 0..ncmds {
103 if offset + 8 > bytes.len() {
104 break;
105 }
106 let cmd = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap());
107 let cmdsize = u32::from_le_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
108 let cmdsize = cmdsize as usize;
109 if cmdsize < 8 || offset + cmdsize > bytes.len() {
110 break;
111 }
112 if cmd == LC_UUID && cmdsize >= 24 {
113 bytes[offset + 8..offset + 24].fill(0);
114 }
115 offset += cmdsize;
116 }
117 bytes
118 }
119
120 #[test]
121 fn realworld_object_snapshots_stay_deterministic_at_o2() {
122 for name in [
123 "realworld_tridiag_spmv.f90",
124 "realworld_axpy_reduce.f90",
125 "realworld_sasum_cleanup.f90",
126 "realworld_three_point_apply.f90",
127 "realworld_binomial_blend.f90",
128 "realworld_shape_guard.f90",
129 "realworld_affine_shift.f90",
130 "realworld_noalias_reuse.f90",
131 "realworld_join_bias_sum.f90",
132 "realworld_seed_overwrite.f90",
133 "realworld_inplace_prefix.f90",
134 "realworld_inplace_symmix.f90",
135 "realworld_elemental_stage.f90",
136 "realworld_ipo_chain.f90",
137 "realworld_doconc_square.f90",
138 "realworld_vector_stage.f90",
139 ] {
140 let source = fixture(name);
141 let first = capture_text(
142 CaptureRequest {
143 input: source.clone(),
144 requested: BTreeSet::from([Stage::Obj]),
145 opt_level: OptLevel::O2,
146 },
147 Stage::Obj,
148 );
149 let second = capture_text(
150 CaptureRequest {
151 input: source.clone(),
152 requested: BTreeSet::from([Stage::Obj]),
153 opt_level: OptLevel::O2,
154 },
155 Stage::Obj,
156 );
157 assert_eq!(
158 first, second,
159 "real-world object snapshot should be deterministic at O2 for {}",
160 name
161 );
162 }
163 }
164
165 #[test]
166 fn realworld_opt_ir_differs_from_raw_ir_at_o2() {
167 for name in [
168 "realworld_tridiag_spmv.f90",
169 "realworld_axpy_reduce.f90",
170 "realworld_sasum_cleanup.f90",
171 "realworld_three_point_apply.f90",
172 "realworld_binomial_blend.f90",
173 "realworld_shape_guard.f90",
174 "realworld_affine_shift.f90",
175 "realworld_noalias_reuse.f90",
176 "realworld_join_bias_sum.f90",
177 "realworld_seed_overwrite.f90",
178 "realworld_inplace_prefix.f90",
179 "realworld_inplace_symmix.f90",
180 "realworld_elemental_stage.f90",
181 "realworld_ipo_chain.f90",
182 "realworld_doconc_square.f90",
183 "realworld_vector_stage.f90",
184 ] {
185 let source = fixture(name);
186 let raw_ir = capture_text(
187 CaptureRequest {
188 input: source.clone(),
189 requested: BTreeSet::from([Stage::Ir]),
190 opt_level: OptLevel::O0,
191 },
192 Stage::Ir,
193 );
194 let opt_ir = capture_text(
195 CaptureRequest {
196 input: source,
197 requested: BTreeSet::from([Stage::OptIr]),
198 opt_level: OptLevel::O2,
199 },
200 Stage::OptIr,
201 );
202 assert_ne!(
203 raw_ir, opt_ir,
204 "O2 optimized IR should materially differ from raw IR for {}",
205 name
206 );
207 }
208 }
209
210 #[test]
211 fn linked_realworld_binaries_are_deterministic_modulo_uuid() {
212 let compiler = find_compiler();
213
214 for name in [
215 "realworld_tridiag_spmv.f90",
216 "realworld_axpy_reduce.f90",
217 "realworld_sasum_cleanup.f90",
218 "realworld_three_point_apply.f90",
219 "realworld_binomial_blend.f90",
220 "realworld_shape_guard.f90",
221 "realworld_affine_shift.f90",
222 "realworld_noalias_reuse.f90",
223 "realworld_join_bias_sum.f90",
224 "realworld_seed_overwrite.f90",
225 "realworld_inplace_prefix.f90",
226 "realworld_inplace_symmix.f90",
227 "realworld_elemental_stage.f90",
228 "realworld_ipo_chain.f90",
229 "realworld_doconc_square.f90",
230 "realworld_vector_stage.f90",
231 ] {
232 let source = fixture(name);
233 let stem = source.file_stem().unwrap().to_str().unwrap();
234 for opt in ["-O0", "-O2", "-O3"] {
235 let bin_path = std::env::temp_dir().join(format!(
236 "afs_realworld_{}_{}_{}",
237 std::process::id(),
238 stem,
239 opt.trim_start_matches('-')
240 ));
241
242 compile_binary(&compiler, &source, opt, &bin_path);
243 let load_commands = tool_output("otool", &["-l", bin_path.to_str().unwrap()]);
244 assert!(
245 load_commands.contains("LC_UUID"),
246 "linked binary at {} should carry LC_UUID for {}:\n{}",
247 opt,
248 name,
249 load_commands
250 );
251 let first =
252 normalize_lc_uuid(fs::read(&bin_path).expect("cannot read first binary image"));
253
254 compile_binary(&compiler, &source, opt, &bin_path);
255 let second =
256 normalize_lc_uuid(fs::read(&bin_path).expect("cannot read second binary image"));
257
258 assert_eq!(
259 first, second,
260 "real-world linked binary should stay deterministic modulo LC_UUID when rebuilt at the same output path ({} {})",
261 name,
262 opt
263 );
264
265 let _ = fs::remove_file(&bin_path);
266 }
267 }
268 }
269