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