Skip to main content

ad_plugins_rs/
std_arrays.rs

1// RTEMS-EXEC-MODEL-ALLOW(3): checked, not waived — all 3 ran and passed
2// on the exec backend (measured on this tree:
3// `EPICS_RS_BUILD_EXEC_BACKEND=thread cargo nextest run -p ad-plugins-rs
4// --all-features`, 556/556). ad-plugins-rs became a census subject when
5// its `build.rs` began deriving `tokio_backend`; nothing here builds a
6// CA server, and the reactor these obtain comes from `#[tokio::test]`
7// itself, which the backend does not remove.
8use std::sync::Arc;
9
10use ad_core_rs::ndarray::NDArray;
11use ad_core_rs::ndarray_pool::NDArrayPool;
12use ad_core_rs::plugin::runtime::{NDPluginProcess, PluginRuntimeHandle, ProcessResult};
13use ad_core_rs::plugin::wiring::WiringRegistry;
14use parking_lot::Mutex;
15
16/// Pure processing logic: stores the latest array and passes it through.
17pub struct StdArraysProcessor {
18    latest_data: Arc<Mutex<Option<Arc<NDArray>>>>,
19}
20
21impl StdArraysProcessor {
22    pub fn new() -> Self {
23        Self {
24            latest_data: Arc::new(Mutex::new(None)),
25        }
26    }
27
28    /// Get a cloneable handle to the latest array.
29    pub fn data_handle(&self) -> Arc<Mutex<Option<Arc<NDArray>>>> {
30        self.latest_data.clone()
31    }
32}
33
34impl Default for StdArraysProcessor {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl NDPluginProcess for StdArraysProcessor {
41    fn process_array(&self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
42        let out = Arc::new(array.clone());
43        *self.latest_data.lock() = Some(out.clone());
44        ProcessResult::arrays(vec![out])
45    }
46
47    fn plugin_type(&self) -> &str {
48        "NDPluginStdArrays"
49    }
50
51    /// C `NDPluginStdArrays.cpp:343` sets `NDArrayCallbacks = 0`: this plugin
52    /// serves pixel data via the StdArray waveforms, not downstream callbacks.
53    fn does_array_callbacks(&self) -> bool {
54        false
55    }
56
57    fn array_data_handle(&self) -> Option<Arc<Mutex<Option<Arc<NDArray>>>>> {
58        Some(self.latest_data.clone())
59    }
60}
61
62/// Create a StdArrays plugin runtime.
63pub fn create_std_arrays_runtime(
64    port_name: &str,
65    pool: Arc<NDArrayPool>,
66    ndarray_port: &str,
67    wiring: Arc<WiringRegistry>,
68) -> (
69    PluginRuntimeHandle,
70    Arc<Mutex<Option<Arc<NDArray>>>>,
71    std::thread::JoinHandle<()>,
72) {
73    let processor = StdArraysProcessor::new();
74    let data_handle = processor.data_handle();
75
76    let (handle, data_jh) = ad_core_rs::plugin::runtime::create_plugin_runtime(
77        port_name,
78        processor,
79        pool,
80        1, // LatestOnly semantics
81        ndarray_port,
82        wiring,
83    );
84
85    (handle, data_handle, data_jh)
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use ad_core_rs::ndarray::{NDDataType, NDDimension};
92
93    fn make_array(id: i32) -> Arc<NDArray> {
94        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
95        arr.unique_id = id;
96        Arc::new(arr)
97    }
98
99    /// Fence: `write_*_blocking` only queues param changes for the data
100    /// thread; the barrier ack proves they have been applied.
101    fn params_applied(handle: &PluginRuntimeHandle) {
102        assert!(
103            handle.wait_params_applied(std::time::Duration::from_secs(10)),
104            "data thread did not apply queued param changes"
105        );
106    }
107
108    fn wait_until(what: &str, mut cond: impl FnMut() -> bool) {
109        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
110        while !cond() {
111            assert!(
112                std::time::Instant::now() < deadline,
113                "timed out waiting for {what}"
114            );
115            std::thread::sleep(std::time::Duration::from_millis(2));
116        }
117    }
118
119    #[test]
120    fn test_processor_stores_and_passes_through() {
121        let proc = StdArraysProcessor::new();
122        let pool = NDArrayPool::new(1_000_000);
123
124        let arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
125        let result = proc.process_array(&arr, &pool);
126        assert_eq!(result.output_arrays.len(), 1);
127
128        let latest = proc.data_handle().lock().clone();
129        assert!(latest.is_some());
130    }
131
132    #[test]
133    fn test_std_arrays_runtime() {
134        let pool = Arc::new(NDArrayPool::new(1_000_000));
135        let wiring = Arc::new(WiringRegistry::new());
136        let (handle, data, _jh) = create_std_arrays_runtime("IMAGE1", pool, "", wiring);
137
138        // Plugins default to disabled — enable for test
139        handle
140            .port_runtime()
141            .port_handle()
142            .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
143            .unwrap();
144        params_applied(&handle);
145
146        let rt = tokio::runtime::Builder::new_current_thread()
147            .enable_all()
148            .build()
149            .unwrap();
150        rt.block_on(handle.array_sender().publish(make_array(42)));
151        wait_until("StdArrays to store the published array", || {
152            data.lock().as_ref().is_some_and(|a| a.unique_id == 42)
153        });
154    }
155
156    #[test]
157    fn test_std_arrays_initial_array_callbacks_off() {
158        // C NDPluginStdArrays.cpp:343 sets NDArrayCallbacks=0 in the
159        // constructor; the initial param a client reads must be 0, not 1.
160        let pool = Arc::new(NDArrayPool::new(1_000_000));
161        let wiring = Arc::new(WiringRegistry::new());
162        let (handle, _data, _jh) = create_std_arrays_runtime("IMAGE1", pool, "", wiring);
163
164        let val = handle
165            .port_runtime()
166            .port_handle()
167            .read_int32_blocking(handle.ndarray_params.array_callbacks, 0)
168            .unwrap();
169        assert_eq!(val, 0, "StdArrays initial NDArrayCallbacks must be 0");
170    }
171
172    #[test]
173    fn test_terminal_plugins_do_not_do_array_callbacks() {
174        // C disables NDArrayCallbacks in the constructor of `NDPluginStdArrays.cpp` (`:343`),
175        // `NDPluginAttribute.cpp` (`:203`), and `NDPluginFile.cpp` (`:948`, base of every
176        // file writer). Each Rust counterpart overrides `does_array_callbacks()` to false so
177        // the initial param reflects this.
178        use crate::attribute::AttributeProcessor;
179        use crate::file_hdf5::Hdf5FileProcessor;
180        use crate::file_jpeg::JpegFileProcessor;
181        use crate::file_magick::MagickFileProcessor;
182        use crate::file_netcdf::NetcdfFileProcessor;
183        use crate::file_nexus::NexusFileProcessor;
184        use crate::file_tiff::TiffFileProcessor;
185        use crate::passthrough::PassthroughProcessor;
186
187        assert!(!StdArraysProcessor::new().does_array_callbacks());
188        assert!(!AttributeProcessor::new("attr", 1).does_array_callbacks());
189        assert!(!Hdf5FileProcessor::new().does_array_callbacks());
190        assert!(!JpegFileProcessor::new(85).does_array_callbacks());
191        assert!(!TiffFileProcessor::new().does_array_callbacks());
192        assert!(!NetcdfFileProcessor::new().does_array_callbacks());
193        assert!(!NexusFileProcessor::new().does_array_callbacks());
194        assert!(!MagickFileProcessor::new().does_array_callbacks());
195
196        // A non-terminal plugin keeps the default: it does deliver downstream.
197        assert!(PassthroughProcessor::new("NDPluginProcess").does_array_callbacks());
198    }
199
200    #[test]
201    fn test_std_arrays_serves_waveform_with_callbacks_off() {
202        // C NDPluginStdArrays fires its typed-array waveform callbacks
203        // (NDPluginStdArrays.cpp:71-73) regardless of NDArrayCallbacks — and it
204        // defaults NDArrayCallbacks=0. So the ArrayData waveform on I/O Intr
205        // must still update with NDArrayCallbacks=0. This guards against the
206        // STD_ARRAY_DATA interrupt being gated by the downstream-delivery flag.
207        use asyn_rs::param::ParamValue;
208
209        let pool = Arc::new(NDArrayPool::new(1_000_000));
210        let wiring = Arc::new(WiringRegistry::new());
211        let (handle, _data, _jh) = create_std_arrays_runtime("IMAGE1", pool, "", wiring);
212
213        let port = handle.port_runtime().port_handle();
214        port.write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
215            .unwrap();
216        // Force NDArrayCallbacks=0 at runtime (StdArrays' C default) — the
217        // waveform output must still fire.
218        port.write_int32_blocking(handle.ndarray_params.array_callbacks, 0, 0)
219            .unwrap();
220        params_applied(&handle);
221
222        let mut rx = port.interrupts().subscribe_async();
223
224        let rt = tokio::runtime::Builder::new_current_thread()
225            .enable_all()
226            .build()
227            .unwrap();
228        rt.block_on(handle.array_sender().publish(make_array(7)));
229        // ArrayCounter==1 ⟹ the frame was processed and its param batch
230        // (including the waveform interrupts) flushed.
231        wait_until("StdArrays to process the frame", || {
232            port.read_int32_blocking(handle.ndarray_params.array_counter, 0)
233                .is_ok_and(|v| v == 1)
234        });
235
236        // make_array(7) is a 4-element UInt8 array → the STD_ARRAY_DATA
237        // interrupt carries it as an Int8Array; the dimensions interrupt carries
238        // an Int32Array. Finding an Int8Array interrupt proves the waveform was
239        // served despite NDArrayCallbacks=0.
240        let mut served = false;
241        loop {
242            match rx.try_recv() {
243                Ok(v) => {
244                    if matches!(v.value, ParamValue::Int8Array(_)) {
245                        served = true;
246                    }
247                }
248                Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) => continue,
249                Err(_) => break,
250            }
251        }
252        assert!(
253            served,
254            "StdArrays must serve STD_ARRAY_DATA with NDArrayCallbacks=0"
255        );
256    }
257
258    #[test]
259    fn test_std_arrays_throttled_frame_does_not_advance_array_counter() {
260        // C NDPluginStdArrays.cpp:202-211 decrements NDArrayCounter when its
261        // MaxByteRate throttle drops the waveform output, so a throttled frame
262        // leaves ArrayCounter unchanged (ImageJ etc. see no new data) while
263        // DroppedArrays advances.
264
265        let pool = Arc::new(NDArrayPool::new(1_000_000));
266        let wiring = Arc::new(WiringRegistry::new());
267        let (handle, _data, _jh) = create_std_arrays_runtime("IMAGE1", pool, "", wiring);
268        let port = handle.port_runtime().port_handle();
269        port.write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
270            .unwrap();
271        // MaxByteRate = 4 bytes/sec; make_array is a 4-byte UInt8 array. The
272        // bucket starts full at 4, so the first frame passes and the next
273        // (sent before a meaningful refill) is throttled.
274        port.write_float64_blocking(handle.plugin_params.max_byte_rate, 0, 4.0)
275            .unwrap();
276        params_applied(&handle);
277
278        let rt = tokio::runtime::Builder::new_current_thread()
279            .enable_all()
280            .build()
281            .unwrap();
282
283        rt.block_on(handle.array_sender().publish(make_array(1)));
284        wait_until("first frame to be served and counted", || {
285            port.read_int32_blocking(handle.ndarray_params.array_counter, 0)
286                .is_ok_and(|v| v == 1)
287        });
288        let counter_after_first = port
289            .read_int32_blocking(handle.ndarray_params.array_counter, 0)
290            .unwrap();
291
292        rt.block_on(handle.array_sender().publish(make_array(2)));
293        // The throttled frame leaves ArrayCounter unchanged, so wait on the
294        // observable it DOES advance: DroppedArrays.
295        wait_until("throttled frame to advance DroppedArrays", || {
296            port.read_int32_blocking(handle.plugin_params.dropped_output_arrays, 0)
297                .is_ok_and(|v| v == 1)
298        });
299        let counter_after_second = port
300            .read_int32_blocking(handle.ndarray_params.array_counter, 0)
301            .unwrap();
302        let dropped = port
303            .read_int32_blocking(handle.plugin_params.dropped_output_arrays, 0)
304            .unwrap();
305
306        assert_eq!(counter_after_first, 1, "first frame is served and counted");
307        assert_eq!(
308            counter_after_second, 1,
309            "throttled frame must NOT advance ArrayCounter"
310        );
311        assert_eq!(dropped, 1, "throttled frame advances DroppedArrays");
312    }
313}