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