//! The closure-bodied, user-code, match, game and LOOP nodes must emit Rust that builds. //! //! Emitting *something* is not the bar — `node_coverage` measures that. This compiles the //! whole program, and additionally asserts that a loop's body lands INSIDE its braces, //! which is the part a compile check alone would not catch: a body emitted after the //! closing brace still compiles, and is silently not a loop body at all. use flowrs_core::graph::{Graph, WireMode}; use std::process::Command; fn check(label: &str, g: &Graph) { let src = match g.to_rust() { Ok(s) => s, Err(e) => panic!("{label} should emit, but codegen refused: {e}"), }; let dir = std::env::temp_dir().join("flowrs_phase1"); std::fs::create_dir_all(dir.join("src")).unwrap(); std::fs::write( dir.join("Cargo.toml"), "[workspace]\n[package]\nname=\"p\"\nversion=\"0.0.0\"\nedition=\"2021\"\n", ) .unwrap(); std::fs::write(dir.join("src/main.rs"), &src).unwrap(); let out = Command::new("cargo") .args(["check", "--quiet", "--offline", "--message-format=short"]) .current_dir(&dir) // `CARGO_TARGET_DIR` removed so this check does not depend on ambient config. // // `graph_build` in flowrs-mcp had a demonstrated false pass from an inherited // target dir: its scratch crates are all `flowrs_graph 0.0.0`, and one shared // target made them a single build unit, so cargo found a later program already // fresh and exited 0 without reading it. These harnesses use the same package // identity, so the same shape is possible here. // // Tested, and it did NOT reproduce: priming a shared target with a passing // harness and then running this one with its own codegen broken still failed // correctly. This is isolation against a hazard that has bitten once nearby, // not a fix for a bug demonstrated here. .env_remove("CARGO_TARGET_DIR") .output() .unwrap(); if out.status.success() { return; } let e = String::from_utf8_lossy(&out.stderr); if e.contains("failed to download") || e.contains("no matching package") { eprintln!("skipped {label}: dependencies unavailable offline"); return; } let diags: Vec<&str> = e.lines().filter(|l| l.contains("error")).take(6).collect(); panic!( "{label} emits Rust that does not compile:\n {}\n---\n{src}---", diags.join("\n ") ); } fn prop(g: &mut Graph, n: flowrs_core::NodeId, k: &str, v: &str) { g.nodes .get_mut(&n) .unwrap() .properties .insert(k.into(), v.into()); } #[test] fn closure_usercode_match_and_game_nodes_compile() { // Standalone: no inputs needed. for ty in ["CodeBlockNode", "Sprite", "Paddle", "Spawn"] { let mut g = Graph::new(); g.add_node(ty, 0.0, 0.0); check(ty, &g); } // CodeBlock with real source. let mut g = Graph::new(); let c = g.add_node("CodeBlock", 0.0, 0.0); prop(&mut g, c, "src", "let a = 2; a * 3"); check("CodeBlock(src)", &g); // Iterator adapters, wired from Range. for (ty, src) in [("Map", "x * 2"), ("Filter", "*x > 2")] { let mut g = Graph::new(); let a = g.add_node("IntLiteral", 0.0, 0.0); prop(&mut g, a, "value", "0"); let b = g.add_node("IntLiteral", 0.0, 0.0); prop(&mut g, b, "value", "5"); let r = g.add_node("Range", 0.0, 0.0); g.add_wire(a, "value", r, "start", WireMode::Move).unwrap(); g.add_wire(b, "value", r, "end", WireMode::Move).unwrap(); let n = g.add_node(ty, 0.0, 0.0); prop(&mut g, n, "src", src); g.add_wire(r, "iterator", n, "iterator", WireMode::Move) .unwrap(); check(&format!("{ty}(wired)"), &g); } // Fold needs an init too. let mut g = Graph::new(); let a = g.add_node("IntLiteral", 0.0, 0.0); prop(&mut g, a, "value", "0"); let b = g.add_node("IntLiteral", 0.0, 0.0); prop(&mut g, b, "value", "5"); let r = g.add_node("Range", 0.0, 0.0); g.add_wire(a, "value", r, "start", WireMode::Move).unwrap(); g.add_wire(b, "value", r, "end", WireMode::Move).unwrap(); let init = g.add_node("IntLiteral", 0.0, 0.0); prop(&mut g, init, "value", "0"); let f = g.add_node("Fold", 0.0, 0.0); prop(&mut g, f, "src", "acc + x as i64"); g.add_wire(r, "iterator", f, "iterator", WireMode::Move) .unwrap(); g.add_wire(init, "value", f, "init", WireMode::Move) .unwrap(); check("Fold(wired)", &g); // Match over a literal. for ty in ["Match", "MatchNode"] { let mut g = Graph::new(); let v = g.add_node("IntLiteral", 0.0, 0.0); prop(&mut g, v, "value", "2"); let m = g.add_node(ty, 0.0, 0.0); prop(&mut g, m, "src", "1 => \"one\", _ => \"other\""); g.add_wire(v, "value", m, "value", WireMode::Move).unwrap(); check(&format!("{ty}(wired)"), &g); } }