//! Live controls in an exported program (`primitives_to_rust_app_live`): a program with a //! step loop reads `n3=0.5` lines on standard input and applies each at the top of its next //! pass, refusing what the control cannot take. //! //! Each program here is compiled with rustc and run. With no line arriving (standard input //! empty, closed, or open and silent) its stdout is byte for byte the stdout of the program //! built without live controls: what T1 computes for the baked values does not change. With //! a line, the control takes the new value at the pass the program says (on stderr), and //! every pass from then on computes with it. use flowrs_core::Graph; use flowrs_core::NodeId; use flowrs_core::codegen::rust::{ LiveControl, LiveKind, has_step_loop, live_kind, primitives_to_rust_app, primitives_to_rust_app_live, }; use flowrs_core::graph::dsl::parse_graph; use std::io::Write; use std::path::PathBuf; use std::process::{Command, Stdio}; fn graph(text: &str) -> Graph { parse_graph(text).expect("graph text").graph } /// A counter: each pass adds the step (n0) to what the last pass summed. const COUNTER: &str = "let n0 = FloatLiteral { value: 1 };\n\ let n1 = math.add(a: n1.sum.copy().prev(), b: n0.value);\n\ let n2 = Indicator(input: n1.sum) { label: \"Count\" };"; /// The counter, paced: each pass waits 40 ms, so a line has time to arrive mid-run. const PACED: &str = "let n0 = FloatLiteral { value: 1 };\n\ let n1 = math.add(a: n1.sum.copy().prev(), b: n0.value);\n\ let n2 = flow.delay { duration_s: 0.04 };\n\ let n3 = Indicator(input: n1.sum) { label: \"Count\" };"; fn step(min: Option, max: Option) -> LiveControl { LiveControl { node: NodeId(0), kind: LiveKind::Number, min, max, options: Vec::new(), } } /// Build `src` with rustc into a scratch directory; the binary's path. fn build(src: &str, name: &str) -> PathBuf { let dir = std::env::temp_dir().join(format!("flowrs_live_{}_{name}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let main = dir.join("main.rs"); let bin = dir.join("prog"); std::fs::write(&main, src).unwrap(); let out = Command::new("rustc") .args(["--edition", "2021", "-O", "-o"]) .arg(&bin) .arg(&main) .output() .expect("rustc runs"); assert!( out.status.success(), "{name} does not compile:\n{}\n{src}", String::from_utf8_lossy(&out.stderr) ); // The live machinery adds no warning of its own (dead code, an unused binding). let warnings = String::from_utf8_lossy(&out.stderr); for ours in [ "flowrs_controls", "ctl_n", "live_controls", "never used", "dead_code", ] { assert!( !warnings.contains(ours), "{name}: a warning about {ours}:\n{warnings}" ); } bin } /// How the program's standard input is given. enum Input<'a> { Null, Closed, Silent, Lines(&'a str), } /// Run `bin` with `input`; its stdout and stderr. fn run(bin: &PathBuf, input: Input) -> (String, String) { let mut cmd = Command::new(bin); cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); cmd.stdin(match input { Input::Null => Stdio::null(), _ => Stdio::piped(), }); let mut child = cmd.spawn().expect("it runs"); // Kept open while the program runs, for `Silent` and `Lines`. let mut held = None; match input { Input::Null => {} Input::Closed => drop(child.stdin.take()), Input::Silent => held = child.stdin.take(), Input::Lines(text) => { let mut w = child.stdin.take().unwrap(); w.write_all(text.as_bytes()).unwrap(); w.flush().unwrap(); held = Some(w); } } let out = child.wait_with_output().expect("it ends"); drop(held); assert!(out.status.success(), "{out:?}"); ( String::from_utf8(out.stdout).unwrap(), String::from_utf8(out.stderr).unwrap(), ) } /// The counts a run printed, in order. fn counts(stdout: &str) -> Vec { stdout .lines() .filter_map(|l| l.strip_prefix(">Count:")) .map(|v| v.parse().unwrap()) .collect() } #[test] fn a_control_is_read_from_its_own_binding_set_at_the_top_of_each_pass() { let g = graph(COUNTER); assert!(has_step_loop(&g)); let p = primitives_to_rust_app_live(&g, 5, &[], &[step(Some(0.0), Some(5.0))]).unwrap(); for want in [ "let mut live_controls = flowrs_controls::listen();", "let mut ctl_n0 = 1.0f64;", "for (name, value) in live_controls.pending() {", "\"n0\" => flowrs_controls::number(&mut ctl_n0, &name, &value, Some(0.0), Some(5.0), false, pass),", "_ => flowrs_controls::refuse(&name, &value, \"it is not a control of this program\", pass),", "mod flowrs_controls {", "pub fn number(", ] { assert!(p.contains(want), "{want} missing from:\n{p}"); } // The binding is declared before the loop, and set before any node of a pass runs. let decl = p.find("let mut ctl_n0").unwrap(); let lp = p.find("for _step in 0..5 {").unwrap(); let set = p.find("live_controls.pending()").unwrap(); let read = p.find("= ctl_n0;").unwrap(); assert!(decl < lp && lp < set && set < read, "{p}"); // Only the setters its controls use: no dead code in the program. assert!( !p.contains("pub fn boolean") && !p.contains("pub fn text("), "{p}" ); // No controls: the program is exactly the one built without them. assert_eq!( primitives_to_rust_app_live(&g, 5, &[], &[]).unwrap(), primitives_to_rust_app(&g, 5, &[]).unwrap() ); } #[test] fn a_program_that_runs_once_takes_no_controls() { // One pass of the loop: a change would never apply. let g = graph(COUNTER); assert!( primitives_to_rust_app_live(&g, 1, &[], &[step(None, None)]) .unwrap_err() .contains("one pass") ); // No step loop at all. let once = graph( "let n0 = FloatLiteral { value: 2 };\nlet n1 = Indicator(input: n0.value) { label: \"Two\" };", ); assert!(!has_step_loop(&once)); assert!(primitives_to_rust_app_live(&once, 5, &[], &[step(None, None)]).is_err()); } #[test] fn what_a_control_holds_while_its_program_runs() { let mut g = graph( "let n0 = IntLiteral { value: 3 };\n\ let n1 = FloatLiteral { value: 0.5 };\n\ let n2 = BoolLiteral { value: true };\n\ let n3 = StringLiteral { value: \"sine\" };", ); let range = g.add_node("Control", 0.0, 0.0); let plain = g.add_node("Control", 0.0, 0.0); let add = g.add_node("Add", 0.0, 0.0); for (id, k, v) in [ (range, "value", "1,2"), (range, "widget", "range"), (plain, "value", "7"), ] { g.get_node_mut(id) .unwrap() .properties .insert(k.into(), v.into()); } assert_eq!((range, plain, add), (NodeId(4), NodeId(5), NodeId(6))); let k = |id| live_kind(&g, NodeId(id)); assert_eq!(k(0), Ok(LiveKind::Integer)); assert_eq!(k(1), Ok(LiveKind::Number)); assert_eq!(k(2), Ok(LiveKind::Bool)); assert_eq!(k(3), Ok(LiveKind::Text)); assert!(k(4).unwrap_err().contains("several values")); assert!(matches!(k(5), Ok(LiveKind::Integer | LiveKind::Number))); assert!(k(6).unwrap_err().contains("not a control")); assert!(k(99).is_err()); } /// The parity proof: with no line arriving, however standard input is given, the live /// program prints byte for byte what the program built without live controls prints. #[test] fn with_no_change_the_program_prints_exactly_what_it_did_before() { for (name, text, steps) in [("counter", COUNTER, 8), ("paced", PACED, 6)] { let g = graph(text); let before = build( &primitives_to_rust_app(&g, steps, &[]).unwrap(), &format!("{name}_before"), ); let live = build( &primitives_to_rust_app_live(&g, steps, &[], &[step(Some(0.0), Some(5.0))]).unwrap(), &format!("{name}_live"), ); let (want, _) = run(&before, Input::Null); assert!(!want.is_empty(), "{name} printed nothing"); for (how, input) in [ ("stdin null", Input::Null), ("stdin closed", Input::Closed), ("stdin open and silent", Input::Silent), ] { let (got, err) = run(&live, input); assert_eq!(got, want, "{name}, {how}: stdout differs"); assert!(err.is_empty(), "{name}, {how}: {err}"); } } } /// A change applies at the top of the next pass, and from then on; what the control /// cannot take is refused on stderr, and the control keeps its value. #[test] fn a_change_applies_at_the_next_pass_and_a_bad_one_is_refused() { let g = graph(PACED); let steps = 30; let live = build( &primitives_to_rust_app_live(&g, steps, &[], &[step(Some(0.0), Some(5.0))]).unwrap(), "paced_change", ); let (out, err) = run( &live, Input::Lines("n0=9\nn7=1\nn0=abc\nnot a line\n$n0=3\n"), ); // The refusals, each saying why, and the change, saying from which pass. assert!(err.contains("n0 = 9 refused at pass"), "{err}"); assert!(err.contains("it is above 5"), "{err}"); assert!( err.contains("n7 = 1 refused at pass") && err.contains("not a control of this program"), "{err}" ); assert!( err.contains("n0 = abc refused at pass") && err.contains("not a number"), "{err}" ); assert!(err.contains("`not a line` is not name=value"), "{err}"); let from: usize = err .lines() .find_map(|l| l.strip_prefix("flowrs: n0 = 3 from pass ")) .expect("the change was taken") .trim() .parse() .unwrap(); // Every pass before it added 1; it and every pass after, 3. let got = counts(&out); assert_eq!(got.len(), steps, "{out}"); let mut sum = 0.0; for (i, c) in got.iter().enumerate() { let pass = i + 1; sum += if pass < from { 1.0 } else { 3.0 }; assert_eq!( *c, sum, "pass {pass} (the change from pass {from}): {got:?}" ); } assert!(from < steps, "the change came in time: pass {from}"); } #[test] fn a_switch_a_whole_number_and_text_each_take_their_own_kind() { // Each control feeds an Indicator, so what it holds each pass is printed. let g = graph( "let n0 = IntLiteral { value: 2 };\n\ let n1 = BoolLiteral { value: false };\n\ let n2 = StringLiteral { value: \"sine\" };\n\ let n8 = FloatLiteral { value: 1 };\n\ let n3 = math.add(a: n3.sum.copy().prev(), b: n8.value);\n\ let n4 = flow.delay { duration_s: 0.03 };\n\ let n5 = Indicator(input: n0.value) { label: \"Whole\" };\n\ let n6 = Indicator(input: n1.value) { label: \"Switch\" };\n\ let n7 = Indicator(input: n2.value) { label: \"Shape\" };", ); let c = |id: u64, kind, options: &[&str]| LiveControl { node: NodeId(id), kind, min: Some(0.0), max: Some(10.0), options: options.iter().map(|s| s.to_string()).collect(), }; let controls = [ c(0, LiveKind::Integer, &[]), c(1, LiveKind::Bool, &[]), c(2, LiveKind::Text, &["sine", "square"]), ]; let src = primitives_to_rust_app_live(&g, 20, &[], &controls).unwrap(); let bin = build(&src, "kinds"); let (out, err) = run( &bin, Input::Lines("n0=2.5\nn0=7\nn1=maybe\nn1=true\nn2=saw\nn2=square\n"), ); assert!( err.contains("n0 = 2.5 refused") && err.contains("not a whole number"), "{err}" ); assert!(err.contains("n1 = maybe refused"), "{err}"); assert!( err.contains("n2 = saw refused") && err.contains("not one of"), "{err}" ); for set in [ "n0 = 7 from pass", "n1 = true from pass", "n2 = square from pass", ] { assert!(err.contains(set), "{set}: {err}"); } let last = |name: &str| { out.lines() .filter_map(|l| l.strip_prefix(&format!(">{name}:"))) .next_back() .unwrap() .to_string() }; assert_eq!(last("Whole"), "7"); assert_eq!(last("Switch"), "1"); assert_eq!(last("Shape"), "\"square\"|t"); }