1use std::collections::VecDeque;
30
31const ATTRPLOT_DATA_EXPOSURE_PERIOD: std::time::Duration = std::time::Duration::from_secs(1);
34
35use ad_core_rs::ndarray::NDArray;
36use ad_core_rs::ndarray_pool::NDArrayPool;
37use ad_core_rs::plugin::runtime::{
38 NDPluginProcess, ParamChangeResult, ParamUpdate, PluginParamSnapshot, ProcessResult,
39};
40use parking_lot::Mutex;
41
42pub const ATTRPLOT_UID_INDEX: i32 = -1;
44pub const ATTRPLOT_NONE_INDEX: i32 = -2;
46pub const ATTRPLOT_UID_LABEL: &str = "UID";
48pub const ATTRPLOT_NONE_LABEL: &str = "None";
50
51pub struct AttrPlotProcessor {
53 state: Mutex<AttrPlotState>,
54}
55
56struct AttrPlotState {
59 n_attributes: usize,
61 n_data_blocks: usize,
63 cache_size: usize,
65 attributes: Vec<String>,
67 buffers: Vec<VecDeque<f64>>,
69 uid_buffer: VecDeque<f64>,
71 data_selections: Vec<i32>,
74 initialized: bool,
76 last_uid: i32,
78 last_expose: Option<std::time::Instant>,
81 params: AttrPlotParams,
83}
84
85#[derive(Default)]
87struct AttrPlotParams {
88 data: Option<usize>,
90 data_label: Option<usize>,
92 data_select: Option<usize>,
94 attribute: Option<usize>,
96 reset: Option<usize>,
98 npts: Option<usize>,
100}
101
102impl AttrPlotProcessor {
103 pub fn new(n_attributes: usize, cache_size: usize, n_data_blocks: usize) -> Self {
109 Self {
110 state: Mutex::new(AttrPlotState::new(n_attributes, cache_size, n_data_blocks)),
111 }
112 }
113
114 pub fn attributes(&self) -> Vec<String> {
116 self.state.lock().attributes.clone()
117 }
118
119 pub fn buffer(&self, index: usize) -> Option<VecDeque<f64>> {
121 self.state.lock().buffers.get(index).cloned()
122 }
123
124 pub fn uid_buffer(&self) -> VecDeque<f64> {
126 self.state.lock().uid_buffer.clone()
127 }
128
129 pub fn num_attributes(&self) -> usize {
131 self.state.lock().attributes.len()
132 }
133
134 pub fn num_data_blocks(&self) -> usize {
136 self.state.lock().n_data_blocks
137 }
138
139 pub fn find_attribute(&self, name: &str) -> Option<usize> {
141 self.state.lock().find_attribute(name)
142 }
143
144 pub fn data_select(&self, block: usize) -> Option<i32> {
146 self.state.lock().data_select(block)
147 }
148
149 pub fn set_data_select(&self, block: usize, value: i32) -> Result<(), &'static str> {
151 self.state.lock().set_data_select(block, value)
152 }
153
154 pub fn data_label(&self, block: usize) -> String {
156 self.state.lock().data_label(block)
157 }
158
159 pub fn reset(&self) {
161 self.state.lock().reset();
162 }
163}
164
165impl AttrPlotState {
166 fn new(n_attributes: usize, cache_size: usize, n_data_blocks: usize) -> Self {
167 Self {
168 n_attributes,
169 n_data_blocks,
170 cache_size,
171 attributes: Vec::new(),
172 buffers: Vec::new(),
173 uid_buffer: VecDeque::new(),
174 data_selections: vec![ATTRPLOT_NONE_INDEX; n_data_blocks],
175 initialized: false,
176 last_uid: -1,
177 last_expose: None,
178 params: AttrPlotParams::default(),
179 }
180 }
181
182 fn find_attribute(&self, name: &str) -> Option<usize> {
184 self.attributes.iter().position(|n| n == name)
185 }
186
187 fn data_select(&self, block: usize) -> Option<i32> {
189 self.data_selections.get(block).copied()
190 }
191
192 fn set_data_select(&mut self, block: usize, value: i32) -> Result<(), &'static str> {
197 if block >= self.n_data_blocks {
198 return Err("data block index out of range");
199 }
200 if value > 0 && (value as usize) >= self.attributes.len() {
203 return Err("attribute selection out of range");
204 }
205 self.data_selections[block] = value;
206 Ok(())
207 }
208
209 fn data_label(&self, block: usize) -> String {
211 match self.data_selections.get(block).copied() {
212 Some(ATTRPLOT_UID_INDEX) => ATTRPLOT_UID_LABEL.to_string(),
213 Some(sel) if sel >= 0 && (sel as usize) < self.attributes.len() => {
214 self.attributes[sel as usize].clone()
215 }
216 _ => ATTRPLOT_NONE_LABEL.to_string(),
217 }
218 }
219
220 fn reset(&mut self) {
222 self.initialized = false;
223 self.uid_buffer.clear();
224 for buf in &mut self.buffers {
225 buf.clear();
226 }
227 self.last_uid = -1;
228 }
229
230 fn push_capped(buf: &mut VecDeque<f64>, value: f64, cache_size: usize) {
232 if cache_size > 0 && buf.len() >= cache_size {
233 buf.pop_front();
234 }
235 buf.push_back(value);
236 }
237
238 fn rebuild_attributes(&mut self, array: &NDArray) {
244 let prior: Vec<Option<String>> = self
246 .data_selections
247 .iter()
248 .map(|&sel| match sel {
249 ATTRPLOT_UID_INDEX => Some(ATTRPLOT_UID_LABEL.to_string()),
250 s if s >= 0 && (s as usize) < self.attributes.len() => {
251 Some(self.attributes[s as usize].clone())
252 }
253 _ => None,
254 })
255 .collect();
256
257 let mut names: Vec<String> = Vec::new();
258 for attr in array.attributes.iter() {
259 if attr.value.as_f64().is_some() {
260 names.push(attr.name.clone());
261 }
262 }
263 names.sort();
264 names.truncate(self.n_attributes);
265
266 self.buffers = vec![VecDeque::new(); names.len()];
267 self.attributes = names;
268
269 for (i, want) in prior.into_iter().enumerate() {
271 self.data_selections[i] = match want {
272 Some(ref n) if n == ATTRPLOT_UID_LABEL => ATTRPLOT_UID_INDEX,
273 Some(n) => self
274 .attributes
275 .iter()
276 .position(|a| a == &n)
277 .map(|p| p as i32)
278 .unwrap_or(ATTRPLOT_NONE_INDEX),
279 None => ATTRPLOT_NONE_INDEX,
280 };
281 }
282 self.initialized = true;
283 }
284
285 fn push_data(&mut self, array: &NDArray) {
287 Self::push_capped(
288 &mut self.uid_buffer,
289 array.unique_id as f64,
290 self.cache_size,
291 );
292 for (i, name) in self.attributes.iter().enumerate() {
293 let value = array
294 .attributes
295 .get(name)
296 .and_then(|attr| attr.value.as_f64())
297 .unwrap_or(f64::NAN);
298 Self::push_capped(&mut self.buffers[i], value, self.cache_size);
299 }
300 }
301
302 fn block_waveform(&self, block: usize) -> Vec<f64> {
307 let selected = self
308 .data_selections
309 .get(block)
310 .copied()
311 .unwrap_or(ATTRPLOT_NONE_INDEX);
312 let src: Option<&VecDeque<f64>> = match selected {
313 ATTRPLOT_UID_INDEX => Some(&self.uid_buffer),
314 s if s >= 0 && (s as usize) < self.buffers.len() => Some(&self.buffers[s as usize]),
315 _ => None,
316 };
317 let size = self.uid_buffer.len();
318 let target = if self.cache_size > 0 {
320 self.cache_size
321 } else {
322 size
323 };
324 let mut out: Vec<f64> = match src {
325 Some(buf) => buf.iter().copied().collect(),
326 None => vec![f64::NAN; size],
327 };
328 let pad = out.last().copied().unwrap_or(f64::NAN);
330 if out.len() < target {
331 out.resize(target, pad);
332 } else {
333 out.truncate(target);
334 }
335 out
336 }
337
338 fn data_updates(&self) -> Vec<ParamUpdate> {
342 let mut updates = Vec::new();
343 if let Some(data) = self.params.data {
344 for block in 0..self.n_data_blocks {
345 updates.push(ParamUpdate::float64_array_addr(
346 data,
347 block as i32,
348 self.block_waveform(block),
349 ));
350 }
351 }
352 updates
353 }
354
355 fn expose_due(&mut self) -> bool {
361 let now = std::time::Instant::now();
362 let due = match self.last_expose {
363 None => true,
364 Some(prev) => now.duration_since(prev) >= ATTRPLOT_DATA_EXPOSURE_PERIOD,
365 };
366 if due {
367 self.last_expose = Some(now);
368 }
369 due
370 }
371
372 fn meta_updates(&self) -> Vec<ParamUpdate> {
376 let mut updates = Vec::new();
377 if let Some(label) = self.params.data_label {
378 for block in 0..self.n_data_blocks {
379 updates.push(ParamUpdate::octet_addr(
380 label,
381 block as i32,
382 self.data_label(block),
383 ));
384 }
385 }
386 if let Some(select) = self.params.data_select {
387 for block in 0..self.n_data_blocks {
388 updates.push(ParamUpdate::int32_addr(
389 select,
390 block as i32,
391 self.data_selections[block],
392 ));
393 }
394 }
395 if let Some(attribute) = self.params.attribute {
397 for i in 0..self.n_attributes {
398 let name = self.attributes.get(i).cloned().unwrap_or_default();
399 updates.push(ParamUpdate::octet_addr(attribute, i as i32, name));
400 }
401 }
402 if let Some(npts) = self.params.npts {
403 updates.push(ParamUpdate::int32(npts, self.uid_buffer.len() as i32));
404 }
405 updates
406 }
407
408 fn build_updates(&mut self) -> Vec<ParamUpdate> {
413 self.last_expose = Some(std::time::Instant::now());
414 let mut updates = self.data_updates();
415 updates.extend(self.meta_updates());
416 updates
417 }
418}
419
420impl NDPluginProcess for AttrPlotProcessor {
421 fn process_array(&self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
422 let mut state = self.state.lock();
423
424 if !state.uid_buffer.is_empty() && array.unique_id <= state.last_uid {
426 state.reset();
427 }
428 state.last_uid = array.unique_id;
429
430 if !state.initialized {
431 state.rebuild_attributes(array);
432 }
433 state.push_data(array);
434
435 let mut updates = state.meta_updates();
438 if state.expose_due() {
439 updates.extend(state.data_updates());
440 }
441 ProcessResult::sink(updates)
442 }
443
444 fn plugin_type(&self) -> &str {
445 "NDAttrPlot"
448 }
449
450 fn register_params(
451 &mut self,
452 base: &mut asyn_rs::port::PortDriverBase,
453 ) -> asyn_rs::error::AsynResult<()> {
454 use asyn_rs::param::ParamType;
455 let state = self.state.get_mut();
456 base.create_param("AP_Data", ParamType::Float64Array)?;
457 base.create_param("AP_DataLabel", ParamType::Octet)?;
458 base.create_param("AP_DataSelect", ParamType::Int32)?;
459 base.create_param("AP_Attribute", ParamType::Octet)?;
460 base.create_param("AP_Reset", ParamType::Int32)?;
461 base.create_param("AP_NPts", ParamType::Int32)?;
462
463 state.params.data = base.find_param("AP_Data");
464 state.params.data_label = base.find_param("AP_DataLabel");
465 state.params.data_select = base.find_param("AP_DataSelect");
466 state.params.attribute = base.find_param("AP_Attribute");
467 state.params.reset = base.find_param("AP_Reset");
468 state.params.npts = base.find_param("AP_NPts");
469 Ok(())
470 }
471
472 fn on_param_change(&self, reason: usize, params: &PluginParamSnapshot) -> ParamChangeResult {
473 let mut state = self.state.lock();
474 if Some(reason) == state.params.data_select {
475 let block = params.addr as usize;
476 let value = params.value.as_i32();
477 if state.set_data_select(block, value).is_ok() {
478 return ParamChangeResult::updates(state.build_updates());
480 }
481 } else if Some(reason) == state.params.reset {
482 state.reset();
485 return ParamChangeResult::updates(state.build_updates());
486 }
487 ParamChangeResult::updates(vec![])
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
495 use ad_core_rs::ndarray::{NDDataType, NDDimension};
496
497 fn make_array_with_attrs(uid: i32, attrs: &[(&str, f64)]) -> NDArray {
498 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
499 arr.unique_id = uid;
500 for (name, value) in attrs {
501 arr.attributes.add(NDAttribute::new_static(
502 *name,
503 String::new(),
504 NDAttrSource::Driver,
505 NDAttrValue::Float64(*value),
506 ));
507 }
508 arr
509 }
510
511 fn proc_with_params(n_attributes: usize, cache: usize, blocks: usize) -> AttrPlotProcessor {
514 let mut proc = AttrPlotProcessor::new(n_attributes, cache, blocks);
515 let mut base = asyn_rs::port::PortDriverBase::new(
516 "ATTRPLOT_ADP53",
517 blocks.max(n_attributes),
518 asyn_rs::port::PortFlags::default(),
519 );
520 proc.register_params(&mut base).unwrap();
521 proc
522 }
523
524 fn has_data_update(result: &ProcessResult, reason: usize) -> bool {
525 result
526 .param_updates
527 .iter()
528 .any(|u| matches!(u, ParamUpdate::Float64Array { reason: r, .. } if *r == reason))
529 }
530
531 #[test]
538 fn test_adp53_ap_data_is_rate_limited_to_the_exposure_period() {
539 let proc = proc_with_params(8, 100, 4);
540 let data_reason = proc.state.lock().params.data.expect("AP_Data registered");
541 let npts_reason = proc.state.lock().params.npts.expect("AP_NPts registered");
542 let pool = NDArrayPool::new(1_000_000);
543
544 let mut exposures = 0;
545 for uid in 1..=3 {
546 let arr = make_array_with_attrs(uid, &[("Temp", 25.0 + uid as f64)]);
547 let result = proc.process_array(&arr, &pool);
548 if has_data_update(&result, data_reason) {
549 exposures += 1;
550 }
551 assert!(
554 result.param_updates.iter().any(|u| matches!(
555 u,
556 ParamUpdate::Int32 { reason: r, .. } if *r == npts_reason
557 )),
558 "frame {uid} must still post AP_NPts",
559 );
560 }
561 assert_eq!(
562 exposures, 1,
563 "three frames inside one exposure period must post AP_Data once",
564 );
565 }
566
567 #[test]
571 fn test_adp53_data_select_write_exposes_immediately() {
572 let proc = proc_with_params(8, 100, 4);
573 let data_reason = proc.state.lock().params.data.expect("AP_Data registered");
574 let select_reason = proc
575 .state
576 .lock()
577 .params
578 .data_select
579 .expect("AP_DataSelect registered");
580 let pool = NDArrayPool::new(1_000_000);
581
582 let arr = make_array_with_attrs(1, &[("Temp", 25.0)]);
583 proc.process_array(&arr, &pool); let snapshot = PluginParamSnapshot {
586 enable_callbacks: true,
587 reason: select_reason,
588 addr: 0,
589 value: ad_core_rs::plugin::runtime::ParamChangeValue::Int32(0),
590 };
591 let result = proc.on_param_change(select_reason, &snapshot);
592 assert!(
593 result.param_updates.iter().any(|u| {
594 matches!(u, ParamUpdate::Float64Array { reason: r, .. } if *r == data_reason)
595 }),
596 "a DataSelect write must post AP_Data even inside the period",
597 );
598 }
599
600 #[test]
601 fn test_attribute_auto_detection() {
602 let proc = AttrPlotProcessor::new(8, 100, 4);
603 let pool = NDArrayPool::new(1_000_000);
604
605 let mut arr = make_array_with_attrs(1, &[("Temp", 25.0), ("Gain", 1.5)]);
606 arr.attributes.add(NDAttribute::new_static(
607 "Label",
608 String::new(),
609 NDAttrSource::Driver,
610 NDAttrValue::String("test".to_string()),
611 ));
612 proc.process_array(&arr, &pool);
613
614 assert_eq!(proc.num_attributes(), 2);
615 assert_eq!(proc.attributes()[0], "Gain");
616 assert_eq!(proc.attributes()[1], "Temp");
617 }
618
619 #[test]
620 fn test_n_attributes_caps_tracked_count() {
621 let proc = AttrPlotProcessor::new(2, 100, 1);
623 let pool = NDArrayPool::new(1_000_000);
624 let arr = make_array_with_attrs(1, &[("D", 4.0), ("A", 1.0), ("C", 3.0), ("B", 2.0)]);
625 proc.process_array(&arr, &pool);
626 assert_eq!(proc.num_attributes(), 2);
627 assert_eq!(proc.attributes(), vec!["A", "B"]);
628 }
629
630 #[test]
631 fn test_data_select_maps_block_to_attribute() {
632 let proc = AttrPlotProcessor::new(8, 100, 2);
634 let pool = NDArrayPool::new(1_000_000);
635 let arr = make_array_with_attrs(1, &[("A", 10.0), ("B", 20.0), ("C", 30.0)]);
636 proc.process_array(&arr, &pool);
637
638 proc.set_data_select(0, 1).unwrap(); proc.set_data_select(1, ATTRPLOT_UID_INDEX).unwrap();
640
641 assert_eq!(proc.data_label(0), "B");
642 assert_eq!(proc.data_label(1), ATTRPLOT_UID_LABEL);
643
644 let wf0 = proc.state.lock().block_waveform(0);
645 assert!((wf0[0] - 20.0).abs() < 1e-10, "block 0 plots attribute B");
646 let wf1 = proc.state.lock().block_waveform(1);
647 assert!((wf1[0] - 1.0).abs() < 1e-10, "block 1 plots UID");
648 }
649
650 #[test]
651 fn test_data_select_rejects_out_of_range() {
652 let proc = AttrPlotProcessor::new(8, 100, 2);
653 let pool = NDArrayPool::new(1_000_000);
654 let arr = make_array_with_attrs(1, &[("A", 1.0)]);
655 proc.process_array(&arr, &pool);
656
657 assert!(proc.set_data_select(0, 1).is_err());
659 assert!(proc.set_data_select(5, 0).is_err());
661 assert!(proc.set_data_select(0, 0).is_ok());
663 assert!(proc.set_data_select(1, ATTRPLOT_UID_INDEX).is_ok());
664 }
665
666 #[test]
667 fn test_data_select_zero_accepted_with_no_attributes() {
668 let proc = AttrPlotProcessor::new(8, 100, 2);
671 assert!(proc.attributes().is_empty());
672 assert!(proc.set_data_select(0, 0).is_ok());
673 assert_eq!(proc.state.lock().data_selections[0], 0);
674 }
675
676 #[test]
677 fn test_unbound_block_label_is_none() {
678 let proc = AttrPlotProcessor::new(8, 100, 3);
679 let pool = NDArrayPool::new(1_000_000);
680 let arr = make_array_with_attrs(1, &[("A", 1.0)]);
681 proc.process_array(&arr, &pool);
682 assert_eq!(proc.data_label(2), ATTRPLOT_NONE_LABEL);
684 assert_eq!(proc.data_select(2), Some(ATTRPLOT_NONE_INDEX));
685 }
686
687 #[test]
688 fn test_npts_tracks_point_count() {
689 let proc = AttrPlotProcessor::new(8, 100, 1);
690 let pool = NDArrayPool::new(1_000_000);
691 for i in 1..=4 {
692 let arr = make_array_with_attrs(i, &[("X", i as f64)]);
693 proc.process_array(&arr, &pool);
694 }
695 assert_eq!(proc.uid_buffer().len(), 4);
696 }
697
698 #[test]
699 fn test_waveform_padded_to_cache_size() {
700 let proc = AttrPlotProcessor::new(8, 6, 1);
703 let pool = NDArrayPool::new(1_000_000);
704 for i in 1..=3 {
705 let arr = make_array_with_attrs(i, &[("X", i as f64 * 10.0)]);
706 proc.process_array(&arr, &pool);
707 }
708 proc.set_data_select(0, 0).unwrap();
709 let wf = proc.state.lock().block_waveform(0);
710 assert_eq!(wf.len(), 6);
711 assert!((wf[0] - 10.0).abs() < 1e-10);
712 assert!((wf[2] - 30.0).abs() < 1e-10);
713 assert!((wf[3] - 30.0).abs() < 1e-10);
715 assert!((wf[5] - 30.0).abs() < 1e-10);
716 }
717
718 #[test]
719 fn test_data_select_preserved_across_rebuild() {
720 let proc = AttrPlotProcessor::new(8, 100, 1);
723 let pool = NDArrayPool::new(1_000_000);
724 let arr = make_array_with_attrs(5, &[("Gain", 1.0), ("Temp", 25.0)]);
725 proc.process_array(&arr, &pool);
726 let temp_idx = proc.find_attribute("Temp").unwrap() as i32;
727 proc.set_data_select(0, temp_idx).unwrap();
728
729 let arr2 = make_array_with_attrs(1, &[("Gain", 2.0), ("Temp", 99.0)]);
731 proc.process_array(&arr2, &pool);
732 assert_eq!(proc.data_label(0), "Temp");
733 let wf = proc.state.lock().block_waveform(0);
734 assert!((wf[0] - 99.0).abs() < 1e-10);
735 }
736
737 #[test]
738 fn test_value_tracking() {
739 let proc = AttrPlotProcessor::new(8, 100, 1);
740 let pool = NDArrayPool::new(1_000_000);
741 for i in 1..=5 {
742 let arr = make_array_with_attrs(i, &[("Value", i as f64 * 10.0)]);
743 proc.process_array(&arr, &pool);
744 }
745 let idx = proc.find_attribute("Value").unwrap();
746 let buf = proc.buffer(idx).unwrap();
747 assert_eq!(buf.len(), 5);
748 assert!((buf[0] - 10.0).abs() < 1e-10);
749 assert!((buf[4] - 50.0).abs() < 1e-10);
750 }
751
752 #[test]
753 fn test_circular_buffer_cache_size() {
754 let proc = AttrPlotProcessor::new(8, 3, 1);
755 let pool = NDArrayPool::new(1_000_000);
756 for i in 1..=5 {
757 let arr = make_array_with_attrs(i, &[("Val", i as f64)]);
758 proc.process_array(&arr, &pool);
759 }
760 let idx = proc.find_attribute("Val").unwrap();
761 let buf = proc.buffer(idx).unwrap();
762 assert_eq!(buf.len(), 3);
763 assert!((buf[0] - 3.0).abs() < 1e-10);
764 assert!((buf[2] - 5.0).abs() < 1e-10);
765 }
766
767 #[test]
768 fn test_uid_decrease_resets_buffers() {
769 let proc = AttrPlotProcessor::new(8, 100, 1);
770 let pool = NDArrayPool::new(1_000_000);
771 for i in 1..=5 {
772 let arr = make_array_with_attrs(i, &[("X", i as f64)]);
773 proc.process_array(&arr, &pool);
774 }
775 let idx = proc.find_attribute("X").unwrap();
776 assert_eq!(proc.buffer(idx).unwrap().len(), 5);
777
778 let arr = make_array_with_attrs(1, &[("X", 100.0)]);
779 proc.process_array(&arr, &pool);
780 let buf = proc.buffer(idx).unwrap();
781 assert_eq!(buf.len(), 1);
782 assert!((buf[0] - 100.0).abs() < 1e-10);
783 }
784
785 #[test]
786 fn test_missing_attribute_uses_nan() {
787 let proc = AttrPlotProcessor::new(8, 100, 1);
788 let pool = NDArrayPool::new(1_000_000);
789 let arr1 = make_array_with_attrs(1, &[("Temp", 25.0)]);
790 proc.process_array(&arr1, &pool);
791
792 let mut arr2 = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
793 arr2.unique_id = 2;
794 proc.process_array(&arr2, &pool);
795
796 let idx = proc.find_attribute("Temp").unwrap();
797 let buf = proc.buffer(idx).unwrap();
798 assert_eq!(buf.len(), 2);
799 assert!((buf[0] - 25.0).abs() < 1e-10);
800 assert!(buf[1].is_nan());
801 }
802
803 #[test]
804 fn test_manual_reset() {
805 let proc = AttrPlotProcessor::new(8, 100, 1);
806 let pool = NDArrayPool::new(1_000_000);
807 let arr = make_array_with_attrs(5, &[("A", 1.0), ("B", 2.0)]);
808 proc.process_array(&arr, &pool);
809 assert_eq!(proc.num_attributes(), 2);
810
811 proc.reset();
812 let arr2 = make_array_with_attrs(1, &[("C", 3.0)]);
814 proc.process_array(&arr2, &pool);
815 assert_eq!(proc.num_attributes(), 1);
816 assert_eq!(proc.attributes()[0], "C");
817 }
818
819 #[test]
820 fn test_unlimited_buffer() {
821 let proc = AttrPlotProcessor::new(8, 0, 1);
822 let pool = NDArrayPool::new(1_000_000);
823 for i in 1..=100 {
824 let arr = make_array_with_attrs(i, &[("X", i as f64)]);
825 proc.process_array(&arr, &pool);
826 }
827 let idx = proc.find_attribute("X").unwrap();
828 assert_eq!(proc.buffer(idx).unwrap().len(), 100);
829 }
830
831 #[test]
832 fn test_plugin_type() {
833 let proc = AttrPlotProcessor::new(8, 100, 1);
835 assert_eq!(proc.plugin_type(), "NDAttrPlot");
836 }
837}