flowrs-engine/crates/flowrs-core/tests/scoped_node_codegen.rs
flowRS fd958c0547 flowRS engine, cut from flowRS 3451b065
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 19:04:39 -05:00

123 lines
4.9 KiB
Rust

//! 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);
}
}