flowrs-engine/crates/flowrs-core/tests/teleplot_indicator.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

167 lines
6.3 KiB
Rust

//! An Indicator in a compiled program prints a labelled Teleplot line (`>name:value`), not
//! an unlabelled `println!`: what Teleplot, flowrs-live and an exported app's shell read.
//!
//! Each case is compiled with rustc, run, and its stdout read by flowrs-live's Teleplot
//! decoder, which was written to Teleplot's own reader and knows nothing of codegen. The
//! value it reads must be the interpreter's value on the port the Indicator shows.
use flowrs_core::codegen::rust::{indicator_names, teleplot_name};
use flowrs_core::graph::dsl::parse_graph;
use flowrs_live::telemetry::{Dialect, decode_all};
fn run(src: &str, name: &str) -> String {
let dir = std::env::temp_dir().join(format!("flowrs_teleplot_{}_{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 build = std::process::Command::new("rustc")
.args(["--edition", "2021", "-o"])
.arg(&bin)
.arg(&main)
.output()
.expect("rustc runs");
assert!(
build.status.success(),
"{name} does not compile:\n{}\n{src}",
String::from_utf8_lossy(&build.stderr)
);
let out = std::process::Command::new(&bin).output().expect("it runs");
let _ = std::fs::remove_dir_all(&dir);
assert!(out.status.success(), "{name}: {out:?}");
String::from_utf8(out.stdout).unwrap()
}
#[test]
fn an_indicator_prints_its_label_and_its_value_as_teleplot() {
let g = parse_graph(
"let n0 = FloatLiteral { value: 2.5 };\n\
let n1 = FloatLiteral { value: 4 };\n\
let n2 = math.mul(a: n0.value, b: n1.value);\n\
let n3 = Indicator(input: n2.product) { label: \"Power: out\" };\n\
let n4 = IntLiteral { value: 7 };\n\
let n5 = Indicator(input: n4.value);\n\
let n6 = BoolLiteral { value: true };\n\
let n7 = Indicator(input: n6.value) { label: \"ok\" };\n\
let n8 = Linspace;\n\
let n9 = MapSin(input: n8.values);\n\
let n10 = Indicator(input: n9.values) { label: \"sin(x)\" };\n\
let n11 = StringLiteral { value: \"a:b; c|d\" };\n\
let n12 = Indicator(input: &n11.value) { label: \"note\" };\n\
let n13 = Indicator;",
)
.expect("the graph parses")
.graph;
let src = g.to_rust().expect("codegen");
assert!(
!src.contains("println!(\"{}\", 0)"),
"an unwired Indicator printed a zero: {src}"
);
let out = run(&src, "kinds");
let live = decode_all(out.as_bytes(), Some(Dialect::Teleplot), "program", 0.0);
assert!(
live.flags.is_empty(),
"flowrs-live flagged {:?}\n{out}",
live.flags
);
let v = |name: &str| {
live.series
.iter()
.find(|s| s.name == name)
.unwrap_or_else(|| {
panic!(
"no series {name:?} in {:?}\n{out}",
live.series.iter().map(|s| &s.name).collect::<Vec<_>>()
)
})
.v
.clone()
};
// The label is the name, Teleplot's separators made spaces; unlabelled, `<Source> · port`.
assert_eq!(v("Power out"), vec![10.0]);
assert_eq!(v("IntLiteral · value"), vec![7.0]);
assert_eq!(v("ok"), vec![1.0]);
// A series arrives as `index:value` pairs, which flowrs-live reads as x and y.
let ys = v("sin(x).y");
assert_eq!(ys.len(), 64, "{out}");
// flowrs-math's sine, as the editor and the program both compute it.
let want: Vec<f64> = (0..64)
.map(|i| flowrs_math::sin(std::f64::consts::TAU * i as f64 / 63.0))
.collect();
assert_eq!(ys, want);
// Text is its `{:?}`, Teleplot's separators escaped, and reads back whole.
let note = live
.logs
.iter()
.find(|l| l.text.starts_with("note: "))
.unwrap_or_else(|| panic!("no text line\n{out}"));
assert_eq!(note.text, "note: \"a%3Ab%3B c%7Cd\"");
// Every line the program printed is a variable: nothing unlabelled is left.
assert!(out.lines().all(|l| l.starts_with('>')), "{out}");
assert_eq!(out.lines().count(), 5, "{out}");
}
#[test]
fn indicator_names_are_their_labels_made_teleplot_safe_and_unique() {
let g = parse_graph(
"let n0 = FloatLiteral { value: 1 };\n\
let n1 = Indicator(input: n0.value) { label: \"t\" };\n\
let n2 = Indicator(input: n0.value) { label: \"t\" };\n\
let n3 = Indicator(input: n0.value);",
)
.unwrap()
.graph;
let names: Vec<String> = indicator_names(&g).into_values().collect();
assert_eq!(names, vec!["t", "t (n2)", "FloatLiteral · value"]);
assert_eq!(teleplot_name("a:b,c|d;e§f\ng"), "a b c d e f g");
assert_eq!(teleplot_name(" "), "value");
}
#[test]
fn an_indicator_in_a_step_loop_prints_every_pass() {
let g = parse_graph(
"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\" };",
)
.unwrap()
.graph;
let out = run(&g.to_rust_steps(3).expect("codegen"), "steps");
let live = decode_all(out.as_bytes(), Some(Dialect::Teleplot), "program", 0.0);
let s = live
.series
.iter()
.find(|s| s.name == "count")
.expect("count");
assert_eq!(s.v, vec![1.0, 2.0, 3.0], "{out}");
}
#[test]
fn a_long_series_goes_over_lines_teleplot_readers_take() {
let g = parse_graph(
"let n0 = IntLiteral { value: 600 };\n\
let n1 = Linspace(count: n0.value);\n\
let n2 = MapSin(input: n1.values);\n\
let n3 = Indicator(input: n2.values) { label: \"wave\" };",
)
.unwrap()
.graph;
let out = run(&g.to_rust().expect("codegen"), "long");
assert!(out.lines().count() > 1, "one line of {} bytes", out.len());
assert!(
out.lines()
.all(|l| l.len() < flowrs_live::telemetry::MAX_LINE),
"{out}"
);
let live = decode_all(out.as_bytes(), Some(Dialect::Teleplot), "program", 0.0);
assert!(live.flags.is_empty(), "{:?}", live.flags);
let ys = &live
.series
.iter()
.find(|s| s.name == "wave.y")
.expect("wave")
.v;
assert_eq!(ys.len(), 600);
let tau = std::f64::consts::TAU;
assert_eq!(ys[599], (0.0 + (tau - 0.0) * 599.0 / 599.0).sin());
}