//! 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 = 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", ); } }