//! Compile the program the product actually emits. //! //! Every other codegen test in this repo compiles a *fragment*: it takes one widget's //! `out.process`, splices it into a hand-written `fn main`, binds the inputs itself and //! supplies its own imports. That harness silently repairs the four things the real //! assembler can get wrong — main's signature, unbound inputs, cross-widget name //! collisions, and glob-import conflicts between pillars — so a fragment test structurally //! cannot catch an assembler bug. //! //! It did not catch one, either. `primitives_to_rust` hard-coded `fn main() {` while //! `data.*` and `stats.*` emit dozens of `?` operators into it, and all 37 of those widgets //! shipped programs that failed with E0277 before running. This file exists so that class //! of bug fails here first. //! //! # What it can and cannot cover //! //! A graph is only checked if every crate its program asks for is resolvable from the //! local registry. `image`, `imageproc` and `tract-onnx` are; **`polars` is not** — its //! transitive `chrono` requirement cannot be satisfied offline, which rules out `data.*` //! and `stats.*`, the very pillars the bug above lived in. Those are guarded instead by //! `no_widget_emits_a_try_operator_into_a_unit_returning_main` in `codegen::rust`, which //! proves the wrapper for every registered widget without compiling anything. //! //! Skips are printed, never silent. A test that quietly checks nothing is worse than no //! test, because it reads as coverage. use flowrs_core::graph::{Graph, WireMode}; /// Versions for the crates a generated program may ask for. /// /// The emitted `// Cargo.toml needs:` line deliberately omits versions — codegen will not /// invent one it cannot stand behind. The harness supplies them, and a crate missing from /// this table is a skip with a named reason rather than a guess. const DEPS: &[(&str, &str)] = &[ ("image", "0.25"), ("imageproc", "0.27"), ("tract_onnx", "0.23"), ("rustfft", "6"), ]; struct Golden { name: &'static str, build: fn() -> Graph, } fn image_pipeline() -> Graph { let mut g = Graph::new(); let load = g.add_node("image.load", 0.0, 0.0); let gray = g.add_node("image.grayscale", 120.0, 0.0); let blur = g.add_node("image.blur", 240.0, 0.0); g.add_wire(load, "image", gray, "image", WireMode::Move) .unwrap(); g.add_wire(gray, "gray", blur, "image", WireMode::Move) .unwrap(); g } fn vision_pipeline() -> Graph { let mut g = Graph::new(); let load = g.add_node("image.load", 0.0, 0.0); let corners = g.add_node("vision.corners", 140.0, 0.0); g.add_wire(load, "image", corners, "image", WireMode::Move) .unwrap(); g } /// Two pillars in ONE program. This is the case a per-widget test can never reach: each /// pillar's glob import is fine alone, and they may still collide when the assembler puts /// them in the same file. fn mixed_image_and_vision() -> Graph { let mut g = Graph::new(); let load = g.add_node("image.load", 0.0, 0.0); let gray = g.add_node("image.grayscale", 120.0, 0.0); let corners = g.add_node("vision.corners", 240.0, 0.0); g.add_wire(load, "image", gray, "image", WireMode::Move) .unwrap(); g.add_wire(gray, "gray", corners, "image", WireMode::Move) .unwrap(); g } /// The primitives implemented in this commit: boolean logic, strings, Option and Print. /// Pure std, so it needs no dependency and compiles in a blink — which is the point: the /// nodes a beginner reaches for first were the ones emitting `// TODO: unsupported`. fn logic_and_strings() -> Graph { let mut g = Graph::new(); let a = g.add_node("BoolLiteral", 0.0, 0.0); let b = g.add_node("BoolLiteral", 0.0, 80.0); let and = g.add_node("And", 120.0, 40.0); let not = g.add_node("Not", 240.0, 40.0); let s = g.add_node("StringNew", 0.0, 200.0); let len = g.add_node("StringLen", 120.0, 200.0); let print = g.add_node("Print", 360.0, 40.0); g.add_wire(a, "value", and, "a", WireMode::Move).unwrap(); g.add_wire(b, "value", and, "b", WireMode::Move).unwrap(); g.add_wire(and, "result", not, "input", WireMode::Move) .unwrap(); g.add_wire(not, "result", print, "input", WireMode::Move) .unwrap(); g.add_wire(s, "string", len, "string", WireMode::Move) .unwrap(); g } const GOLDENS: &[Golden] = &[ Golden { name: "logic_and_strings", build: logic_and_strings, }, Golden { name: "image_pipeline", build: image_pipeline, }, Golden { name: "vision_pipeline", build: vision_pipeline, }, Golden { name: "mixed_image_and_vision", build: mixed_image_and_vision, }, ]; /// The crates a generated program declares it needs. fn needed_crates(program: &str) -> Vec { program .lines() .find(|l| l.starts_with("// Cargo.toml needs:")) .map(|l| { l.trim_start_matches("// Cargo.toml needs:") .split(',') .map(|c| c.trim().to_string()) .filter(|c| !c.is_empty()) .collect() }) .unwrap_or_default() } #[test] fn every_golden_graph_emits_a_program_that_compiles() { // A missing cargo used to SKIP and pass green. It now fails unless the run opts out // explicitly with FLOWRS_ALLOW_MISSING_CARGO=1. if std::process::Command::new("cargo") .arg("-V") .output() .is_err() { assert!( std::env::var("FLOWRS_ALLOW_MISSING_CARGO").as_deref() == Ok("1"), "golden_graphs: no cargo on PATH; the compile check cannot pass without it \ (set FLOWRS_ALLOW_MISSING_CARGO=1 to skip on purpose)" ); eprintln!("SKIP golden_graphs: no cargo on PATH (FLOWRS_ALLOW_MISSING_CARGO=1)"); return; } // One scratch crate, reused across every golden: the dependency tree is built once and // cached in target/, so only the emitted main.rs is recompiled per graph. let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .and_then(|p| p.parent()) .expect("workspace root") .to_path_buf(); let dir = std::env::var_os("CARGO_TARGET_DIR") .map(std::path::PathBuf::from) .unwrap_or_else(|| root.join("target")) .join("flowrs_golden_graphs"); std::fs::create_dir_all(dir.join("src")).expect("scratch crate"); let mut checked = 0; let mut skipped: Vec = Vec::new(); let mut failures: Vec = Vec::new(); for g in GOLDENS { let graph = (g.build)(); let program = match graph.to_rust() { Ok(p) => p, Err(e) => { failures.push(format!("{}: codegen failed: {e}", g.name)); continue; } }; let needs = needed_crates(&program); let mut deps = String::new(); let mut missing = Vec::new(); for c in &needs { match DEPS.iter().find(|(k, _)| k == c) { // The hint carries the *import* spelling (`tract_onnx`); the crate is // `tract-onnx`. Cargo accepts either in a manifest key, so normalise. Some((_, v)) => deps.push_str(&format!("{} = \"{}\"\n", c.replace('_', "-"), v)), None => missing.push(c.clone()), } } if !missing.is_empty() { skipped.push(format!( "{} (no pinned version for: {})", g.name, missing.join(", ") )); continue; } std::fs::write( dir.join("Cargo.toml"), format!( "[workspace]\n[package]\nname = \"flowrs_golden\"\nversion = \"0.0.0\"\n\ edition = \"2021\"\n[dependencies]\n{deps}" ), ) .expect("manifest"); std::fs::write(dir.join("src/main.rs"), &program).expect("source"); let out = std::process::Command::new("cargo") .args(["check", "--quiet", "--offline"]) .current_dir(&dir) .output() .expect("cargo check"); if out.status.success() { checked += 1; continue; } let err = String::from_utf8_lossy(&out.stderr); // An unresolvable dependency is an environment limit, not a codegen defect, but it // is not a pass either: it FAILS unless the run opts out with // FLOWRS_ALLOW_OFFLINE_SKIP=1, the same rule as the in-crate compile harnesses // (`skip_for_missing_dependency`). rustc having run means the code is the problem. // `"registry"` is deliberately not a marker: every diagnostic that cites a // dependency's source path contains it, so it turned real errors into skips. let lower = err.to_ascii_lowercase(); let missing_dependency = !err.contains("error[E") && !err.contains("could not compile") && [ "no matching package", "failed to download", "attempting to make an http request", "failed to get", "failed to load source", "failed to select a version", "offline mode", ] .iter() .any(|m| lower.contains(m)); if missing_dependency { if std::env::var("FLOWRS_ALLOW_OFFLINE_SKIP").as_deref() == Ok("1") { skipped.push(format!("{} (deps unresolvable offline)", g.name)); continue; } failures.push(format!( "{}: its dependencies are not available offline, so it was not compiled; \ fetch them once with `cargo fetch --manifest-path {}`, or set \ FLOWRS_ALLOW_OFFLINE_SKIP=1 to skip on purpose\n{}", g.name, dir.join("Cargo.toml").display(), err.trim() )); continue; } failures.push(format!( "{}: generated program did not compile\n{}\n--- source ---\n{}", g.name, err.trim(), program )); } for s in &skipped { eprintln!("SKIP golden_graphs: {s}"); } assert!( failures.is_empty(), "{} of {} golden graph(s) emitted code that does not compile:\n\n{}", failures.len(), GOLDENS.len(), failures.join("\n\n") ); // If every graph skipped, the test proved nothing — fail rather than report green. assert!( checked > 0, "no golden graph could be compiled ({} skipped); this test verified NOTHING", skipped.len() ); eprintln!( "golden_graphs: {checked} compiled, {} skipped", skipped.len() ); }