1use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use tokio::sync::{RwLock, mpsc};
10use tracing::debug;
11
12use std::future::Future;
13use std::pin::Pin;
14
15use spvirit_codec::spvd_decode::{DecodedValue, FieldDesc, FieldType, StructureDesc, TypeCode};
16use spvirit_types::{NtPayload, ScalarArrayValue, ScalarValue};
17
18use crate::apply::{
19 apply_alarm_update, apply_control_update, apply_display_update, apply_scalar_array_put,
20 apply_value_update,
21};
22use crate::monitor::MonitorRegistry;
23use crate::pvstore::{PvInfo, Source};
24use crate::types::{RecordData, RecordInstance};
25
26pub type OnPutCallback = Arc<dyn Fn(&str, &DecodedValue) + Send + Sync>;
28
29pub type ScanCallback = Arc<dyn Fn(&str) -> ScalarValue + Send + Sync>;
31
32pub type LinkCallback = Arc<dyn Fn(&[ScalarValue]) -> ScalarValue + Send + Sync>;
34
35pub(crate) type PutValidator = Arc<dyn Fn(&str, &DecodedValue) -> Result<(), String> + Send + Sync>;
37
38pub(crate) struct LinkDef {
40 pub output: String,
41 pub inputs: Vec<String>,
42 pub compute: LinkCallback,
43}
44
45struct PvEntry {
46 record: RecordInstance,
47 subscribers: Vec<mpsc::Sender<NtPayload>>,
48 last_posted: Option<f64>,
52}
53
54pub struct SimplePvStore {
56 pvs: RwLock<HashMap<String, PvEntry>>,
57 on_put: HashMap<String, OnPutCallback>,
58 links: Vec<LinkDef>,
59 compute_alarms: bool,
60 registry: RwLock<Option<Arc<MonitorRegistry>>>,
61 validators: RwLock<HashMap<String, PutValidator>>,
62}
63
64impl SimplePvStore {
65 pub(crate) fn new(
66 records: HashMap<String, RecordInstance>,
67 on_put: HashMap<String, OnPutCallback>,
68 links: Vec<LinkDef>,
69 compute_alarms: bool,
70 ) -> Self {
71 let pvs = records
72 .into_iter()
73 .map(|(name, mut record)| {
74 record.stamp_missing_timestamps();
75 let last_posted = initial_posted(&record);
76 (
77 name,
78 PvEntry {
79 record,
80 subscribers: Vec::new(),
81 last_posted,
82 },
83 )
84 })
85 .collect();
86 Self {
87 pvs: RwLock::new(pvs),
88 on_put,
89 links,
90 compute_alarms,
91 registry: RwLock::new(None),
92 validators: RwLock::new(HashMap::new()),
93 }
94 }
95
96 pub async fn set_registry(&self, registry: Arc<MonitorRegistry>) {
99 *self.registry.write().await = Some(registry);
100 }
101
102 pub(crate) async fn set_validator(&self, name: String, v: PutValidator) {
104 self.validators.write().await.insert(name, v);
105 }
106
107 pub async fn insert(&self, name: String, mut record: RecordInstance) {
109 record.stamp_missing_timestamps();
110 let mut pvs = self.pvs.write().await;
111 let last_posted = initial_posted(&record);
112 pvs.insert(
113 name,
114 PvEntry {
115 record,
116 subscribers: Vec::new(),
117 last_posted,
118 },
119 );
120 }
121
122 pub async fn remove(&self, name: &str) -> bool {
127 self.pvs.write().await.remove(name).is_some()
128 }
129
130 pub async fn get_value(&self, name: &str) -> Option<ScalarValue> {
132 let pvs = self.pvs.read().await;
133 pvs.get(name).map(|e| e.record.current_value())
134 }
135
136 pub async fn get_record(&self, name: &str) -> Option<RecordInstance> {
141 let pvs = self.pvs.read().await;
142 pvs.get(name).map(|e| e.record.clone())
143 }
144
145 pub async fn get_nt(&self, name: &str) -> Option<NtPayload> {
147 let pvs = self.pvs.read().await;
148 pvs.get(name).map(|e| e.record.to_ntpayload())
149 }
150
151 pub async fn set_value(&self, name: &str, value: ScalarValue) -> bool {
153 if self.set_value_inner(name, value).await {
154 self.evaluate_links(name).await;
155 true
156 } else {
157 false
158 }
159 }
160
161 pub async fn set_array_value(&self, name: &str, value: ScalarArrayValue) -> bool {
163 if self.set_array_value_inner(name, value).await {
164 self.evaluate_links(name).await;
165 true
166 } else {
167 false
168 }
169 }
170
171 pub async fn put_nt(&self, name: &str, payload: NtPayload) -> bool {
173 if self.put_nt_inner(name, payload).await {
174 self.evaluate_links(name).await;
175 true
176 } else {
177 false
178 }
179 }
180
181 pub async fn set_alarm(&self, name: &str, severity: i32, status: i32, message: &str) -> bool {
189 let payload = {
190 let mut pvs = self.pvs.write().await;
191 let Some(entry) = pvs.get_mut(name) else {
192 return false;
193 };
194 let alarm = if let Some(nt) = entry.record.nt_scalar_mut() {
195 (
196 &mut nt.alarm_severity,
197 &mut nt.alarm_status,
198 &mut nt.alarm_message,
199 )
200 } else {
201 match &mut entry.record.data {
202 RecordData::NtEnum { nt, .. } => (
203 &mut nt.alarm.severity,
204 &mut nt.alarm.status,
205 &mut nt.alarm.message,
206 ),
207 RecordData::Waveform { nt, .. }
208 | RecordData::Aai { nt, .. }
209 | RecordData::Aao { nt, .. }
210 | RecordData::SubArray { nt, .. } => (
211 &mut nt.alarm.severity,
212 &mut nt.alarm.status,
213 &mut nt.alarm.message,
214 ),
215 _ => return false,
216 }
217 };
218 let (sev, sta, msg) = alarm;
219 let changed = *sev != severity || *sta != status || msg.as_str() != message;
220 if !changed {
221 return false;
222 }
223 *sev = severity;
224 *sta = status;
225 *msg = message.to_string();
226
227 let payload = entry.record.to_ntpayload();
228 entry
229 .subscribers
230 .retain(|tx| tx.try_send(payload.clone()).is_ok());
231 payload
232 };
233
234 let reg = self.registry.read().await;
235 if let Some(registry) = reg.as_ref() {
236 registry.notify_monitors(name, &payload).await;
237 }
238 true
239 }
240
241 async fn set_value_inner(&self, name: &str, value: ScalarValue) -> bool {
244 let payload = {
245 let mut pvs = self.pvs.write().await;
246 if let Some(entry) = pvs.get_mut(name) {
247 let prev_severity = entry.record.to_ntscalar().alarm_severity;
248 let changed = entry.record.set_scalar_value(value, self.compute_alarms);
249 if changed {
250 if !should_post_update(entry, prev_severity) {
251 return true;
255 }
256 let payload = entry.record.to_ntpayload();
257 entry
258 .subscribers
259 .retain(|tx| tx.try_send(payload.clone()).is_ok());
260 Some(payload)
261 } else {
262 None
263 }
264 } else {
265 return false;
266 }
267 };
268
269 if let Some(payload) = payload {
270 let reg = self.registry.read().await;
272 if let Some(registry) = reg.as_ref() {
273 registry.notify_monitors(name, &payload).await;
274 }
275 true
276 } else {
277 false
278 }
279 }
280
281 async fn set_array_value_inner(&self, name: &str, value: ScalarArrayValue) -> bool {
284 let payload = {
285 let mut pvs = self.pvs.write().await;
286 if let Some(entry) = pvs.get_mut(name) {
287 let changed = entry.record.set_array_value(value);
288 if changed {
289 let payload = entry.record.to_ntpayload();
290 entry
291 .subscribers
292 .retain(|tx| tx.try_send(payload.clone()).is_ok());
293 Some(payload)
294 } else {
295 None
296 }
297 } else {
298 return false;
299 }
300 };
301
302 if let Some(payload) = payload {
303 let reg = self.registry.read().await;
305 if let Some(registry) = reg.as_ref() {
306 registry.notify_monitors(name, &payload).await;
307 }
308 true
309 } else {
310 false
311 }
312 }
313
314 async fn put_nt_inner(&self, name: &str, payload: NtPayload) -> bool {
317 let payload = {
318 let mut pvs = self.pvs.write().await;
319 if let Some(entry) = pvs.get_mut(name) {
320 let changed = entry.record.set_nt_payload(payload);
321 if changed {
322 let payload = entry.record.to_ntpayload();
323 entry
324 .subscribers
325 .retain(|tx| tx.try_send(payload.clone()).is_ok());
326 Some(payload)
327 } else {
328 None
329 }
330 } else {
331 return false;
332 }
333 };
334
335 if let Some(payload) = payload {
336 let reg = self.registry.read().await;
338 if let Some(registry) = reg.as_ref() {
339 registry.notify_monitors(name, &payload).await;
340 }
341 true
342 } else {
343 false
344 }
345 }
346
347 async fn evaluate_links(&self, changed_pv: &str) {
350 if self.links.is_empty() {
351 return;
352 }
353 let mut queue = vec![changed_pv.to_string()];
354 let mut visited = HashSet::new();
355
356 while let Some(pv) = queue.pop() {
357 if !visited.insert(pv.clone()) {
358 debug!("Circular link detected for PV '{}', skipping", pv);
359 continue;
360 }
361 for link in &self.links {
362 if !link.inputs.iter().any(|i| i == &pv) {
363 continue;
364 }
365 let values = {
367 let pvs = self.pvs.read().await;
368 link.inputs
369 .iter()
370 .map(|n| {
371 pvs.get(n)
372 .map(|e| e.record.current_value())
373 .unwrap_or(ScalarValue::F64(0.0))
374 })
375 .collect::<Vec<_>>()
376 };
377 let new_val = (link.compute)(&values);
378 if self.set_value_inner(&link.output, new_val).await {
379 queue.push(link.output.clone());
380 }
381 }
382 }
383 }
384
385 pub async fn pv_names(&self) -> Vec<String> {
387 let pvs = self.pvs.read().await;
388 pvs.keys().cloned().collect()
389 }
390}
391
392impl Source for SimplePvStore {
393 fn claim(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<PvInfo>> + Send + '_>> {
394 let name = name.to_string();
395 Box::pin(async move {
396 let pvs = self.pvs.read().await;
397 let entry = pvs.get(&name)?;
398 let descriptor = descriptor_for_payload(&entry.record.to_ntpayload());
399 Some(PvInfo {
400 descriptor,
401 writable: entry.record.writable(),
402 })
403 })
404 }
405
406 fn get(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<NtPayload>> + Send + '_>> {
407 let name = name.to_string();
408 Box::pin(async move {
409 let pvs = self.pvs.read().await;
410 pvs.get(&name).map(|e| e.record.to_ntpayload())
411 })
412 }
413
414 fn put(
415 &self,
416 name: &str,
417 value: &DecodedValue,
418 ) -> Pin<Box<dyn Future<Output = Result<Vec<(String, NtPayload)>, String>> + Send + '_>> {
419 let name = name.to_string();
420 let value = value.clone();
421 Box::pin(async move {
422 let validator = {
427 let guard = self.validators.read().await;
428 guard.get(&name).cloned()
429 };
430 if let Some(v) = validator {
431 v(&name, &value)?;
432 }
433
434 let result = {
435 let mut pvs = self.pvs.write().await;
436 let entry = pvs
437 .get_mut(&name)
438 .ok_or_else(|| format!("PV '{}' not found", name))?;
439
440 if !entry.record.writable() {
441 return Err(format!("PV '{}' is not writable", name));
442 }
443
444 let prev_severity = entry.record.to_ntscalar().alarm_severity;
445 let changed = apply_put_to_record(&mut entry.record, &value, self.compute_alarms);
446 if !changed {
447 return Ok(vec![]);
448 }
449
450 if should_post_update(entry, prev_severity) {
451 let payload = entry.record.to_ntpayload();
452 entry
453 .subscribers
454 .retain(|tx| tx.try_send(payload.clone()).is_ok());
455 Some((name.clone(), payload))
456 } else {
457 None
459 }
460 }; if let Some(cb) = self.on_put.get(&name) {
464 let cb = cb.clone();
465 let n = name.clone();
466 let v = value.clone();
467 tokio::spawn(async move { cb(&n, &v) });
468 }
469
470 self.evaluate_links(&name).await;
472
473 Ok(result.into_iter().collect())
474 })
475 }
476
477 fn subscribe(
478 &self,
479 name: &str,
480 ) -> Pin<Box<dyn Future<Output = Option<mpsc::Receiver<NtPayload>>> + Send + '_>> {
481 let name = name.to_string();
482 Box::pin(async move {
483 let mut pvs = self.pvs.write().await;
484 let entry = pvs.get_mut(&name)?;
485 let (tx, rx) = mpsc::channel(64);
486 entry.subscribers.push(tx);
487 Some(rx)
488 })
489 }
490
491 fn names(&self) -> Pin<Box<dyn Future<Output = Vec<String>> + Send + '_>> {
492 Box::pin(async move {
493 let pvs = self.pvs.read().await;
494 pvs.keys().cloned().collect()
495 })
496 }
497}
498
499fn scalar_as_f64(v: &ScalarValue) -> Option<f64> {
503 Some(match v {
504 ScalarValue::I8(x) => *x as f64,
505 ScalarValue::I16(x) => *x as f64,
506 ScalarValue::I32(x) => *x as f64,
507 ScalarValue::I64(x) => *x as f64,
508 ScalarValue::U8(x) => *x as f64,
509 ScalarValue::U16(x) => *x as f64,
510 ScalarValue::U32(x) => *x as f64,
511 ScalarValue::U64(x) => *x as f64,
512 ScalarValue::F32(x) => *x as f64,
513 ScalarValue::F64(x) => *x,
514 ScalarValue::Bool(_) | ScalarValue::Str(_) => return None,
515 })
516}
517
518fn initial_posted(record: &RecordInstance) -> Option<f64> {
522 match record.to_ntpayload() {
523 NtPayload::Scalar(nt) => scalar_as_f64(&nt.value),
524 _ => None,
525 }
526}
527
528fn should_post_update(entry: &mut PvEntry, prev_severity: i32) -> bool {
536 let new_f = match entry.record.to_ntpayload() {
537 NtPayload::Scalar(nt) => match scalar_as_f64(&nt.value) {
538 Some(f) => f,
539 None => return true,
540 },
541 _ => return true,
542 };
543 let mdel = crate::record_fields::mdel_of(&entry.record);
544 let severity_changed = entry.record.to_ntscalar().alarm_severity != prev_severity;
545 let within_deadband = mdel > 0.0
546 && !severity_changed
547 && entry
548 .last_posted
549 .is_some_and(|last| (new_f - last).abs() < mdel);
550 if within_deadband {
551 return false;
552 }
553 entry.last_posted = Some(new_f);
554 true
555}
556
557fn apply_put_to_record(
559 record: &mut RecordInstance,
560 value: &DecodedValue,
561 compute_alarms: bool,
562) -> bool {
563 let fields = match value {
564 DecodedValue::Structure(f) => f,
565 other => {
566 return apply_put_to_record(
568 record,
569 &DecodedValue::Structure(vec![("value".to_string(), other.clone())]),
570 compute_alarms,
571 );
572 }
573 };
574
575 let mut changed = false;
576
577 match &mut record.data {
578 RecordData::Ai { nt, .. }
579 | RecordData::Ao { nt, .. }
580 | RecordData::Bi { nt, .. }
581 | RecordData::Bo { nt, .. }
582 | RecordData::StringIn { nt, .. }
583 | RecordData::StringOut { nt, .. } => {
584 for (name, val) in fields {
585 match name.as_str() {
586 "value" => {
587 changed |= apply_value_update(nt, val, compute_alarms);
588 }
589 "alarm" => {
590 changed |= apply_alarm_update(nt, val);
591 }
592 "display" => {
593 changed |= apply_display_update(nt, val);
594 }
595 "control" => {
596 changed |= apply_control_update(nt, val);
597 }
598 _ => {}
599 }
600 }
601 }
602 RecordData::Waveform { nt, nord, .. }
603 | RecordData::Aai { nt, nord, .. }
604 | RecordData::Aao { nt, nord, .. }
605 | RecordData::SubArray { nt, nord, .. } => {
606 changed = apply_scalar_array_put(nt, nord, value);
607 }
608 RecordData::NtTable { .. } | RecordData::NtNdArray { .. } => {
609 debug!("PUT to NtTable/NtNdArray not yet supported in SimplePvStore");
611 }
612 RecordData::NtEnum { nt, .. } => {
613 for (name, val) in fields {
615 if name == "value" {
616 let idx = match val {
617 DecodedValue::Int32(v) => Some(*v),
618 DecodedValue::Int64(v) => Some(*v as i32),
619 DecodedValue::Int16(v) => Some(*v as i32),
620 DecodedValue::Int8(v) => Some(*v as i32),
621 DecodedValue::Float64(v) => Some(*v as i32),
622 _ => None,
623 };
624 if let Some(idx) = idx {
625 if idx < 0 || (idx as usize) >= nt.choices.len() {
626 } else if nt.index != idx {
628 nt.index = idx;
629 changed = true;
630 }
631 }
632 }
633 }
634 }
635 RecordData::Generic { .. } => {
636 debug!("PUT to Generic not yet supported in SimplePvStore");
637 }
638 }
639
640 changed
641}
642
643pub fn descriptor_for_payload(payload: &NtPayload) -> StructureDesc {
646 match payload {
647 NtPayload::Scalar(nt) => nt_scalar_desc(&nt.value),
648 NtPayload::ScalarArray(arr) => nt_scalar_array_desc(&arr.value),
649 _ => spvirit_codec::spvd_encode::nt_payload_desc(payload),
654 }
655}
656
657fn value_type_code(sv: &ScalarValue) -> TypeCode {
658 match sv {
659 ScalarValue::Bool(_) => TypeCode::Boolean,
660 ScalarValue::I8(_) => TypeCode::Int8,
661 ScalarValue::I16(_) => TypeCode::Int16,
662 ScalarValue::I32(_) => TypeCode::Int32,
663 ScalarValue::I64(_) => TypeCode::Int64,
664 ScalarValue::U8(_) => TypeCode::UInt8,
665 ScalarValue::U16(_) => TypeCode::UInt16,
666 ScalarValue::U32(_) => TypeCode::UInt32,
667 ScalarValue::U64(_) => TypeCode::UInt64,
668 ScalarValue::F32(_) => TypeCode::Float32,
669 ScalarValue::F64(_) => TypeCode::Float64,
670 ScalarValue::Str(_) => TypeCode::String,
671 }
672}
673
674fn array_type_code(sav: &ScalarArrayValue) -> TypeCode {
675 match sav {
676 ScalarArrayValue::Bool(_) => TypeCode::Boolean,
677 ScalarArrayValue::I8(_) => TypeCode::Int8,
678 ScalarArrayValue::I16(_) => TypeCode::Int16,
679 ScalarArrayValue::I32(_) => TypeCode::Int32,
680 ScalarArrayValue::I64(_) => TypeCode::Int64,
681 ScalarArrayValue::U8(_) => TypeCode::UInt8,
682 ScalarArrayValue::U16(_) => TypeCode::UInt16,
683 ScalarArrayValue::U32(_) => TypeCode::UInt32,
684 ScalarArrayValue::U64(_) => TypeCode::UInt64,
685 ScalarArrayValue::F32(_) => TypeCode::Float32,
686 ScalarArrayValue::F64(_) => TypeCode::Float64,
687 ScalarArrayValue::Str(_) => TypeCode::String,
688 }
689}
690
691fn nt_scalar_desc(sv: &ScalarValue) -> StructureDesc {
692 let tc = value_type_code(sv);
693 StructureDesc {
694 struct_id: Some("epics:nt/NTScalar:1.0".to_string()),
695 fields: vec![
696 FieldDesc {
697 name: "value".to_string(),
698 field_type: FieldType::Scalar(tc),
699 },
700 FieldDesc {
701 name: "alarm".to_string(),
702 field_type: FieldType::Structure(alarm_desc()),
703 },
704 FieldDesc {
705 name: "timeStamp".to_string(),
706 field_type: FieldType::Structure(timestamp_desc()),
707 },
708 FieldDesc {
709 name: "display".to_string(),
710 field_type: FieldType::Structure(display_desc()),
711 },
712 FieldDesc {
713 name: "control".to_string(),
714 field_type: FieldType::Structure(control_desc()),
715 },
716 FieldDesc {
717 name: "valueAlarm".to_string(),
718 field_type: FieldType::Structure(value_alarm_desc()),
719 },
720 ],
721 }
722}
723
724fn nt_scalar_array_desc(sav: &ScalarArrayValue) -> StructureDesc {
725 let tc = array_type_code(sav);
726 StructureDesc {
727 struct_id: Some("epics:nt/NTScalarArray:1.0".to_string()),
728 fields: vec![
729 FieldDesc {
730 name: "value".to_string(),
731 field_type: FieldType::ScalarArray(tc),
732 },
733 FieldDesc {
734 name: "alarm".to_string(),
735 field_type: FieldType::Structure(alarm_desc()),
736 },
737 FieldDesc {
738 name: "timeStamp".to_string(),
739 field_type: FieldType::Structure(timestamp_desc()),
740 },
741 FieldDesc {
742 name: "display".to_string(),
743 field_type: FieldType::Structure(display_desc()),
744 },
745 FieldDesc {
746 name: "control".to_string(),
747 field_type: FieldType::Structure(control_desc()),
748 },
749 ],
750 }
751}
752
753fn alarm_desc() -> StructureDesc {
754 StructureDesc {
755 struct_id: Some("alarm_t".to_string()),
756 fields: vec![
757 FieldDesc {
758 name: "severity".to_string(),
759 field_type: FieldType::Scalar(TypeCode::Int32),
760 },
761 FieldDesc {
762 name: "status".to_string(),
763 field_type: FieldType::Scalar(TypeCode::Int32),
764 },
765 FieldDesc {
766 name: "message".to_string(),
767 field_type: FieldType::String,
768 },
769 ],
770 }
771}
772
773fn timestamp_desc() -> StructureDesc {
774 StructureDesc {
775 struct_id: Some("time_t".to_string()),
776 fields: vec![
777 FieldDesc {
778 name: "secondsPastEpoch".to_string(),
779 field_type: FieldType::Scalar(TypeCode::Int64),
780 },
781 FieldDesc {
782 name: "nanoseconds".to_string(),
783 field_type: FieldType::Scalar(TypeCode::Int32),
784 },
785 FieldDesc {
786 name: "userTag".to_string(),
787 field_type: FieldType::Scalar(TypeCode::Int32),
788 },
789 ],
790 }
791}
792
793fn display_desc() -> StructureDesc {
794 StructureDesc {
795 struct_id: Some("display_t".to_string()),
796 fields: vec![
797 FieldDesc {
798 name: "limitLow".to_string(),
799 field_type: FieldType::Scalar(TypeCode::Float64),
800 },
801 FieldDesc {
802 name: "limitHigh".to_string(),
803 field_type: FieldType::Scalar(TypeCode::Float64),
804 },
805 FieldDesc {
806 name: "description".to_string(),
807 field_type: FieldType::String,
808 },
809 FieldDesc {
810 name: "units".to_string(),
811 field_type: FieldType::String,
812 },
813 FieldDesc {
814 name: "precision".to_string(),
815 field_type: FieldType::Scalar(TypeCode::Int32),
816 },
817 FieldDesc {
818 name: "form".to_string(),
819 field_type: FieldType::Structure(StructureDesc {
820 struct_id: Some("enum_t".to_string()),
821 fields: vec![
822 FieldDesc {
823 name: "index".to_string(),
824 field_type: FieldType::Scalar(TypeCode::Int32),
825 },
826 FieldDesc {
827 name: "choices".to_string(),
828 field_type: FieldType::StringArray,
829 },
830 ],
831 }),
832 },
833 ],
834 }
835}
836
837fn control_desc() -> StructureDesc {
838 StructureDesc {
839 struct_id: Some("control_t".to_string()),
840 fields: vec![
841 FieldDesc {
842 name: "limitLow".to_string(),
843 field_type: FieldType::Scalar(TypeCode::Float64),
844 },
845 FieldDesc {
846 name: "limitHigh".to_string(),
847 field_type: FieldType::Scalar(TypeCode::Float64),
848 },
849 FieldDesc {
850 name: "minStep".to_string(),
851 field_type: FieldType::Scalar(TypeCode::Float64),
852 },
853 ],
854 }
855}
856
857fn value_alarm_desc() -> StructureDesc {
858 StructureDesc {
859 struct_id: Some("valueAlarm_t".to_string()),
860 fields: vec![
861 FieldDesc {
862 name: "active".to_string(),
863 field_type: FieldType::Scalar(TypeCode::Boolean),
864 },
865 FieldDesc {
866 name: "lowAlarmLimit".to_string(),
867 field_type: FieldType::Scalar(TypeCode::Float64),
868 },
869 FieldDesc {
870 name: "lowWarningLimit".to_string(),
871 field_type: FieldType::Scalar(TypeCode::Float64),
872 },
873 FieldDesc {
874 name: "highWarningLimit".to_string(),
875 field_type: FieldType::Scalar(TypeCode::Float64),
876 },
877 FieldDesc {
878 name: "highAlarmLimit".to_string(),
879 field_type: FieldType::Scalar(TypeCode::Float64),
880 },
881 FieldDesc {
882 name: "lowAlarmSeverity".to_string(),
883 field_type: FieldType::Scalar(TypeCode::Int32),
884 },
885 FieldDesc {
886 name: "lowWarningSeverity".to_string(),
887 field_type: FieldType::Scalar(TypeCode::Int32),
888 },
889 FieldDesc {
890 name: "highWarningSeverity".to_string(),
891 field_type: FieldType::Scalar(TypeCode::Int32),
892 },
893 FieldDesc {
894 name: "highAlarmSeverity".to_string(),
895 field_type: FieldType::Scalar(TypeCode::Int32),
896 },
897 FieldDesc {
898 name: "hysteresis".to_string(),
899 field_type: FieldType::Scalar(TypeCode::UInt8),
900 },
901 ],
902 }
903}
904
905#[cfg(test)]
906mod tests {
907 use super::*;
908 use crate::types::{DbCommonState, RecordType};
909 use spvirit_types::{
910 NdCodec, NdDimension, NtNdArray, NtPayload, NtScalar, NtScalarArray, NtTable,
911 NtTableColumn, ScalarArrayValue, ScalarValue,
912 };
913
914 fn make_ai(name: &str, val: f64) -> RecordInstance {
915 RecordInstance {
916 name: name.to_string(),
917 record_type: RecordType::Ai,
918 common: DbCommonState::default(),
919 data: RecordData::Ai {
920 nt: NtScalar::from_value(ScalarValue::F64(val)),
921 inp: None,
922 siml: None,
923 siol: None,
924 simm: false,
925 },
926 raw_fields: HashMap::new(),
927 }
928 }
929
930 fn make_mbbo(name: &str, choices: Vec<String>, initial: i32) -> RecordInstance {
931 RecordInstance {
932 name: name.to_string(),
933 record_type: RecordType::Mbbo,
934 common: DbCommonState::default(),
935 data: RecordData::NtEnum {
936 nt: spvirit_types::NtEnum::new(initial, choices),
937 inp: None,
938 out: None,
939 omsl: crate::types::OutputMode::Supervisory,
940 },
941 raw_fields: HashMap::new(),
942 }
943 }
944
945 fn make_ao(name: &str, val: f64) -> RecordInstance {
946 RecordInstance {
947 name: name.to_string(),
948 record_type: RecordType::Ao,
949 common: DbCommonState::default(),
950 data: RecordData::Ao {
951 nt: NtScalar::from_value(ScalarValue::F64(val)),
952 out: None,
953 dol: None,
954 omsl: crate::types::OutputMode::Supervisory,
955 drvl: None,
956 drvh: None,
957 oroc: None,
958 siml: None,
959 siol: None,
960 simm: false,
961 },
962 raw_fields: HashMap::new(),
963 }
964 }
965
966 #[tokio::test]
967 async fn mdel_deadband_suppresses_small_monitor_updates() {
968 let recs = crate::db::parse_db(
969 r#"
970record(ao, "DB:AO") {
971 field(VAL, "0.0")
972 field(MDEL, "0.5")
973}"#,
974 )
975 .expect("parse");
976 let store = SimplePvStore::new(recs, HashMap::new(), Vec::new(), false);
977 let mut rx = Source::subscribe(&store, "DB:AO")
978 .await
979 .expect("subscribed");
980
981 assert!(store.set_value("DB:AO", ScalarValue::F64(0.2)).await);
983 assert!(store.set_value("DB:AO", ScalarValue::F64(0.9)).await);
985
986 match rx.recv().await.expect("posted update") {
987 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::F64(0.9)),
988 other => panic!("expected scalar, got {other:?}"),
989 }
990 assert!(rx.try_recv().is_err());
992 assert_eq!(store.get_value("DB:AO").await, Some(ScalarValue::F64(0.9)));
994 }
995
996 fn make_waveform(name: &str, value: ScalarArrayValue) -> RecordInstance {
997 let nelm = value.len();
998 RecordInstance {
999 name: name.to_string(),
1000 record_type: RecordType::Waveform,
1001 common: DbCommonState::default(),
1002 data: RecordData::Waveform {
1003 nt: NtScalarArray::from_value(value),
1004 inp: None,
1005 ftvl: "DOUBLE".to_string(),
1006 nelm,
1007 nord: nelm,
1008 },
1009 raw_fields: HashMap::new(),
1010 }
1011 }
1012
1013 fn make_nt_table(name: &str) -> RecordInstance {
1014 RecordInstance {
1015 name: name.to_string(),
1016 record_type: RecordType::NtTable,
1017 common: DbCommonState::default(),
1018 data: RecordData::NtTable {
1019 nt: NtTable {
1020 labels: vec!["X".to_string(), "Y".to_string()],
1021 columns: vec![
1022 NtTableColumn {
1023 name: "x".to_string(),
1024 values: ScalarArrayValue::F64(vec![1.0, 2.0]),
1025 },
1026 NtTableColumn {
1027 name: "y".to_string(),
1028 values: ScalarArrayValue::F64(vec![10.0, 20.0]),
1029 },
1030 ],
1031 descriptor: Some("table".to_string()),
1032 alarm: None,
1033 time_stamp: None,
1034 },
1035 inp: None,
1036 out: None,
1037 omsl: crate::types::OutputMode::Supervisory,
1038 },
1039 raw_fields: HashMap::new(),
1040 }
1041 }
1042
1043 fn make_nt_ndarray(name: &str) -> RecordInstance {
1044 RecordInstance {
1045 name: name.to_string(),
1046 record_type: RecordType::NtNdArray,
1047 common: DbCommonState::default(),
1048 data: RecordData::NtNdArray {
1049 nt: NtNdArray {
1050 value: ScalarArrayValue::U8(vec![0; 4]),
1051 codec: NdCodec {
1052 name: "none".to_string(),
1053 parameters: HashMap::new(),
1054 },
1055 compressed_size: 4,
1056 uncompressed_size: 4,
1057 dimension: vec![NdDimension {
1058 size: 2,
1059 offset: 0,
1060 full_size: 2,
1061 binning: 1,
1062 reverse: false,
1063 }],
1064 unique_id: 1,
1065 data_time_stamp: Default::default(),
1066 attribute: vec![],
1067 descriptor: Some("ndarray".to_string()),
1068 alarm: None,
1069 time_stamp: None,
1070 display: None,
1071 },
1072 inp: None,
1073 out: None,
1074 omsl: crate::types::OutputMode::Supervisory,
1075 },
1076 raw_fields: HashMap::new(),
1077 }
1078 }
1079
1080 #[tokio::test]
1081 async fn store_stamps_initial_timestamps_on_static_records() {
1082 let mut records = HashMap::new();
1086 records.insert("TEST:TBL".into(), make_nt_table("TEST:TBL"));
1087 records.insert("TEST:NDA".into(), make_nt_ndarray("TEST:NDA"));
1088 records.insert(
1089 "TEST:ENUM".into(),
1090 make_mbbo("TEST:ENUM", vec!["A".into(), "B".into()], 0),
1091 );
1092 records.insert(
1093 "TEST:WF".into(),
1094 make_waveform("TEST:WF", ScalarArrayValue::F64(vec![0.0])),
1095 );
1096 records.insert("TEST:AI".into(), make_ai("TEST:AI", 1.0));
1097 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1098
1099 match store.get_nt("TEST:TBL").await.unwrap() {
1100 NtPayload::Table(nt) => {
1101 assert!(nt.time_stamp.expect("table stamped").seconds_past_epoch > 0)
1102 }
1103 _ => panic!("expected table"),
1104 }
1105 match store.get_nt("TEST:NDA").await.unwrap() {
1106 NtPayload::NdArray(nt) => {
1107 assert!(nt.time_stamp.expect("ndarray stamped").seconds_past_epoch > 0);
1108 assert!(nt.data_time_stamp.seconds_past_epoch > 0);
1109 }
1110 _ => panic!("expected ndarray"),
1111 }
1112 match store.get_nt("TEST:ENUM").await.unwrap() {
1113 NtPayload::Enum(nt) => assert!(nt.time_stamp.seconds_past_epoch > 0),
1114 _ => panic!("expected enum"),
1115 }
1116 match store.get_nt("TEST:WF").await.unwrap() {
1117 NtPayload::ScalarArray(nt) => assert!(nt.time_stamp.seconds_past_epoch > 0),
1118 _ => panic!("expected scalar array"),
1119 }
1120 match store.get_nt("TEST:AI").await.unwrap() {
1121 NtPayload::Scalar(nt) => {
1122 assert!(nt.time_stamp.expect("scalar stamped").seconds_past_epoch > 0)
1123 }
1124 _ => panic!("expected scalar"),
1125 }
1126 }
1127
1128 #[tokio::test]
1129 async fn has_pv_returns_true_for_existing() {
1130 let mut records = HashMap::new();
1131 records.insert("TEST:AI".into(), make_ai("TEST:AI", 1.0));
1132 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1133 assert!(store.claim("TEST:AI").await.is_some());
1134 assert!(store.claim("MISSING").await.is_none());
1135 }
1136
1137 #[tokio::test]
1138 async fn get_snapshot_returns_payload() {
1139 let mut records = HashMap::new();
1140 records.insert("TEST:AI".into(), make_ai("TEST:AI", 42.0));
1141 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1142 let snap = store.get("TEST:AI").await.unwrap();
1143 match snap {
1144 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::F64(42.0)),
1145 _ => panic!("expected scalar"),
1146 }
1147 }
1148
1149 #[tokio::test]
1150 async fn put_value_updates_writable_record() {
1151 let mut records = HashMap::new();
1152 records.insert("TEST:AO".into(), make_ao("TEST:AO", 0.0));
1153 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1154
1155 let val = DecodedValue::Structure(vec![("value".to_string(), DecodedValue::Float64(99.5))]);
1156 let result = store.put("TEST:AO", &val).await.unwrap();
1157 assert_eq!(result.len(), 1);
1158 assert_eq!(result[0].0, "TEST:AO");
1159
1160 let snap = store.get("TEST:AO").await.unwrap();
1161 match snap {
1162 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::F64(99.5)),
1163 _ => panic!("expected scalar"),
1164 }
1165 }
1166
1167 #[tokio::test]
1168 async fn put_wire_rejects_out_of_range_enum_index() {
1169 let mut records = HashMap::new();
1170 records.insert(
1171 "E".into(),
1172 make_mbbo("E", vec!["A".into(), "B".into(), "C".into()], 0),
1173 );
1174 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1175
1176 let result = Source::put(&store, "E", &DecodedValue::Int32(7))
1178 .await
1179 .unwrap();
1180 assert!(result.is_empty());
1181 assert_eq!(store.get_value("E").await.unwrap(), ScalarValue::I32(0));
1182
1183 let result = Source::put(&store, "E", &DecodedValue::Int32(2))
1185 .await
1186 .unwrap();
1187 assert_eq!(result.len(), 1);
1188 assert_eq!(store.get_value("E").await.unwrap(), ScalarValue::I32(2));
1189 }
1190
1191 #[tokio::test]
1192 async fn put_value_rejects_readonly() {
1193 let mut records = HashMap::new();
1194 records.insert("TEST:AI".into(), make_ai("TEST:AI", 1.0));
1195 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1196
1197 let val = DecodedValue::Float64(5.0);
1198 let err = store.put("TEST:AI", &val).await.unwrap_err();
1199 assert!(err.contains("not writable"));
1200 }
1201
1202 #[tokio::test]
1203 async fn set_value_bypasses_writable_check() {
1204 let mut records = HashMap::new();
1205 records.insert("TEST:AI".into(), make_ai("TEST:AI", 1.0));
1206 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1207 assert!(store.set_value("TEST:AI", ScalarValue::F64(10.0)).await);
1208 let val = store.get_value("TEST:AI").await.unwrap();
1209 assert_eq!(val, ScalarValue::F64(10.0));
1210 }
1211
1212 #[tokio::test]
1213 async fn set_array_value_updates_all_scalar_array_types() {
1214 let cases: Vec<ScalarArrayValue> = vec![
1215 ScalarArrayValue::Bool(vec![false, true]),
1216 ScalarArrayValue::I8(vec![1, 2]),
1217 ScalarArrayValue::I16(vec![1, 2]),
1218 ScalarArrayValue::I32(vec![1, 2]),
1219 ScalarArrayValue::I64(vec![1, 2]),
1220 ScalarArrayValue::U8(vec![1, 2]),
1221 ScalarArrayValue::U16(vec![1, 2]),
1222 ScalarArrayValue::U32(vec![1, 2]),
1223 ScalarArrayValue::U64(vec![1, 2]),
1224 ScalarArrayValue::F32(vec![1.0, 2.0]),
1225 ScalarArrayValue::F64(vec![1.0, 2.0]),
1226 ScalarArrayValue::Str(vec!["a".to_string(), "b".to_string()]),
1227 ];
1228
1229 for (idx, updated) in cases.into_iter().enumerate() {
1230 let pv = format!("TEST:WF:{idx}");
1231 let mut records = HashMap::new();
1232 records.insert(pv.clone(), make_waveform(&pv, updated.clone()));
1233 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1234
1235 assert!(!store.set_array_value(&pv, updated.clone()).await);
1236
1237 let second = match updated {
1238 ScalarArrayValue::Bool(_) => ScalarArrayValue::Bool(vec![true, false]),
1239 ScalarArrayValue::I8(_) => ScalarArrayValue::I8(vec![3, 4]),
1240 ScalarArrayValue::I16(_) => ScalarArrayValue::I16(vec![3, 4]),
1241 ScalarArrayValue::I32(_) => ScalarArrayValue::I32(vec![3, 4]),
1242 ScalarArrayValue::I64(_) => ScalarArrayValue::I64(vec![3, 4]),
1243 ScalarArrayValue::U8(_) => ScalarArrayValue::U8(vec![3, 4]),
1244 ScalarArrayValue::U16(_) => ScalarArrayValue::U16(vec![3, 4]),
1245 ScalarArrayValue::U32(_) => ScalarArrayValue::U32(vec![3, 4]),
1246 ScalarArrayValue::U64(_) => ScalarArrayValue::U64(vec![3, 4]),
1247 ScalarArrayValue::F32(_) => ScalarArrayValue::F32(vec![3.0, 4.0]),
1248 ScalarArrayValue::F64(_) => ScalarArrayValue::F64(vec![3.0, 4.0]),
1249 ScalarArrayValue::Str(_) => {
1250 ScalarArrayValue::Str(vec!["x".to_string(), "y".to_string()])
1251 }
1252 };
1253
1254 assert!(store.set_array_value(&pv, second.clone()).await);
1255 let snap = store.get(&pv).await.unwrap();
1256 match snap {
1257 NtPayload::ScalarArray(nt) => assert_eq!(nt.value, second),
1258 _ => panic!("expected scalar array"),
1259 }
1260 }
1261 }
1262
1263 #[tokio::test]
1264 async fn get_nt_returns_full_payload() {
1265 let mut records = HashMap::new();
1266 records.insert("TEST:AI".into(), make_ai("TEST:AI", 12.5));
1267 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1268
1269 let nt = store.get_nt("TEST:AI").await.unwrap();
1270 match nt {
1271 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::F64(12.5)),
1272 _ => panic!("expected scalar payload"),
1273 }
1274 }
1275
1276 #[tokio::test]
1277 async fn put_nt_updates_scalar_array_table_and_ndarray() {
1278 let mut records = HashMap::new();
1279 records.insert("TEST:AI".into(), make_ai("TEST:AI", 1.0));
1280 records.insert(
1281 "TEST:WF".into(),
1282 make_waveform("TEST:WF", ScalarArrayValue::F64(vec![0.0, 0.0])),
1283 );
1284 records.insert("TEST:TBL".into(), make_nt_table("TEST:TBL"));
1285 records.insert("TEST:NDA".into(), make_nt_ndarray("TEST:NDA"));
1286 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1287
1288 assert!(
1289 store
1290 .put_nt(
1291 "TEST:AI",
1292 NtPayload::Scalar(NtScalar::from_value(ScalarValue::F64(5.0))),
1293 )
1294 .await
1295 );
1296 assert!(
1297 store
1298 .put_nt(
1299 "TEST:WF",
1300 NtPayload::ScalarArray(NtScalarArray::from_value(ScalarArrayValue::F64(vec![
1301 3.0, 4.0
1302 ],))),
1303 )
1304 .await
1305 );
1306
1307 let table = NtTable {
1308 labels: vec!["X".to_string(), "Y".to_string()],
1309 columns: vec![
1310 NtTableColumn {
1311 name: "x".to_string(),
1312 values: ScalarArrayValue::F64(vec![2.0, 3.0]),
1313 },
1314 NtTableColumn {
1315 name: "y".to_string(),
1316 values: ScalarArrayValue::F64(vec![20.0, 30.0]),
1317 },
1318 ],
1319 descriptor: Some("updated table".to_string()),
1320 alarm: None,
1321 time_stamp: None,
1322 };
1323 assert!(
1324 store
1325 .put_nt("TEST:TBL", NtPayload::Table(table.clone()))
1326 .await
1327 );
1328
1329 let ndarray = NtNdArray {
1330 value: ScalarArrayValue::U8(vec![1, 2, 3, 4]),
1331 codec: NdCodec {
1332 name: "none".to_string(),
1333 parameters: HashMap::new(),
1334 },
1335 compressed_size: 4,
1336 uncompressed_size: 4,
1337 dimension: vec![NdDimension {
1338 size: 4,
1339 offset: 0,
1340 full_size: 4,
1341 binning: 1,
1342 reverse: false,
1343 }],
1344 unique_id: 2,
1345 data_time_stamp: Default::default(),
1346 attribute: vec![],
1347 descriptor: Some("updated ndarray".to_string()),
1348 alarm: None,
1349 time_stamp: None,
1350 display: None,
1351 };
1352 assert!(
1353 store
1354 .put_nt("TEST:NDA", NtPayload::NdArray(ndarray.clone()))
1355 .await
1356 );
1357
1358 assert!(
1359 !store
1360 .put_nt(
1361 "TEST:AI",
1362 NtPayload::ScalarArray(NtScalarArray::from_value(ScalarArrayValue::F64(vec![
1363 1.0
1364 ]))),
1365 )
1366 .await
1367 );
1368
1369 match store.get_nt("TEST:TBL").await.unwrap() {
1372 NtPayload::Table(mut nt) => {
1373 let ts = nt.time_stamp.take().expect("table put must be stamped");
1374 assert!(ts.seconds_past_epoch > 0);
1375 assert_eq!(nt, table);
1376 }
1377 _ => panic!("expected table payload"),
1378 }
1379 match store.get_nt("TEST:NDA").await.unwrap() {
1380 NtPayload::NdArray(mut nt) => {
1381 let ts = nt.time_stamp.take().expect("ndarray put must be stamped");
1382 assert!(ts.seconds_past_epoch > 0);
1383 assert!(nt.data_time_stamp.seconds_past_epoch > 0);
1384 nt.data_time_stamp = Default::default();
1385 assert_eq!(nt, ndarray);
1386 }
1387 _ => panic!("expected ndarray payload"),
1388 }
1389 }
1390
1391 #[tokio::test]
1392 async fn descriptor_matches_value_type() {
1393 let mut records = HashMap::new();
1394 records.insert("TEST:AI".into(), make_ai("TEST:AI", 0.0));
1395 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1396 let info = store.claim("TEST:AI").await.unwrap();
1397 assert_eq!(
1398 info.descriptor.struct_id.as_deref(),
1399 Some("epics:nt/NTScalar:1.0")
1400 );
1401 let desc = info.descriptor;
1402 let value_field = desc.field("value").unwrap();
1403 assert!(matches!(
1404 value_field.field_type,
1405 FieldType::Scalar(TypeCode::Float64)
1406 ));
1407 }
1408
1409 #[tokio::test]
1410 async fn subscribe_receives_updates() {
1411 let mut records = HashMap::new();
1412 records.insert("TEST:AO".into(), make_ao("TEST:AO", 0.0));
1413 let store = SimplePvStore::new(records, HashMap::new(), vec![], false);
1414
1415 let mut rx = Source::subscribe(&store, "TEST:AO").await.unwrap();
1416
1417 let val = DecodedValue::Structure(vec![("value".to_string(), DecodedValue::Float64(7.7))]);
1418 store.put("TEST:AO", &val).await.unwrap();
1419
1420 let update = rx.recv().await.unwrap();
1421 match update {
1422 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::F64(7.7)),
1423 _ => panic!("expected scalar"),
1424 }
1425 }
1426
1427 #[tokio::test]
1428 async fn on_put_callback_is_invoked() {
1429 use std::sync::atomic::{AtomicBool, Ordering};
1430
1431 let called = Arc::new(AtomicBool::new(false));
1432 let called2 = called.clone();
1433
1434 let mut records = HashMap::new();
1435 records.insert("CB:AO".into(), make_ao("CB:AO", 0.0));
1436
1437 let mut on_put = HashMap::new();
1438 let cb: OnPutCallback = Arc::new(move |_name, _val| {
1439 called2.store(true, Ordering::SeqCst);
1440 });
1441 on_put.insert("CB:AO".into(), cb);
1442
1443 let store = SimplePvStore::new(records, on_put, vec![], false);
1444 let val = DecodedValue::Structure(vec![("value".to_string(), DecodedValue::Float64(1.0))]);
1445 store.put("CB:AO", &val).await.unwrap();
1446
1447 tokio::task::yield_now().await;
1449 tokio::task::yield_now().await;
1450
1451 assert!(called.load(Ordering::SeqCst));
1452 }
1453
1454 #[tokio::test]
1455 async fn validator_rejects_put_before_apply() {
1456 let mut records = std::collections::HashMap::new();
1457 records.insert(
1458 "V".to_string(),
1459 crate::pva_server::make_output_record(
1460 "V",
1461 crate::types::RecordType::Ao,
1462 ScalarValue::F64(1.0),
1463 ),
1464 );
1465 let store =
1466 SimplePvStore::new(records, std::collections::HashMap::new(), Vec::new(), false);
1467 store
1468 .set_validator(
1469 "V".to_string(),
1470 std::sync::Arc::new(|_name, _val| Err("nope".to_string())),
1471 )
1472 .await;
1473
1474 let dv = DecodedValue::Float64(2.0);
1475 let res = Source::put(&store, "V", &dv).await;
1476 assert_eq!(res, Err("nope".to_string()));
1477 assert_eq!(store.get_value("V").await, Some(ScalarValue::F64(1.0)));
1479 }
1480
1481 #[tokio::test]
1482 async fn remove_deletes_record_and_is_idempotent() {
1483 let mut records = std::collections::HashMap::new();
1484 records.insert(
1485 "T:GONE".to_string(),
1486 crate::pva_server::make_scalar_record("T:GONE", RecordType::Ai, ScalarValue::F64(1.0)),
1487 );
1488 let store = SimplePvStore::new(records, Default::default(), Vec::new(), false);
1489
1490 assert!(store.get_value("T:GONE").await.is_some());
1491 assert!(store.remove("T:GONE").await, "first remove returns true");
1492 assert!(store.get_value("T:GONE").await.is_none(), "record is gone");
1493 assert!(!store.remove("T:GONE").await, "second remove returns false");
1494 assert!(store.claim("T:GONE").await.is_none(), "claim no longer matches");
1495 }
1496
1497 #[tokio::test]
1498 async fn validator_allows_structure_wrapped_put_through() {
1499 let mut records = std::collections::HashMap::new();
1505 records.insert("W".to_string(), make_ao("W", 1.0));
1506 let store =
1507 SimplePvStore::new(records, std::collections::HashMap::new(), Vec::new(), false);
1508 store
1509 .set_validator("W".to_string(), std::sync::Arc::new(|_name, _val| Ok(())))
1510 .await;
1511
1512 let dv = DecodedValue::Structure(vec![("value".to_string(), DecodedValue::Float64(5.0))]);
1513 let res = Source::put(&store, "W", &dv).await;
1514 assert!(res.is_ok());
1515 assert_eq!(store.get_value("W").await, Some(ScalarValue::F64(5.0)));
1516 }
1517}