348 lines
12 KiB
Rust
348 lines
12 KiB
Rust
//! 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<f64>, max: Option<f64>) -> 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<f64> {
|
|
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<T: std::str::FromStr>(",
|
|
] {
|
|
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");
|
|
}
|