//! Taint tracking through the executor (`graph::taint`): every way a value can travel from //! a neural source to another node is followed, and an agent's run (`withhold`) computes on //! zeros in the recording's shape, so nothing it reports, logs or prints depends on a //! sample. //! //! The source is a live biosignal stream whose every sample is a distinctive value //! (4321.333... plus a per-sample offset), and each test tries one way an agent could get a //! sample out: index one, take its minimum, do arithmetic on it, compare it with a //! threshold, average a window of one, format it into text or print it, let a widget compute //! on it, steer a loop by it, pass it through the program scope, a written file, a held wire //! or an in-place write-back. use flowrs_core::graph::runtime::Runtime; use flowrs_core::graph::taint::{self, LOGGED_WITHHELD, Taint}; use flowrs_core::graph::{Graph, GraphExecutor, NodeId, WidgetRegistry}; use flowrs_core::{Value, WireMode}; use std::collections::HashMap; use std::sync::Arc; /// A 4-channel synthetic EEG stream, every sample replaced by a value no computation on /// zeros can produce: 4321.333 + channel + sample / 1024. fn stream() -> (flowrs_live::LiveStream, Vec) { use flowrs_live::biosignal::{Out, Source, stream_for, synthetic}; let mut syn = synthetic::Synthetic::new(synthetic::Config { channels: 4, ..synthetic::Config::default() }); let meta = syn.meta().clone(); let mut st = stream_for(&meta, "synthetic EEG"); let mut out = Out::default(); syn.take(64, 0.0, &mut out); out.apply(&meta, &mut st); let mut values = Vec::new(); for (c, x) in st.series.iter_mut().enumerate() { for (i, v) in x.v.iter_mut().enumerate() { *v = 4321.0 + c as f64 + i as f64 / 1024.0 + 1.0 / 3.0; values.push(*v); } } (st, values) } fn runtime() -> (Arc, Vec) { let rt = Runtime::new(); let (st, values) = stream(); rt.put_live("bio", st); assert!( rt.is_neural("live", "bio"), "an EEG stream is marked neural" ); (rt, values) } struct G { g: Graph, } impl G { fn new() -> Self { G { g: Graph::new() } } fn node(&mut self, ty: &str, props: &[(&str, &str)]) -> NodeId { let n = self.g.add_node(ty, 0.0, 0.0); for (k, v) in props { self.g .nodes .get_mut(&n) .unwrap() .properties .insert((*k).into(), (*v).into()); } n } fn wire(&mut self, a: NodeId, ap: &str, b: NodeId, bp: &str) { self.g.add_wire(a, ap, b, bp, WireMode::Copy).unwrap(); } /// The biosignal source: channel C3 on `signal`. fn source(&mut self) -> NodeId { self.node("live.biosignal", &[("stream", "bio"), ("channel", "C3")]) } fn int(&mut self, v: i64) -> NodeId { self.node("IntLiteral", &[("value", &v.to_string())]) } fn float(&mut self, v: f64) -> NodeId { self.node("FloatLiteral", &[("value", &v.to_string())]) } } /// Run `g` for the person (every value) and for an agent (`withhold`), each on `rt`. fn both(g: &Graph, rt: &Arc) -> (Run, Run) { let person = run(g, rt, false); let agent = run(g, rt, true); (person, agent) } struct Run { outputs: HashMap<(NodeId, String), Value>, taint: Taint, console: String, log: String, } fn run(g: &Graph, rt: &Arc, withhold: bool) -> Run { let mut ex = GraphExecutor::with_runtime(WidgetRegistry::with_builtins(), rt.clone()).withhold(withhold); let before = rt.log().events.len(); let steps = ex.run(g).expect("runs"); let mut outputs = HashMap::new(); for s in steps { for (p, v) in s.outputs { outputs.insert((s.node_id, p), v); } } let log = format!("{:?}", &rt.log().events[before..]); Run { outputs, taint: ex.taint().clone(), console: ex.console().to_string(), log, } } /// Every rendering of a recorded sample (and of the sample plus or minus one, times two, /// and of a comparison's threshold side) that must not appear in what an agent run shows. fn assert_no_sample(text: &str, values: &[f64], what: &str) { for x in values { for y in [*x, x + 1.0, x * 2.0] { let r = format!("{y}"); assert!(!text.contains(&r), "{what}: carries {r}"); } } } /// The port carries neural data in both runs, the person sees the real value, and the /// agent run's value is the zero-world one (never a sample) and is logged as withheld. fn assert_withheld( person: &Run, agent: &Run, node: NodeId, port: &str, values: &[f64], what: &str, ) { assert!( person.taint.port(node, port), "{what}: tainted for the person" ); assert!( agent.taint.port(node, port), "{what}: tainted for the agent" ); let key = (node, port.to_string()); assert!( person.outputs.contains_key(&key), "{what}: the person's run computed it" ); if let Some(v) = agent.outputs.get(&key) { assert_no_sample(&format!("{v:?}"), values, what); } assert_no_sample(&agent.log, values, &format!("{what} (log)")); assert!( agent.log.contains(LOGGED_WITHHELD), "{what}: logged as withheld" ); } #[test] fn indexing_one_sample() { let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let i = g.int(5); let get = g.node("VecGet", &[]); g.wire(src, "signal", get, "vec"); g.wire(i, "value", get, "index"); let (p, a) = both(&g.g, &rt); assert_withheld(&p, &a, get, "item", &values, "VecGet"); // The person reads the real sample. assert!(matches!(p.outputs[&(get, "item".into())], Value::Float(x) if x > 4321.0)); // The index itself is not neural. assert!(!a.taint.port(i, "value")); assert!(!a.taint.node(i)); } #[test] fn minimum_and_maximum() { let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let lo = g.node("Min", &[]); let hi = g.node("Max", &[]); g.wire(src, "signal", lo, "input"); g.wire(src, "signal", hi, "input"); let (p, a) = both(&g.g, &rt); assert_withheld(&p, &a, lo, "result", &values, "Min"); assert_withheld(&p, &a, hi, "result", &values, "Max"); } #[test] fn arithmetic_on_one_sample() { let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let get = g.node("VecGet", &[]); g.wire(src, "signal", get, "vec"); let one = g.float(1.0); let add = g.node("Add", &[]); g.wire(get, "item", add, "a"); g.wire(one, "value", add, "b"); let (p, a) = both(&g.g, &rt); assert_withheld(&p, &a, add, "result", &values, "Add"); } #[test] fn a_comparison_used_as_an_oracle() { // `sample > t`, bisected over t, reads a sample one bit per run. The bit is tainted, // and so is everything it selects. let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let get = g.node("VecGet", &[]); g.wire(src, "signal", get, "vec"); let t = g.float(4321.5); let gt = g.node("GreaterThan", &[]); g.wire(get, "item", gt, "a"); g.wire(t, "value", gt, "b"); let yes = g.int(1); let no = g.int(0); let pick = g.node("IfThenElse", &[]); g.wire(gt, "result", pick, "condition"); g.wire(yes, "value", pick, "then_value"); g.wire(no, "value", pick, "else_value"); let (p, a) = both(&g.g, &rt); assert_withheld(&p, &a, gt, "result", &values, "GreaterThan"); assert_withheld(&p, &a, pick, "result", &values, "IfThenElse"); // For the agent the answer does not depend on the threshold or the data: the source // is zeros, so `0 > t` is false whatever t is. assert!(matches!( a.outputs[&(gt, "result".into())], Value::Bool(false) )); } #[test] fn statistics_on_a_window_of_one() { let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let get = g.node("VecGet", &[]); g.wire(src, "signal", get, "vec"); let v = g.node("VecNew", &[]); let push = g.node("VecPush", &[]); g.wire(v, "vec", push, "vec"); g.wire(get, "item", push, "item"); let mean = g.node("Mean", &[]); g.wire(push, "vec", mean, "input"); let (p, a) = both(&g.g, &rt); assert_withheld(&p, &a, mean, "result", &values, "Mean of one"); // The person's mean of one sample is that sample. assert!(matches!(p.outputs[&(mean, "result".into())], Value::Float(x) if x > 4321.0)); } #[test] fn string_formatting_and_the_console() { let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let get = g.node("VecGet", &[]); g.wire(src, "signal", get, "vec"); let print = g.node("PrintLn", &[]); g.wire(get, "item", print, "input"); let hello = g.node("StringLiteral", &[("value", "hello")]); let print2 = g.node("PrintLn", &[]); g.wire(hello, "value", print2, "input"); let (p, a) = both(&g.g, &rt); // The person's console has the sample; the agent's console shows the untainted line // and one fixed marker, never how much was printed or what. assert!(p.console.contains("4323."), "{}", p.console); let shown = a.taint.console_for_agent(); assert!(shown.contains("hello"), "{shown}"); assert!(shown.contains("withheld"), "{shown}"); assert_no_sample(&shown, &values, "agent console"); assert!(a.taint.node(print)); assert!(!a.taint.node(print2)); assert_no_sample(&a.log, &values, "log"); } #[test] fn a_widgets_output() { let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let get = g.node("VecGet", &[]); g.wire(src, "signal", get, "vec"); let two = g.float(2.0); let mul = g.node("math.mul", &[]); g.wire(get, "item", mul, "a"); g.wire(two, "value", mul, "b"); let (p, a) = both(&g.g, &rt); assert_withheld(&p, &a, mul, "product", &values, "math.mul"); } #[test] fn a_loop_steered_by_neural_data_taints_its_body() { // `for x in channel { v.push(1) }`: what the body made depends on how many passes the // samples drove, and a body node's `break` could have depended on one, so the whole // body is tainted. let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let lp = g.node("ForLoop", &[]); g.wire(src, "signal", lp, "iterator"); let v = g.node("VecNew", &[]); let push = g.node("VecPush", &[]); g.wire(v, "vec", push, "vec"); let one = g.int(1); g.wire(one, "value", push, "item"); g.wire(push, "vec", lp, "body"); let owners = flowrs_core::graph::loops::body_owners(&g.g); assert_eq!(owners.get(&push), Some(&lp), "push is the loop's body"); let (p, a) = both(&g.g, &rt); assert!( p.taint.node(lp) && a.taint.node(lp), "the loop ran on neural data" ); assert!(a.taint.node(push), "its body ran on neural data"); assert!(a.taint.port(push, "vec")); assert_no_sample(&a.log, &values, "log"); } #[test] fn a_body_node_on_neural_data_taints_its_loop() { // A loop over literals whose body reads a sample: `break` may depend on it, so the // loop's result and every body node are tainted. let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let get = g.node("VecGet", &[]); g.wire(src, "signal", get, "vec"); let items = g.node("Linspace", &[]); let lp = g.node("LoopNode", &[]); g.wire(items, "values", lp, "iterator"); let t = g.float(4321.5); let gt = g.node("GreaterThan", &[]); g.wire(get, "item", gt, "a"); g.wire(t, "value", gt, "b"); g.wire(gt, "result", lp, "body"); let (_, a) = both(&g.g, &rt); let owners = flowrs_core::graph::loops::body_owners(&g.g); assert_eq!( owners.get(>), Some(&lp), "the comparison is the loop's body" ); assert!(a.taint.node(lp), "the loop is tainted by its body"); assert!(a.taint.port(lp, "result")); // So is every node of the body, those on no neural wire too: what each made, and // how often, is the passes the samples allowed. assert_eq!( owners.get(&t), Some(&lp), "the threshold is computed in the body" ); assert!( a.taint.node(t) && a.taint.port(t, "value"), "a body literal is tainted" ); assert_no_sample(&a.log, &values, "log"); } #[test] fn the_program_scope() { // A code block binds a sample as `input` in the program's scope; a later code block // wired to nothing neural prints it. let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let get = g.node("VecGet", &[]); g.wire(src, "signal", get, "vec"); let write = g.node("CodeBlock", &[("src", "input + 0.0")]); g.wire(get, "item", write, "inputs"); let read = g.node("CodeBlockNode", &[("src", "println!(\"{}\", input);")]); let (p, a) = both(&g.g, &rt); assert!(a.taint.node(write)); assert!(a.taint.node(read), "the scope carried the sample"); assert!( p.console.contains("4323."), "the person's program printed it: {}", p.console ); let shown = a.taint.console_for_agent(); assert_no_sample(&shown, &values, "agent console"); assert_no_sample(&a.log, &values, "log"); } #[test] fn a_file_the_run_wrote() { let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let get = g.node("VecGet", &[]); g.wire(src, "signal", get, "vec"); let fmt = g.node("CodeBlock", &[("src", "format!(\"{}\", input)")]); g.wire(get, "item", fmt, "inputs"); let name = g.node("StringLiteral", &[("value", "leak.txt")]); let write = g.node("WriteFile", &[]); g.wire(name, "value", write, "path"); g.wire(fmt, "outputs", write, "contents"); let name2 = g.node("StringLiteral", &[("value", "leak.txt")]); let read = g.node("ReadFile", &[]); g.wire(name2, "value", read, "path"); // A widget that loads the file the run wrote: no wire ties it to the recording, and // it is not one of the primitives that share the run's state, so only the run's note // of which files it wrote from neural data taints it. let fmt_csv = g.node("CodeBlock", &[("src", "format!(\"v\\n{}\\n\", input)")]); g.wire(get, "item", fmt_csv, "inputs"); let csv_name = g.node("StringLiteral", &[("value", "leak.csv")]); let write_csv = g.node("WriteFile", &[]); g.wire(csv_name, "value", write_csv, "path"); g.wire(fmt_csv, "outputs", write_csv, "contents"); let load = g.node("data.csv.load", &[("path", "leak.csv")]); let (p, a) = both(&g.g, &rt); assert!(p.taint.node(write) && a.taint.node(write)); assert!( p.outputs.contains_key(&(load, "data".to_string())), "the loader ran after the write" ); assert_withheld(&p, &a, load, "data", &values, "data.csv.load"); if p.outputs.contains_key(&(read, "contents".to_string())) { assert!( p.taint.node(read), "reading what was written from neural data" ); assert_withheld(&p, &a, read, "contents", &values, "ReadFile"); } assert_no_sample(&a.log, &values, "log"); } #[test] fn a_held_wire_carries_taint_to_the_next_step() { let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let get = g.node("VecGet", &[]); g.wire(src, "signal", get, "vec"); let add = g.node("Add", &[]); g.wire(get, "item", add, "a"); let fb = g.g.add_wire(add, "result", add, "b", WireMode::Copy) .unwrap(); g.g.wires.get_mut(&fb).unwrap().feedback = true; // The person steps first, then an agent on the same runtime: what the person's step // held is real, and the agent's step reads it as zeros. for withhold in [false, true] { let mut ex = GraphExecutor::with_runtime(WidgetRegistry::with_builtins(), rt.clone()) .withhold(withhold); ex.step(&g.g).unwrap(); let key = flowrs_core::graph::held::key(&g.g.wires[&fb]); assert!(rt.is_neural("held", &key), "the held value is marked"); ex.step(&g.g).unwrap(); assert!(ex.taint().port(add, "result")); if withhold { assert_no_sample(&format!("{:?}", rt.held.get(&key)), &values, "held value"); } } rt.clear("held"); } #[test] fn an_in_place_write_back_reaches_the_bindings_other_readers() { // `v.push(sample)` then `v.len()` through the wire `v` came on: the length is read from // the binding the sample went into. let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let get = g.node("VecGet", &[]); g.wire(src, "signal", get, "vec"); let v = g.node("VecNew", &[]); let push = g.node("VecPush", &[]); g.wire(v, "vec", push, "vec"); g.wire(get, "item", push, "item"); let len = g.node("VecLen", &[]); g.wire(v, "vec", len, "vec"); let (p, a) = both(&g.g, &rt); assert!(p.taint.port(len, "length") && a.taint.port(len, "length")); assert_no_sample(&a.log, &values, "log"); } #[test] fn an_agent_run_computes_on_zeros_in_the_recorded_shape() { let (rt, values) = runtime(); let mut g = G::new(); let src = g.source(); let (p, a) = both(&g.g, &rt); let real = &p.outputs[&(src, "signal".into())]; let zero = &a.outputs[&(src, "signal".into())]; assert_eq!(format!("{:?}", taint::zeroed(real)), format!("{zero:?}")); assert_no_sample(&format!("{zero:?}"), &values, "zeroed signal"); // The same length: the shape is the recording's. let len = |v: &Value| match v { Value::Vec(x) => x.len(), _ => 0, }; assert_eq!(len(real), len(zero)); assert!(len(real) > 0); // Every kind of value a source can hand on comes out as zeros of its own shape. let ts = flowrs_schemas::Timeseries { sample_rate: 250.0, t0: 0.0, channels: vec!["Cz".into(), "Pz".into()], units: vec!["uV".into(), "uV".into()], data: vec![vec![values[0], values[1]], vec![values[2], values[3]]], events: vec![], epochs: vec![], }; let Value::Timeseries(z) = taint::zeroed(&Value::Timeseries(std::sync::Arc::new(ts))) else { panic!("a timeseries stays a timeseries"); }; assert_eq!(z.data, vec![vec![0.0, 0.0], vec![0.0, 0.0]]); assert_eq!((z.sample_rate, z.channels.len()), (250.0, 2)); assert!(matches!(taint::zeroed(&Value::Float(values[0])), Value::Float(x) if x == 0.0)); assert!(matches!(taint::zeroed(&Value::Int(7)), Value::Int(0))); // The header's ports are not tainted and keep their values. for port in ["rate", "channels", "units"] { if let Some(v) = p.outputs.get(&(src, port.into())) { assert!(!a.taint.port(src, port), "{port} is metadata"); assert_eq!( Some(format!("{v:?}")), a.outputs.get(&(src, port.into())).map(|x| format!("{x:?}")), "{port}" ); } } assert!(a.taint.sources.contains(&src)); assert!(a.taint.read.contains(&("live".into(), "bio".into()))); } #[test] fn a_graph_free_of_neural_data_is_untouched() { let (rt, _) = runtime(); let mut g = G::new(); let x = g.float(2.5); let y = g.float(4.0); let add = g.node("Add", &[]); g.wire(x, "value", add, "a"); g.wire(y, "value", add, "b"); let (p, a) = both(&g.g, &rt); assert!(!p.taint.any() && !a.taint.any()); assert!(matches!(a.outputs[&(add, "result".into())], Value::Float(x) if x == 6.5)); assert!(!a.log.contains(LOGGED_WITHHELD)); } #[test] fn a_file_is_neural_by_its_format_or_its_owners_word() { let rt = Runtime::new(); let mut edf = b"0 ".to_vec(); edf.resize(256, b' '); rt.put_file("x.bin", edf); assert!(rt.is_neural("file", "x.bin"), "EDF by content"); rt.put_file("notes.txt", b"hello".to_vec()); assert!(!rt.is_neural("file", "notes.txt")); rt.put_file_neural("export.csv", b"t,Cz\n0,1.5\n".to_vec()); assert!(rt.is_neural("file", "export.csv"), "the owner's word"); rt.put_file("r.dat", vec![1, 2, 3]); assert!( !rt.is_neural("file", "r.dat"), "a .dat alone is not a recording" ); rt.put_file("r.hea", b"r 1 250 3\n".to_vec()); assert!(rt.is_neural("file", "r.dat"), "with its WFDB header it is"); for name in [ "a.edf", "a.bdf", "a.xdf", "a.vhdr", "a.eeg", "a.nwb", "a.snirf", "a.set", ] { assert!(taint::neural_file(name, b""), "{name}"); } assert!(taint::neural_file("x", b"XDF:....")); assert!(taint::neural_file("x", b"\xffBIOSEMI...")); assert!(taint::neural_file( "x", b"Brain Vision Data Exchange Header File Version 1.0" )); // A replay applies the owner's word too. let log = rt.log(); let rt2 = Runtime::new(); let g = Graph::new(); let _ = flowrs_core::graph::runtime::replay(&log, &g, WidgetRegistry::with_builtins()); let _ = rt2; } #[test] fn the_cone_and_its_digest() { let mut g = G::new(); let src = g.source(); let get = g.node("VecGet", &[]); g.wire(src, "signal", get, "vec"); let other = g.float(1.0); let cone = taint::cone(&g.g, get); assert!(cone.contains(&src) && cone.contains(&get) && !cone.contains(&other)); let d1 = taint::cone_digest(&g.g, &cone); // Editing anything upstream changes it; editing elsewhere does not. g.g.nodes .get_mut(&other) .unwrap() .properties .insert("value".into(), "2".into()); assert_eq!(d1, taint::cone_digest(&g.g, &cone)); g.g.nodes .get_mut(&src) .unwrap() .properties .insert("channel".into(), "C4".into()); assert_ne!(d1, taint::cone_digest(&g.g, &cone)); } /// A reader of recordings that takes an input, as a future source may (`neuro.open` and /// `live.biosignal` take none today): its header ports stay open only while it is tainted /// for reading alone. Handed a value derived from neural data, its header could be /// anything that value chose, so it is tainted like every other output. struct HeaderSource; impl flowrs_core::graph::widget::Widget for HeaderSource { fn id(&self) -> String { "test.header_source".into() } fn name(&self) -> String { self.id() } fn category(&self) -> String { "Test".into() } fn inputs(&self) -> Vec { vec![flowrs_core::graph::widget::PortSpec { name: "pick".into(), semantic_type: "f64".into(), variadic: false, }] } fn outputs(&self) -> Vec { ["signal", "channels"] .into_iter() .map(|n| flowrs_core::graph::widget::PortSpec { name: n.into(), semantic_type: "f64".into(), variadic: false, }) .collect() } fn parameters(&self) -> Vec { vec![] } fn visual(&self) -> flowrs_core::graph::widget::WidgetVisual { flowrs_core::graph::widget::WidgetVisual { icon: String::new(), color: "#000".into(), min_size: (1, 1), resizable: false, } } fn codegen( &self, _: &flowrs_core::graph::widget::CodegenContext, _: HashMap, ) -> flowrs_core::graph::widget::WidgetCodegenResult { flowrs_core::graph::widget::WidgetCodegenResult { outputs: HashMap::new(), imports: vec![], init: None, process: String::new(), cleanup: None, state: vec![], } } fn reads_neural(&self) -> bool { true } fn metadata_ports(&self) -> &'static [&'static str] { &["channels"] } fn execute( &self, _: &flowrs_core::graph::widget::ExecContext, inputs: HashMap, ) -> HashMap { let pick = inputs.get("pick").cloned().unwrap_or(Value::Float(0.0)); HashMap::from([ ("signal".to_string(), Value::Float(1.0)), ("channels".to_string(), pick), ]) } } #[test] fn a_header_port_is_open_only_for_a_source_tainted_by_reading_alone() { let (rt, values) = runtime(); let mut registry = WidgetRegistry::with_builtins(); registry.register(Box::new(HeaderSource)); let mut g = G::new(); let alone = g.node("test.header_source", &[]); let src = g.source(); let get = g.node("VecGet", &[]); g.wire(src, "signal", get, "vec"); let fed = g.node("test.header_source", &[]); g.wire(get, "item", fed, "pick"); let mut ex = GraphExecutor::with_runtime(registry, rt.clone()).withhold(true); ex.run(&g.g).expect("runs"); let t = ex.taint().clone(); assert!( t.port(alone, "signal") && !t.port(alone, "channels"), "reading alone" ); assert!( t.port(fed, "signal") && t.port(fed, "channels"), "handed a sample, its header is the sample's" ); assert_no_sample(&format!("{:?}", t), &values, "taint"); }