Rust · 8982 bytes Raw Blame History
1 //! fortsh module graph smoke test.
2 //!
3 //! Scans all 55 fortsh source files for MODULE/USE statements,
4 //! builds the dependency graph using armfortas's dep_scan module,
5 //! resolves topological compilation order, and attempts to compile
6 //! each file to .o in order.
7 //!
8 //! This is the ultimate integration test for the module system —
9 //! if it works on a real 55-module project, the module system
10 //! can handle real-world dependency chains.
11 //!
12 //! NOTE: This test requires the fortsh repo to be checked out at
13 //! ~/Documents/GithubOrgs/FortranGoingOnForty/fortsh. If absent,
14 //! the test is silently skipped.
15
16 use std::fs;
17 use std::path::{Path, PathBuf};
18 use std::process::Command;
19
20 const FORTSH_COMPILED_FLOOR: usize = 14;
21
22 fn find_compiler() -> PathBuf {
23 if let Some(path) = std::env::var_os("CARGO_BIN_EXE_armfortas") {
24 return PathBuf::from(path);
25 }
26 for c in &["target/debug/armfortas", "target/release/armfortas"] {
27 let p = PathBuf::from(c);
28 if p.exists() {
29 return fs::canonicalize(&p).unwrap();
30 }
31 }
32 panic!("armfortas binary not found");
33 }
34
35 fn find_fortsh() -> Option<PathBuf> {
36 let home = std::env::var("HOME").ok()?;
37 let candidates = [
38 PathBuf::from(&home).join("Documents/GithubOrgs/FortranGoingOnForty/fortsh/src"),
39 PathBuf::from("../fortsh/src"),
40 ];
41 for c in &candidates {
42 if c.is_dir() {
43 return fs::canonicalize(c).ok();
44 }
45 }
46 None
47 }
48
49 /// Simple line-by-line MODULE/USE scanner (same logic as dep_scan).
50 fn scan_file(path: &Path) -> (Vec<String>, Vec<String>) {
51 let content = fs::read_to_string(path).unwrap_or_default();
52 let mut defines = Vec::new();
53 let mut uses = Vec::new();
54
55 for line in content.lines() {
56 let trimmed = line.trim().to_lowercase();
57 if trimmed.starts_with('!') || trimmed.is_empty() {
58 continue;
59 }
60
61 if trimmed.starts_with("module ") {
62 let rest = trimmed[7..].trim();
63 if rest.starts_with("procedure")
64 || rest.starts_with("function")
65 || rest.starts_with("subroutine")
66 {
67 continue;
68 }
69 if let Some(name) = rest.split_whitespace().next() {
70 let clean = name.trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_');
71 if !clean.is_empty() {
72 defines.push(clean.to_string());
73 }
74 }
75 }
76
77 if trimmed.starts_with("use ") || trimmed.starts_with("use,") {
78 let rest = if trimmed.starts_with("use,") {
79 if let Some(idx) = trimmed.find("::") {
80 trimmed[idx + 2..].trim()
81 } else {
82 &trimmed[4..]
83 }
84 } else {
85 trimmed[4..].trim()
86 };
87 if let Some(name) = rest
88 .split(|c: char| c == ',' || c == ':' || c.is_whitespace())
89 .next()
90 {
91 let clean = name.trim();
92 if !clean.is_empty() && clean != "only" {
93 uses.push(clean.to_string());
94 }
95 }
96 }
97 }
98
99 defines.sort();
100 defines.dedup();
101 uses.sort();
102 uses.dedup();
103 (defines, uses)
104 }
105
106 fn is_hard_failure(stderr: &str) -> bool {
107 stderr.contains("INTERNAL COMPILER ERROR")
108 || stderr.contains("internal error:")
109 || stderr.contains("assembler failed:")
110 || stderr.contains("coerce_to_type:")
111 }
112
113 #[test]
114 fn fortsh_module_graph_resolves() {
115 let fortsh_src = match find_fortsh() {
116 Some(p) => p,
117 None => {
118 eprintln!("SKIP: fortsh source not found");
119 return;
120 }
121 };
122
123 // Collect all .f90 files.
124 let mut sources: Vec<PathBuf> = Vec::new();
125 fn collect_f90(dir: &Path, out: &mut Vec<PathBuf>) {
126 for entry in fs::read_dir(dir).unwrap() {
127 let entry = entry.unwrap();
128 let path = entry.path();
129 if path.is_dir() {
130 collect_f90(&path, out);
131 } else if path.extension().map(|e| e == "f90").unwrap_or(false) {
132 out.push(path);
133 }
134 }
135 }
136 collect_f90(&fortsh_src, &mut sources);
137 sources.sort();
138
139 eprintln!("Found {} fortsh source files", sources.len());
140 assert!(
141 sources.len() >= 50,
142 "expected ~55 fortsh files, found {}",
143 sources.len()
144 );
145
146 // Scan each for module/use.
147 let mut module_to_file: std::collections::HashMap<String, usize> =
148 std::collections::HashMap::new();
149 let mut file_info: Vec<(PathBuf, Vec<String>, Vec<String>)> = Vec::new();
150
151 for (i, src) in sources.iter().enumerate() {
152 let (defines, uses) = scan_file(src);
153 for def in &defines {
154 module_to_file.insert(def.clone(), i);
155 }
156 file_info.push((src.clone(), defines, uses));
157 }
158
159 eprintln!("Module count: {}", module_to_file.len());
160
161 // Build adjacency and in-degree for topo sort.
162 let n = sources.len();
163 let mut in_degree = vec![0usize; n];
164 let mut dependents: Vec<Vec<usize>> = vec![vec![]; n];
165
166 let intrinsic = [
167 "iso_c_binding",
168 "iso_fortran_env",
169 "ieee_arithmetic",
170 "ieee_exceptions",
171 "ieee_features",
172 ];
173 for (i, (_path, _defs, uses)) in file_info.iter().enumerate() {
174 for used in uses {
175 if intrinsic.contains(&used.as_str()) {
176 continue;
177 }
178 if let Some(&j) = module_to_file.get(used.as_str()) {
179 if i != j {
180 dependents[j].push(i);
181 in_degree[i] += 1;
182 }
183 }
184 // External modules not in the set are OK (e.g., C interop modules
185 // might USE system modules). We just skip them.
186 }
187 }
188
189 // Kahn's topo sort.
190 let mut queue: std::collections::VecDeque<usize> = std::collections::VecDeque::new();
191 for i in 0..n {
192 if in_degree[i] == 0 {
193 queue.push_back(i);
194 }
195 }
196 let mut order = Vec::new();
197 while let Some(j) = queue.pop_front() {
198 order.push(j);
199 for &dep in &dependents[j] {
200 in_degree[dep] -= 1;
201 if in_degree[dep] == 0 {
202 queue.push_back(dep);
203 }
204 }
205 }
206
207 assert_eq!(
208 order.len(),
209 n,
210 "circular dependency in fortsh module graph — {} files unresolved",
211 n - order.len()
212 );
213 eprintln!("Topological order resolved ({} files)", order.len());
214
215 // Attempt to compile each in order.
216 let compiler = find_compiler();
217 let build_dir = std::env::temp_dir().join(format!("afs_fortsh_graph_{}", std::process::id()));
218 fs::create_dir_all(&build_dir).unwrap();
219
220 let mut compiled = 0;
221 let mut failed = Vec::new();
222 let mut hard_failures = Vec::new();
223
224 for &idx in &order {
225 let src = &file_info[idx].0;
226 let stem = src.file_stem().unwrap().to_str().unwrap();
227 let obj = build_dir.join(format!("{}.o", stem));
228
229 let result = Command::new(&compiler)
230 .current_dir(&build_dir)
231 .args([
232 src.to_str().unwrap(),
233 "-c",
234 "-O0",
235 "-DUSE_C_STRINGS",
236 "-o",
237 obj.to_str().unwrap(),
238 &format!("-J{}", build_dir.display()),
239 &format!("-I{}", build_dir.display()),
240 ])
241 .output()
242 .expect("compiler launch failed");
243
244 if result.status.success() {
245 compiled += 1;
246 } else {
247 let stderr = String::from_utf8_lossy(&result.stderr);
248 let summary = format!(
249 "{}: {}",
250 stem,
251 stderr.lines().next().unwrap_or("unknown error")
252 );
253 if is_hard_failure(&stderr) {
254 hard_failures.push(summary.clone());
255 }
256 failed.push(summary);
257 }
258 }
259
260 let _ = fs::remove_dir_all(&build_dir);
261
262 eprintln!(
263 "fortsh module graph: {}/{} compiled, {} failed",
264 compiled,
265 n,
266 failed.len()
267 );
268
269 if !failed.is_empty() {
270 eprintln!("Failures:");
271 for f in &failed {
272 eprintln!(" {}", f);
273 }
274 }
275
276 if !hard_failures.is_empty() {
277 panic!(
278 "fortsh compile should not ICE or assembler-crash; hard failures: {}",
279 hard_failures.join(" | ")
280 );
281 }
282
283 assert!(
284 compiled >= FORTSH_COMPILED_FLOOR,
285 "fortsh compiled {}/{} which regressed below the floor of {}",
286 compiled,
287 n,
288 FORTSH_COMPILED_FLOOR
289 );
290
291 // Report counts for human tracking.
292 eprintln!(
293 "\nfortsh scorecard: {}/{} compiled ({:.0}%)",
294 compiled,
295 n,
296 compiled as f64 / n as f64 * 100.0,
297 );
298 }
299