716 lines
25 KiB
Rust
716 lines
25 KiB
Rust
//! 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<f64>) {
|
|
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<Runtime>, Vec<f64>) {
|
|
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<Runtime>) -> (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<Runtime>, 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<flowrs_core::graph::widget::PortSpec> {
|
|
vec![flowrs_core::graph::widget::PortSpec {
|
|
name: "pick".into(),
|
|
semantic_type: "f64".into(),
|
|
variadic: false,
|
|
}]
|
|
}
|
|
fn outputs(&self) -> Vec<flowrs_core::graph::widget::PortSpec> {
|
|
["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<flowrs_core::graph::widget::ParameterDef> {
|
|
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<String, String>,
|
|
) -> 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<String, Value>,
|
|
) -> HashMap<String, Value> {
|
|
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");
|
|
}
|