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

105 lines
3.8 KiB
Rust

//! A widget with no inputs must produce its outputs from its parameters alone.
//!
//! `vec3.constant` did not. Its `value` parameter defaults to the empty string,
//! `parse_vec3("")` fails, and `execute` returned an empty map — while `codegen` fell back
//! to `[0.0; 3]`. So a freshly dropped node computed zeros when compiled and nothing when
//! run, and the T0 half was invisible: until recently a wire carrying nothing was dropped
//! from the probe list and looked exactly like a wire whose probe was switched off.
//!
//! It was found by asking which source widgets produce nothing: of 25, sixteen did, and
//! fifteen of those read a file, a device or target hardware and cannot answer in a test.
//! `vec3.constant` reads a string. That is what made it the odd one out rather than one of
//! a class, and this test is that question kept.
use flowrs_core::graph::widget::{ExecContext, builtin_registry};
use std::collections::HashMap;
/// Sources that legitimately produce nothing with default parameters, because their value
/// comes from somewhere this process cannot reach.
///
/// Every entry is a file, a capture device, a model registry or target hardware. A widget
/// belongs here only if it CANNOT answer without one — not because it happens not to.
const NEEDS_THE_WORLD: &[&str] = &[
// Files and captured media.
"audio.load",
"audio.record",
"data.csv.load",
"data.json.load",
"image.camera",
"image.load",
"microphone",
"video.open",
// Models, held in a side registry the browser populates.
"ml.onnx",
"neural.load_model",
// A file the browser's store published, read by name from the same kind of registry.
// With no file behind the name it produces nothing, because an absent file is not an
// empty one.
"record.file",
// A TDMS channel from the same store, read by name.
"data.tdms",
// A logic or waveform capture from the same store, read by name.
"capture.open",
// A published twin capture.
"twin.capture",
// A stream a live source published: a serial port, an RTT channel or sampled globals,
// from a board or a twin. Nothing published under the name is not an empty stream.
"live.serial",
"live.rtt",
"live.symbol",
"live.biosignal",
// On-target peripherals: these run on the MCU, not here.
"embedded.adc",
"embedded.gpio.digital_in",
"embedded.timer.interval",
"embedded.uart.read",
];
#[test]
fn every_parameter_only_source_produces_a_value() {
let registry = builtin_registry();
let mut silent = Vec::new();
for w in registry.all() {
let id = w.id();
if !w.inputs().is_empty()
|| w.outputs().is_empty()
|| NEEDS_THE_WORLD.contains(&id.as_str())
{
continue;
}
let params: HashMap<String, String> = w
.parameters()
.into_iter()
.map(|p| (p.name, p.default))
.collect();
if w.execute(&ExecContext::new(params), HashMap::new())
.is_empty()
{
silent.push(id);
}
}
silent.sort();
assert!(
silent.is_empty(),
"these widgets take no inputs, so their output is a function of their parameters \
alone — and with the parameters they declare by default they produced nothing:\n {}",
silent.join("\n "),
);
}
/// The exception list must not rot into a place to hide a bug.
#[test]
fn the_exception_list_only_names_widgets_that_exist_and_are_sources() {
let registry = builtin_registry();
for id in NEEDS_THE_WORLD {
let w = registry
.get(id)
.unwrap_or_else(|| panic!("`{id}` is excused but is not registered any more"));
assert!(
w.inputs().is_empty(),
"`{id}` has inputs, so it is not a source and does not need excusing",
);
}
}