1use std::marker::PhantomData;
41use std::sync::{Arc, Mutex};
42
43use spvirit_codec::spvd_decode::DecodedValue;
44use spvirit_types::{NtPayload, ScalarArrayValue, ScalarValue};
45
46use crate::pva_server::{make_array_record, make_output_record, make_scalar_record};
47use crate::simple_store::SimplePvStore;
48use crate::types::{RecordInstance, RecordType};
49
50#[derive(Debug, Clone, PartialEq)]
52pub enum PvError {
53 Unbound,
55 NotFound(String),
57 TypeMismatch {
59 expected: &'static str,
60 actual: String,
61 },
62 PutRejected(String),
64}
65
66impl std::fmt::Display for PvError {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 match self {
69 PvError::Unbound => write!(f, "PV handle is not bound to a server yet"),
70 PvError::NotFound(n) => write!(f, "PV '{n}' not found"),
71 PvError::TypeMismatch { expected, actual } => {
72 write!(
73 f,
74 "PV value type mismatch: expected {expected}, record holds {actual}"
75 )
76 }
77 PvError::PutRejected(msg) => write!(f, "PUT rejected: {msg}"),
78 }
79 }
80}
81
82impl std::error::Error for PvError {}
83
84pub trait PvScalar: Sized + Send + Sync + 'static {
86 const TYPE_NAME: &'static str;
88 fn into_scalar(self) -> ScalarValue;
89 fn from_scalar(v: ScalarValue) -> Option<Self>;
90
91 fn from_decoded(dv: &DecodedValue) -> Option<Self> {
101 Self::from_scalar(crate::convert::decoded_to_scalar_value(dv))
102 }
103}
104
105impl PvScalar for f64 {
106 const TYPE_NAME: &'static str = "f64";
107 fn into_scalar(self) -> ScalarValue {
108 ScalarValue::F64(self)
109 }
110 fn from_scalar(v: ScalarValue) -> Option<Self> {
111 match v {
112 ScalarValue::F64(x) => Some(x),
113 ScalarValue::F32(x) => Some(x as f64),
114 _ => None,
115 }
116 }
117 fn from_decoded(dv: &DecodedValue) -> Option<Self> {
118 crate::convert::decoded_to_f64(dv)
119 }
120}
121
122impl PvScalar for bool {
123 const TYPE_NAME: &'static str = "bool";
124 fn into_scalar(self) -> ScalarValue {
125 ScalarValue::Bool(self)
126 }
127 fn from_scalar(v: ScalarValue) -> Option<Self> {
128 match v {
129 ScalarValue::Bool(b) => Some(b),
130 _ => None,
131 }
132 }
133 fn from_decoded(dv: &DecodedValue) -> Option<Self> {
134 crate::convert::decoded_to_bool(dv)
135 }
136}
137
138impl PvScalar for i32 {
139 const TYPE_NAME: &'static str = "i32";
140 fn into_scalar(self) -> ScalarValue {
141 ScalarValue::I32(self)
142 }
143 fn from_scalar(v: ScalarValue) -> Option<Self> {
144 match v {
145 ScalarValue::I32(x) => Some(x),
146 ScalarValue::I16(x) => Some(x as i32),
147 ScalarValue::I8(x) => Some(x as i32),
148 _ => None,
149 }
150 }
151 fn from_decoded(dv: &DecodedValue) -> Option<Self> {
152 crate::convert::decoded_to_i32(dv)
153 }
154}
155
156impl PvScalar for String {
157 const TYPE_NAME: &'static str = "String";
158 fn into_scalar(self) -> ScalarValue {
159 ScalarValue::Str(self)
160 }
161 fn from_scalar(v: ScalarValue) -> Option<Self> {
162 match v {
163 ScalarValue::Str(s) => Some(s),
164 _ => None,
165 }
166 }
167 fn from_decoded(dv: &DecodedValue) -> Option<Self> {
168 crate::convert::decoded_to_string(dv)
169 }
170}
171
172impl PvScalar for ScalarValue {
173 const TYPE_NAME: &'static str = "scalar";
174 fn into_scalar(self) -> ScalarValue {
175 self
176 }
177 fn from_scalar(v: ScalarValue) -> Option<Self> {
178 Some(v)
179 }
180 fn from_decoded(dv: &DecodedValue) -> Option<Self> {
185 Some(match dv {
186 DecodedValue::Boolean(b) => ScalarValue::Bool(*b),
187 DecodedValue::Int8(n) => ScalarValue::I8(*n),
188 DecodedValue::Int16(n) => ScalarValue::I16(*n),
189 DecodedValue::Int32(n) => ScalarValue::I32(*n),
190 DecodedValue::Int64(n) => ScalarValue::I64(*n),
191 DecodedValue::UInt8(n) => ScalarValue::U8(*n),
192 DecodedValue::UInt16(n) => ScalarValue::U16(*n),
193 DecodedValue::UInt32(n) => ScalarValue::U32(*n),
194 DecodedValue::UInt64(n) => ScalarValue::U64(*n),
195 DecodedValue::Float32(f) => ScalarValue::F32(*f),
196 DecodedValue::Float64(f) => ScalarValue::F64(*f),
197 DecodedValue::String(s) => ScalarValue::Str(s.clone()),
198 _ => return None,
199 })
200 }
201}
202
203pub(crate) struct PendingDef {
204 pub(crate) record: RecordInstance,
205 pub(crate) validator: Option<crate::simple_store::PutValidator>,
206 pub(crate) scan: Option<(std::time::Duration, crate::simple_store::ScanCallback)>,
207 pub(crate) calc: Option<(Vec<String>, crate::simple_store::LinkCallback)>,
208}
209
210pub(crate) enum PvState {
211 Pending(PendingDef),
212 Bound(Arc<SimplePvStore>),
213}
214
215pub(crate) struct PvShared {
216 pub(crate) name: String,
217 pub(crate) state: Mutex<PvState>,
218}
219
220pub struct Pv<T: PvScalar> {
222 pub(crate) shared: Arc<PvShared>,
223 _marker: PhantomData<fn() -> T>,
224}
225
226impl<T: PvScalar> Clone for Pv<T> {
227 fn clone(&self) -> Self {
228 Self {
229 shared: self.shared.clone(),
230 _marker: PhantomData,
231 }
232 }
233}
234
235impl<T: PvScalar> Pv<T> {
236 fn from_record(record: RecordInstance) -> Self {
237 Self {
238 shared: Arc::new(PvShared {
239 name: record.name.clone(),
240 state: Mutex::new(PvState::Pending(PendingDef {
241 record,
242 validator: None,
243 scan: None,
244 calc: None,
245 })),
246 }),
247 _marker: PhantomData,
248 }
249 }
250
251 pub fn name(&self) -> &str {
252 &self.shared.name
253 }
254
255 #[cfg(test)]
258 fn pending_record(&self) -> Option<RecordInstance> {
259 match &*self.shared.state.lock().unwrap() {
260 PvState::Pending(def) => Some(def.record.clone()),
261 PvState::Bound(_) => None,
262 }
263 }
264
265 fn with_record(self, f: impl FnOnce(&mut RecordInstance)) -> Self {
267 {
268 let mut state = self.shared.state.lock().unwrap();
269 match &mut *state {
270 PvState::Pending(def) => f(&mut def.record),
271 PvState::Bound(_) => {
272 tracing::warn!("Pv '{}': option ignored, already bound", self.shared.name)
273 }
274 }
275 }
276 self
277 }
278
279 pub fn units(self, units: impl Into<String>) -> Self {
280 let u = units.into();
281 self.with_record(|r| {
282 if let Some(nt) = r.nt_scalar_mut() {
283 nt.units = u;
284 }
285 })
286 }
287
288 pub fn prec(self, prec: i32) -> Self {
289 self.with_record(|r| {
290 if let Some(nt) = r.nt_scalar_mut() {
291 nt.display_precision = prec;
292 }
293 })
294 }
295
296 pub fn desc(self, desc: impl Into<String>) -> Self {
297 let d = desc.into();
298 self.with_record(|r| {
299 r.common.desc = d.clone();
300 if let Some(nt) = r.nt_scalar_mut() {
301 nt.display_description = d;
302 }
303 })
304 }
305
306 pub fn adel(self, deadband: f64) -> Self {
308 self.with_record(|r| {
309 r.raw_fields.insert("ADEL".into(), trim_float(deadband));
310 })
311 }
312
313 pub fn mdel(self, deadband: f64) -> Self {
315 self.with_record(|r| {
316 r.raw_fields.insert("MDEL".into(), trim_float(deadband));
317 })
318 }
319
320 pub fn drive_limits(self, low: f64, high: f64) -> Self {
321 self.with_record(|r| {
322 if let Some(nt) = r.nt_scalar_mut() {
323 nt.control_low = low;
324 nt.control_high = high;
325 }
326 })
327 }
328
329 pub fn alarm_limits(self, lolo: f64, low: f64, high: f64, hihi: f64) -> Self {
330 self.with_record(|r| {
331 if let Some(nt) = r.nt_scalar_mut() {
332 nt.value_alarm_active = true;
333 nt.value_alarm_low_alarm_limit = lolo;
334 nt.value_alarm_low_warning_limit = low;
335 nt.value_alarm_high_warning_limit = high;
336 nt.value_alarm_high_alarm_limit = hihi;
337 }
338 })
339 }
340
341 pub fn on_put<F>(self, f: F) -> Self
344 where
345 F: Fn(&Pv<T>, T) -> Result<(), String> + Send + Sync + 'static,
346 {
347 let handle = self.clone();
348 let validator: crate::simple_store::PutValidator = Arc::new(move |_name, dv| {
349 let scalar_dv = unwrap_value_field(dv);
355 let typed = T::from_decoded(scalar_dv)
356 .ok_or_else(|| format!("expected {}, got {scalar_dv:?}", T::TYPE_NAME))?;
357 f(&handle, typed)
358 });
359 {
360 let mut state = self.shared.state.lock().unwrap();
361 match &mut *state {
362 PvState::Pending(def) => def.validator = Some(validator),
363 PvState::Bound(_) => {
364 tracing::warn!("Pv '{}': on_put ignored, already bound", self.shared.name)
365 }
366 }
367 }
368 self
369 }
370
371 pub fn scan<F>(self, period: std::time::Duration, f: F) -> Self
373 where
374 F: Fn(&Pv<T>) -> T + Send + Sync + 'static,
375 {
376 let handle = self.clone();
377 let cb: crate::simple_store::ScanCallback = Arc::new(move |_name| f(&handle).into_scalar());
378 {
379 let mut state = self.shared.state.lock().unwrap();
380 if let PvState::Pending(def) = &mut *state {
381 def.scan = Some((period, cb));
382 } else {
383 tracing::warn!("Pv '{}': scan ignored, already bound", self.shared.name);
384 }
385 }
386 self
387 }
388}
389
390impl Pv<f64> {
391 pub fn calc<F>(name: impl Into<String>, inputs: &[&Pv<f64>], f: F) -> Self
393 where
394 F: Fn(&[f64]) -> f64 + Send + Sync + 'static,
395 {
396 let name = name.into();
397 let input_names: Vec<String> = inputs.iter().map(|p| p.shared.name.clone()).collect();
398 let initial = ScalarValue::F64(0.0);
399 let pv = Self::from_record(make_scalar_record(&name, RecordType::Ai, initial));
400 let compute: crate::simple_store::LinkCallback = Arc::new(move |values| {
401 let floats: Vec<f64> = values
402 .iter()
403 .map(|v| f64::from_scalar(v.clone()).unwrap_or(0.0))
404 .collect();
405 ScalarValue::F64(f(&floats))
406 });
407 if let PvState::Pending(def) = &mut *pv.shared.state.lock().unwrap() {
408 def.calc = Some((input_names, compute));
409 }
410 pv
411 }
412}
413
414fn unwrap_value_field(dv: &DecodedValue) -> &DecodedValue {
418 match dv {
419 DecodedValue::Structure(fields) => fields
420 .iter()
421 .find(|(name, _)| name == "value")
422 .map(|(_, v)| v)
423 .unwrap_or(dv),
424 other => other,
425 }
426}
427
428fn trim_float(v: f64) -> String {
430 if v.fract() == 0.0 && v.abs() < 1e15 {
431 format!("{}", v as i64)
432 } else {
433 format!("{v}")
434 }
435}
436
437impl Pv<f64> {
438 pub fn ai(name: impl Into<String>, initial: f64) -> Self {
439 let name = name.into();
440 Self::from_record(make_scalar_record(
441 &name,
442 RecordType::Ai,
443 ScalarValue::F64(initial),
444 ))
445 }
446 pub fn ao(name: impl Into<String>, initial: f64) -> Self {
447 let name = name.into();
448 Self::from_record(make_output_record(
449 &name,
450 RecordType::Ao,
451 ScalarValue::F64(initial),
452 ))
453 }
454}
455
456impl Pv<bool> {
457 pub fn bi(name: impl Into<String>, initial: bool) -> Self {
458 let name = name.into();
459 Self::from_record(make_scalar_record(
460 &name,
461 RecordType::Bi,
462 ScalarValue::Bool(initial),
463 ))
464 }
465 pub fn bo(name: impl Into<String>, initial: bool) -> Self {
466 let name = name.into();
467 Self::from_record(make_output_record(
468 &name,
469 RecordType::Bo,
470 ScalarValue::Bool(initial),
471 ))
472 }
473}
474
475impl Pv<String> {
476 pub fn string_in(name: impl Into<String>, initial: impl Into<String>) -> Self {
477 let name = name.into();
478 Self::from_record(make_scalar_record(
479 &name,
480 RecordType::StringIn,
481 ScalarValue::Str(initial.into()),
482 ))
483 }
484 pub fn string_out(name: impl Into<String>, initial: impl Into<String>) -> Self {
485 let name = name.into();
486 Self::from_record(make_output_record(
487 &name,
488 RecordType::StringOut,
489 ScalarValue::Str(initial.into()),
490 ))
491 }
492}
493
494impl Pv<i32> {
495 pub fn longin(name: impl Into<String>, initial: i32) -> Self {
497 let name = name.into();
498 Self::from_record(make_scalar_record(
499 &name,
500 RecordType::LongIn,
501 ScalarValue::I32(initial),
502 ))
503 }
504 pub fn longout(name: impl Into<String>, initial: i32) -> Self {
506 let name = name.into();
507 Self::from_record(make_output_record(
508 &name,
509 RecordType::LongOut,
510 ScalarValue::I32(initial),
511 ))
512 }
513 pub fn mbbi(name: impl Into<String>, choices: Vec<String>, initial: i32) -> Self {
515 Self::from_enum_record(name.into(), choices, initial, RecordType::Mbbi)
516 }
517 pub fn mbbo(name: impl Into<String>, choices: Vec<String>, initial: i32) -> Self {
519 Self::from_enum_record(name.into(), choices, initial, RecordType::Mbbo)
520 }
521 fn from_enum_record(name: String, choices: Vec<String>, initial: i32, rt: RecordType) -> Self {
522 let data = crate::types::RecordData::NtEnum {
524 nt: spvirit_types::NtEnum::new(initial, choices),
525 inp: None,
526 out: None,
527 omsl: crate::types::OutputMode::Supervisory,
528 };
529 Self::from_record(RecordInstance {
530 name: name.clone(),
531 record_type: rt,
532 common: crate::types::DbCommonState::default(),
533 data,
534 raw_fields: std::collections::HashMap::new(),
535 })
536 }
537}
538
539pub(crate) fn scalar_family_record_type(v: &ScalarValue, writable: bool) -> RecordType {
543 match (v, writable) {
544 (ScalarValue::F32(_) | ScalarValue::F64(_), false) => RecordType::Ai,
545 (ScalarValue::F32(_) | ScalarValue::F64(_), true) => RecordType::Ao,
546 (ScalarValue::Bool(_), false) => RecordType::Bi,
547 (ScalarValue::Bool(_), true) => RecordType::Bo,
548 (ScalarValue::Str(_), false) => RecordType::StringIn,
549 (ScalarValue::Str(_), true) => RecordType::StringOut,
550 (_, false) => RecordType::LongIn,
551 (_, true) => RecordType::LongOut,
552 }
553}
554
555impl Pv<ScalarValue> {
556 pub fn scalar_in(name: impl Into<String>, initial: ScalarValue) -> Self {
574 let name = name.into();
575 let rt = scalar_family_record_type(&initial, false);
576 Self::from_record(make_scalar_record(&name, rt, initial))
577 }
578 pub fn scalar_out(name: impl Into<String>, initial: ScalarValue) -> Self {
591 let name = name.into();
592 let rt = scalar_family_record_type(&initial, true);
593 Self::from_record(make_output_record(&name, rt, initial))
594 }
595}
596
597impl<T: PvScalar> Pv<T> {
598 fn store(&self) -> Result<Arc<SimplePvStore>, PvError> {
599 match &*self.shared.state.lock().unwrap() {
600 PvState::Bound(store) => Ok(store.clone()),
601 PvState::Pending(_) => Err(PvError::Unbound),
602 }
603 }
604
605 pub async fn set(&self, value: T) -> Result<(), PvError> {
608 let store = self.store()?;
609 if store
610 .set_value(&self.shared.name, value.into_scalar())
611 .await
612 {
613 Ok(())
614 } else if store.get_value(&self.shared.name).await.is_some() {
615 Ok(())
621 } else {
622 Err(PvError::NotFound(self.shared.name.clone()))
623 }
624 }
625
626 pub async fn set_alarm(
630 &self,
631 severity: i32,
632 status: i32,
633 message: &str,
634 ) -> Result<(), PvError> {
635 let store = self.store()?;
636 if store
637 .set_alarm(&self.shared.name, severity, status, message)
638 .await
639 {
640 Ok(())
641 } else if store.get_value(&self.shared.name).await.is_some() {
642 Ok(())
643 } else {
644 Err(PvError::NotFound(self.shared.name.clone()))
645 }
646 }
647
648 pub async fn get(&self) -> Result<T, PvError> {
650 let store = self.store()?;
651 let v = store
652 .get_value(&self.shared.name)
653 .await
654 .ok_or_else(|| PvError::NotFound(self.shared.name.clone()))?;
655 let actual = format!("{v:?}");
656 T::from_scalar(v).ok_or(PvError::TypeMismatch {
657 expected: T::TYPE_NAME,
658 actual,
659 })
660 }
661
662 pub(crate) async fn attach(store: &Arc<SimplePvStore>, name: &str) -> Result<Self, PvError> {
664 match store.get_nt(name).await {
672 None => return Err(PvError::NotFound(name.to_string())),
673 Some(NtPayload::Scalar(_)) | Some(NtPayload::Enum(_)) => {}
674 Some(other) => {
675 return Err(PvError::TypeMismatch {
676 expected: T::TYPE_NAME,
677 actual: nt_payload_kind(&other).to_string(),
678 });
679 }
680 }
681 let v = store
682 .get_value(name)
683 .await
684 .ok_or_else(|| PvError::NotFound(name.to_string()))?;
685 let actual = format!("{v:?}");
686 if T::from_scalar(v).is_none() {
687 return Err(PvError::TypeMismatch {
688 expected: T::TYPE_NAME,
689 actual,
690 });
691 }
692 Ok(Self {
693 shared: Arc::new(PvShared {
694 name: name.to_string(),
695 state: Mutex::new(PvState::Bound(store.clone())),
696 }),
697 _marker: PhantomData,
698 })
699 }
700}
701
702fn nt_payload_kind(p: &NtPayload) -> &'static str {
704 match p {
705 NtPayload::Scalar(_) => "Scalar",
706 NtPayload::ScalarArray(_) => "ScalarArray",
707 NtPayload::Table(_) => "Table",
708 NtPayload::NdArray(_) => "NdArray",
709 NtPayload::Enum(_) => "Enum",
710 NtPayload::Generic { .. } => "Generic",
711 }
712}
713
714pub struct PvArray {
718 pub(crate) shared: Arc<PvShared>,
719}
720
721impl Clone for PvArray {
722 fn clone(&self) -> Self {
723 Self {
724 shared: self.shared.clone(),
725 }
726 }
727}
728
729impl PvArray {
730 fn from_record(record: RecordInstance) -> Self {
731 Self {
732 shared: Arc::new(PvShared {
733 name: record.name.clone(),
734 state: Mutex::new(PvState::Pending(PendingDef {
735 record,
736 validator: None,
737 scan: None,
738 calc: None,
739 })),
740 }),
741 }
742 }
743
744 pub fn waveform(name: impl Into<String>, data: ScalarArrayValue) -> Self {
746 let name = name.into();
747 Self::from_record(make_array_record(&name, RecordType::Waveform, data))
748 }
749
750 pub fn aai(name: impl Into<String>, data: ScalarArrayValue) -> Self {
752 let name = name.into();
753 Self::from_record(make_array_record(&name, RecordType::Aai, data))
754 }
755
756 pub fn aao(name: impl Into<String>, data: ScalarArrayValue) -> Self {
758 let name = name.into();
759 Self::from_record(make_array_record(&name, RecordType::Aao, data))
760 }
761
762 pub fn name(&self) -> &str {
763 &self.shared.name
764 }
765
766 #[cfg(test)]
769 fn pending_record(&self) -> Option<RecordInstance> {
770 match &*self.shared.state.lock().unwrap() {
771 PvState::Pending(def) => Some(def.record.clone()),
772 PvState::Bound(_) => None,
773 }
774 }
775
776 fn store(&self) -> Result<Arc<SimplePvStore>, PvError> {
777 match &*self.shared.state.lock().unwrap() {
778 PvState::Bound(store) => Ok(store.clone()),
779 PvState::Pending(_) => Err(PvError::Unbound),
780 }
781 }
782
783 pub async fn set(&self, data: ScalarArrayValue) -> Result<(), PvError> {
785 let store = self.store()?;
786 if store.set_array_value(&self.shared.name, data).await {
787 Ok(())
788 } else {
789 match store.get_nt(&self.shared.name).await {
790 Some(NtPayload::ScalarArray(_)) => Ok(()),
794 Some(other) => Err(PvError::TypeMismatch {
795 expected: "array",
796 actual: nt_payload_kind(&other).to_string(),
797 }),
798 None => Err(PvError::NotFound(self.shared.name.clone())),
799 }
800 }
801 }
802
803 pub async fn set_alarm(
807 &self,
808 severity: i32,
809 status: i32,
810 message: &str,
811 ) -> Result<(), PvError> {
812 let store = self.store()?;
813 if store
814 .set_alarm(&self.shared.name, severity, status, message)
815 .await
816 {
817 Ok(())
818 } else if store.get_nt(&self.shared.name).await.is_some() {
819 Ok(())
820 } else {
821 Err(PvError::NotFound(self.shared.name.clone()))
822 }
823 }
824
825 pub async fn get(&self) -> Result<ScalarArrayValue, PvError> {
827 let store = self.store()?;
828 match store.get_nt(&self.shared.name).await {
829 Some(NtPayload::ScalarArray(nt)) => Ok(nt.value),
830 Some(other) => Err(PvError::TypeMismatch {
831 expected: "array",
832 actual: nt_payload_kind(&other).to_string(),
833 }),
834 None => Err(PvError::NotFound(self.shared.name.clone())),
835 }
836 }
837
838 pub(crate) async fn attach(store: &Arc<SimplePvStore>, name: &str) -> Result<Self, PvError> {
840 match store.get_nt(name).await {
841 Some(NtPayload::ScalarArray(_)) => Ok(Self {
842 shared: Arc::new(PvShared {
843 name: name.to_string(),
844 state: Mutex::new(PvState::Bound(store.clone())),
845 }),
846 }),
847 Some(other) => Err(PvError::TypeMismatch {
848 expected: "array",
849 actual: nt_payload_kind(&other).to_string(),
850 }),
851 None => Err(PvError::NotFound(name.to_string())),
852 }
853 }
854}
855
856impl From<PvArray> for AnyPv {
857 fn from(pv: PvArray) -> Self {
858 Self { shared: pv.shared }
859 }
860}
861
862pub struct AnyPv {
864 pub(crate) shared: Arc<PvShared>,
865}
866
867impl<T: PvScalar> From<Pv<T>> for AnyPv {
868 fn from(pv: Pv<T>) -> Self {
869 Self { shared: pv.shared }
870 }
871}
872
873impl AnyPv {
874 pub(crate) fn take_record(&self) -> Option<RecordInstance> {
876 let state = self.shared.state.lock().unwrap();
877 match &*state {
878 PvState::Pending(def) => Some(def.record.clone()),
879 PvState::Bound(_) => None,
880 }
881 }
882
883 pub(crate) fn bind(&self, store: &Arc<SimplePvStore>) {
885 *self.shared.state.lock().unwrap() = PvState::Bound(store.clone());
886 }
887
888 pub fn name(&self) -> &str {
889 &self.shared.name
890 }
891
892 pub(crate) fn take_validator(&self) -> Option<crate::simple_store::PutValidator> {
894 let mut state = self.shared.state.lock().unwrap();
895 match &mut *state {
896 PvState::Pending(def) => def.validator.take(),
897 PvState::Bound(_) => None,
898 }
899 }
900
901 pub(crate) fn take_scan(
903 &self,
904 ) -> Option<(std::time::Duration, crate::simple_store::ScanCallback)> {
905 let mut state = self.shared.state.lock().unwrap();
906 match &mut *state {
907 PvState::Pending(def) => def.scan.take(),
908 PvState::Bound(_) => None,
909 }
910 }
911
912 pub(crate) fn take_calc(&self) -> Option<(Vec<String>, crate::simple_store::LinkCallback)> {
914 let mut state = self.shared.state.lock().unwrap();
915 match &mut *state {
916 PvState::Pending(def) => def.calc.take(),
917 PvState::Bound(_) => None,
918 }
919 }
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925 use spvirit_types::NtScalar;
926
927 #[test]
928 fn pvscalar_roundtrip_f64() {
929 assert_eq!(f64::from_scalar(ScalarValue::F64(1.5)), Some(1.5));
930 assert!(matches!(1.5f64.into_scalar(), ScalarValue::F64(x) if x == 1.5));
931 }
932
933 #[test]
934 fn pvscalar_f64_accepts_f32_widening() {
935 assert_eq!(f64::from_scalar(ScalarValue::F32(2.0)), Some(2.0));
936 }
937
938 #[test]
939 fn pvscalar_rejects_wrong_variant() {
940 assert_eq!(f64::from_scalar(ScalarValue::Str("x".into())), None);
941 assert_eq!(bool::from_scalar(ScalarValue::F64(1.0)), None);
942 assert_eq!(i32::from_scalar(ScalarValue::Str("1".into())), None);
943 assert_eq!(String::from_scalar(ScalarValue::Bool(true)), None);
944 }
945
946 #[test]
947 fn pverror_display() {
948 let e = PvError::TypeMismatch {
949 expected: "f64",
950 actual: "Str".into(),
951 };
952 assert!(e.to_string().contains("f64"));
953 assert!(PvError::Unbound.to_string().contains("not bound"));
954 }
955
956 #[test]
957 fn ai_constructor_builds_record_template() {
958 let pv = Pv::ai("SIM:TEMP", 22.5).units("C").prec(2).desc("Temp");
959 let rec = pv.pending_record().expect("still pending");
960 assert_eq!(rec.name, "SIM:TEMP");
961 let nt = rec.to_ntscalar();
962 assert_eq!(nt.value, ScalarValue::F64(22.5));
963 assert_eq!(nt.units, "C");
964 assert_eq!(nt.display_precision, 2);
965 assert_eq!(rec.common.desc, "Temp");
966 assert!(!rec.writable(), "ai is read-only over the wire");
967 assert_eq!(pv.name(), "SIM:TEMP");
968 }
969
970 #[test]
971 fn ao_is_writable_with_drive_limits() {
972 let pv = Pv::ao("SIM:SP", 25.0).drive_limits(0.0, 100.0);
973 let rec = pv.pending_record().unwrap();
974 assert!(rec.writable());
975 let nt = rec.to_ntscalar();
976 assert_eq!(nt.control_low, 0.0);
977 assert_eq!(nt.control_high, 100.0);
978 }
979
980 #[test]
981 fn mdel_adel_go_to_raw_fields() {
982 let pv = Pv::ai("SIM:X", 0.0).mdel(0.5).adel(1.0);
983 let rec = pv.pending_record().unwrap();
984 assert_eq!(rec.raw_fields.get("MDEL").map(String::as_str), Some("0.5"));
985 assert_eq!(rec.raw_fields.get("ADEL").map(String::as_str), Some("1"));
986 }
987
988 #[test]
989 fn alarm_limits_set_value_alarm_block() {
990 let pv = Pv::ao("SIM:A", 0.0).alarm_limits(-10.0, -5.0, 5.0, 10.0);
991 let nt = pv.pending_record().unwrap().to_ntscalar();
992 assert_eq!(nt.value_alarm_low_alarm_limit, -10.0);
993 assert_eq!(nt.value_alarm_low_warning_limit, -5.0);
994 assert_eq!(nt.value_alarm_high_warning_limit, 5.0);
995 assert_eq!(nt.value_alarm_high_alarm_limit, 10.0);
996 assert!(nt.value_alarm_active);
997 }
998
999 #[test]
1000 fn bool_and_string_constructors() {
1001 assert!(Pv::bo("B", true).pending_record().unwrap().writable());
1002 assert!(!Pv::bi("B2", false).pending_record().unwrap().writable());
1003 let s = Pv::string_in("S", "hello").pending_record().unwrap();
1004 assert_eq!(s.to_ntscalar().value, ScalarValue::Str("hello".into()));
1005 }
1006
1007 #[test]
1008 fn longin_longout_constructors() {
1009 let li = Pv::longin("L:IN", 42);
1010 let rec = li.pending_record().unwrap();
1011 assert_eq!(rec.record_type, crate::types::RecordType::LongIn);
1012 assert_eq!(rec.to_ntscalar().value, ScalarValue::I32(42));
1013 assert!(!rec.writable());
1014
1015 let lo = Pv::longout("L:OUT", 7).drive_limits(0.0, 1000.0);
1016 let rec = lo.pending_record().unwrap();
1017 assert_eq!(rec.record_type, crate::types::RecordType::LongOut);
1018 assert!(rec.writable());
1019 assert_eq!(rec.to_ntscalar().control_high, 1000.0);
1020 }
1021
1022 #[tokio::test]
1023 async fn longout_set_get_roundtrip() {
1024 let store = empty_store();
1025 let pv = Pv::longout("L:RT", 1);
1026 let any: AnyPv = pv.clone().into();
1027 let rec = any.take_record().unwrap();
1028 store.insert(rec.name.clone(), rec).await;
1029 any.bind(&store);
1030 pv.set(99).await.unwrap();
1031 assert_eq!(pv.get().await, Ok(99));
1032 }
1033
1034 #[test]
1035 fn mbbi_mbbo_constructors() {
1036 let m = Pv::mbbi("M:I", vec!["Off".into(), "On".into(), "Auto".into()], 1);
1037 let rec = m.pending_record().unwrap();
1038 assert_eq!(rec.record_type, crate::types::RecordType::Mbbi);
1039 assert_eq!(rec.current_value(), ScalarValue::I32(1));
1040
1041 let o = Pv::mbbo("M:O", vec!["A".into(), "B".into()], 0);
1042 assert!(o.pending_record().unwrap().writable());
1043 }
1044
1045 #[tokio::test]
1046 async fn mbbo_set_get_index_with_bounds() {
1047 let store = empty_store();
1048 let pv = Pv::mbbo("M:RT", vec!["Stop".into(), "Run".into(), "Fault".into()], 0);
1049 let any: AnyPv = pv.clone().into();
1050 let rec = any.take_record().unwrap();
1051 store.insert(rec.name.clone(), rec).await;
1052 any.bind(&store);
1053
1054 pv.set(2).await.unwrap();
1055 assert_eq!(pv.get().await, Ok(2));
1056 let _ = pv.set(7).await;
1059 assert_eq!(pv.get().await, Ok(2));
1060 }
1061
1062 #[test]
1063 fn set_scalar_value_on_raw_nt_enum_record() {
1064 let mut rec = Pv::mbbo("M:RAW", vec!["Off".into(), "On".into(), "Auto".into()], 0)
1065 .pending_record()
1066 .unwrap();
1067 assert!(rec.set_scalar_value(ScalarValue::I32(2), true));
1069 assert_eq!(rec.current_value(), ScalarValue::I32(2));
1070 assert!(!rec.set_scalar_value(ScalarValue::I32(2), true));
1072 assert!(!rec.set_scalar_value(ScalarValue::I32(7), true));
1074 assert_eq!(rec.current_value(), ScalarValue::I32(2));
1075 assert!(!rec.set_scalar_value(ScalarValue::I32(-1), true));
1076 assert_eq!(rec.current_value(), ScalarValue::I32(2));
1077 }
1078
1079 #[test]
1080 fn set_scalar_value_stamps_timestamp() {
1081 let mut rec = Pv::ao("A:TS", 1.0).pending_record().unwrap();
1082 assert!(rec.set_scalar_value(ScalarValue::F64(2.0), false));
1083 match rec.to_ntpayload() {
1084 NtPayload::Scalar(nt) => {
1085 let ts = nt.time_stamp.expect("scalar update must store a timestamp");
1086 assert!(ts.seconds_past_epoch > 0);
1087 }
1088 other => panic!("expected scalar payload, got {other:?}"),
1089 }
1090 assert!(!rec.set_scalar_value(ScalarValue::F64(2.0), false));
1092 }
1093
1094 #[test]
1095 fn set_scalar_value_stamps_enum_timestamp() {
1096 let mut rec = Pv::mbbo("M:TS", vec!["Off".into(), "On".into()], 0)
1097 .pending_record()
1098 .unwrap();
1099 assert!(rec.set_scalar_value(ScalarValue::I32(1), false));
1100 match rec.to_ntpayload() {
1101 NtPayload::Enum(nt) => {
1102 assert!(nt.time_stamp.seconds_past_epoch > 0);
1103 }
1104 other => panic!("expected enum payload, got {other:?}"),
1105 }
1106 }
1107
1108 #[test]
1109 fn set_nt_payload_stamps_missing_timestamp_but_keeps_explicit_one() {
1110 let mut rec = Pv::ao("A:NT", 1.0).pending_record().unwrap();
1111
1112 let nt = NtScalar::from_value(ScalarValue::F64(2.0));
1114 assert!(rec.set_nt_payload(NtPayload::Scalar(nt)));
1115 match rec.to_ntpayload() {
1116 NtPayload::Scalar(nt) => {
1117 let ts = nt.time_stamp.expect("put_nt must store a timestamp");
1118 assert!(ts.seconds_past_epoch > 0);
1119 }
1120 other => panic!("expected scalar payload, got {other:?}"),
1121 }
1122
1123 let nt = NtScalar::from_value(ScalarValue::F64(3.0)).with_timestamp(1_700_000_000, 42);
1125 assert!(rec.set_nt_payload(NtPayload::Scalar(nt)));
1126 match rec.to_ntpayload() {
1127 NtPayload::Scalar(nt) => {
1128 let ts = nt.time_stamp.unwrap();
1129 assert_eq!(ts.seconds_past_epoch, 1_700_000_000);
1130 assert_eq!(ts.nanoseconds, 42);
1131 }
1132 other => panic!("expected scalar payload, got {other:?}"),
1133 }
1134 }
1135
1136 #[tokio::test]
1137 async fn pv_array_roundtrip_and_serve() {
1138 let wf = PvArray::waveform("W:1", ScalarArrayValue::F64(vec![1.0, 2.0, 3.0]));
1139 let server = crate::pva_server::PvaServer::serve([AnyPv::from(wf.clone())])
1140 .build()
1141 .await;
1142 assert_eq!(
1143 wf.get().await,
1144 Ok(ScalarArrayValue::F64(vec![1.0, 2.0, 3.0]))
1145 );
1146 wf.set(ScalarArrayValue::F64(vec![4.0, 5.0])).await.unwrap();
1147 match wf.get().await.unwrap() {
1148 ScalarArrayValue::F64(v) => assert_eq!(v, vec![4.0, 5.0]),
1149 other => panic!("wrong kind: {other:?}"),
1150 }
1151 let h = server.array_pv("W:1").await.unwrap();
1153 assert!(matches!(h.get().await.unwrap(), ScalarArrayValue::F64(_)));
1154 assert!(matches!(
1156 server.pv::<f64>("W:1").await,
1157 Err(PvError::TypeMismatch { .. })
1158 ));
1159 let t = Pv::ai("W:S", 1.0);
1161 let s2 = crate::pva_server::PvaServer::serve([AnyPv::from(t)])
1162 .build()
1163 .await;
1164 assert!(matches!(
1165 s2.array_pv("W:S").await,
1166 Err(PvError::TypeMismatch { .. })
1167 ));
1168 }
1169
1170 #[test]
1171 fn aai_read_only_aao_writable() {
1172 let a = PvArray::aai("W:AI", ScalarArrayValue::I32(vec![1]));
1173 assert!(a.pending_record().is_some());
1174 assert!(!AnyPv::from(a).take_record().unwrap().writable());
1175 let b = PvArray::aao("W:AO", ScalarArrayValue::I32(vec![1]));
1176 assert!(AnyPv::from(b).take_record().unwrap().writable());
1177 }
1178
1179 #[tokio::test]
1180 async fn scalar_attach_rejects_array_backed_record() {
1181 let store = empty_store();
1186 let wf = PvArray::waveform("W:GUARD", ScalarArrayValue::I32(vec![1, 2, 3]));
1187 let any: AnyPv = wf.into();
1188 let rec = any.take_record().unwrap();
1189 store.insert(rec.name.clone(), rec).await;
1190
1191 let bad = Pv::<i32>::attach(&store, "W:GUARD").await;
1192 assert!(matches!(bad, Err(PvError::TypeMismatch { .. })));
1193 }
1194
1195 fn empty_store() -> Arc<SimplePvStore> {
1196 Arc::new(SimplePvStore::new(
1197 std::collections::HashMap::new(),
1198 std::collections::HashMap::new(),
1199 Vec::new(),
1200 false,
1201 ))
1202 }
1203
1204 #[tokio::test]
1205 async fn set_get_before_bind_errors() {
1206 let pv = Pv::ai("SIM:X", 1.0);
1207 assert_eq!(pv.set(2.0).await, Err(PvError::Unbound));
1208 assert_eq!(pv.get().await, Err(PvError::Unbound));
1209 }
1210
1211 #[tokio::test]
1212 async fn bind_then_set_get_roundtrip() {
1213 let store = empty_store();
1214 let pv = Pv::ai("SIM:X", 1.0).units("mm");
1215 let any: AnyPv = pv.clone().into();
1216 let rec = any.take_record().expect("pending record");
1217 store.insert(rec.name.clone(), rec).await;
1218 any.bind(&store);
1219
1220 assert_eq!(pv.get().await, Ok(1.0));
1221 pv.set(2.5).await.unwrap();
1222 assert_eq!(pv.get().await, Ok(2.5));
1223 assert_eq!(pv.clone().get().await, Ok(2.5));
1225 }
1226
1227 #[tokio::test]
1228 async fn attach_mints_typed_handle_and_checks_type() {
1229 let store = empty_store();
1230 let src = Pv::ai("SIM:Y", 3.0);
1231 let any: AnyPv = src.into();
1232 let rec = any.take_record().unwrap();
1233 store.insert(rec.name.clone(), rec).await;
1234
1235 let h: Pv<f64> = Pv::attach(&store, "SIM:Y").await.unwrap();
1236 assert_eq!(h.get().await, Ok(3.0));
1237
1238 let bad = Pv::<bool>::attach(&store, "SIM:Y").await;
1239 assert!(matches!(bad, Err(PvError::TypeMismatch { .. })));
1240 let missing = Pv::<f64>::attach(&store, "NOPE").await;
1241 assert!(matches!(missing, Err(PvError::NotFound(ref n)) if n == "NOPE"));
1242 }
1243
1244 #[tokio::test]
1245 async fn set_same_value_is_ok_not_not_found() {
1246 let store = empty_store();
1247 let pv = Pv::ai("SIM:Z", 1.0);
1248 let any: AnyPv = pv.clone().into();
1249 let rec = any.take_record().unwrap();
1250 store.insert(rec.name.clone(), rec).await;
1251 any.bind(&store);
1252
1253 assert_eq!(pv.set(2.5).await, Ok(()));
1254 assert_eq!(pv.set(2.5).await, Ok(()));
1256 assert_eq!(pv.get().await, Ok(2.5));
1257 }
1258
1259 #[tokio::test]
1260 async fn set_alarm_posts_and_reads_back() {
1261 let store = empty_store();
1262 let pv = Pv::ai("A:1", 1.0);
1263 let any: AnyPv = pv.clone().into();
1264 let rec = any.take_record().unwrap();
1265 store.insert(rec.name.clone(), rec).await;
1266 any.bind(&store);
1267
1268 let mut rx = crate::pvstore::Source::subscribe(&*store, "A:1")
1270 .await
1271 .unwrap();
1272
1273 pv.set_alarm(2, 3, "sensor dead").await.unwrap();
1274 let rec = store.get_record("A:1").await.unwrap();
1275 let nt = rec.to_ntscalar();
1276 assert_eq!(nt.alarm_severity, 2);
1277 assert_eq!(nt.alarm_status, 3);
1278 assert_eq!(nt.alarm_message, "sensor dead");
1279 let posted = rx.try_recv().expect("alarm change must post");
1281 drop(posted);
1282 pv.set_alarm(2, 3, "sensor dead").await.unwrap();
1284 assert!(rx.try_recv().is_err());
1285 let ghost = Pv::ai("A:GHOST", 0.0);
1287 assert_eq!(ghost.set_alarm(1, 0, "x").await, Err(PvError::Unbound));
1288
1289 let missing: Pv<f64> = Pv::attach(&store, "A:1").await.unwrap();
1290 assert_eq!(missing.get().await, Ok(1.0));
1292 }
1293
1294 #[tokio::test]
1295 async fn set_alarm_missing_record_is_not_found() {
1296 let store = empty_store();
1297 let pv = Pv::ai("A:MISSING", 0.0);
1298 let any: AnyPv = pv.clone().into();
1299 any.bind(&store);
1300 assert_eq!(
1301 pv.set_alarm(1, 1, "x").await,
1302 Err(PvError::NotFound("A:MISSING".into()))
1303 );
1304 }
1305
1306 #[tokio::test]
1307 async fn set_alarm_on_enum_and_array_records() {
1308 let store = empty_store();
1309
1310 let mbbo = Pv::mbbo("M:ALM", vec!["Stop".into(), "Run".into()], 0);
1311 let any: AnyPv = mbbo.clone().into();
1312 let rec = any.take_record().unwrap();
1313 store.insert(rec.name.clone(), rec).await;
1314 any.bind(&store);
1315
1316 let wf = PvArray::waveform("W:ALM", ScalarArrayValue::F64(vec![1.0, 2.0]));
1317 let any: AnyPv = wf.clone().into();
1318 let rec = any.take_record().unwrap();
1319 store.insert(rec.name.clone(), rec).await;
1320 any.bind(&store);
1321
1322 assert!(store.set_alarm("M:ALM", 2, 5, "enum fault").await);
1323 assert!(store.set_alarm("W:ALM", 1, 4, "array fault").await);
1324
1325 match store.get_nt("M:ALM").await.unwrap() {
1326 NtPayload::Enum(nt) => {
1327 assert_eq!(nt.alarm.severity, 2);
1328 assert_eq!(nt.alarm.status, 5);
1329 assert_eq!(nt.alarm.message, "enum fault");
1330 }
1331 other => panic!("expected Enum, got {other:?}"),
1332 }
1333 match store.get_nt("W:ALM").await.unwrap() {
1334 NtPayload::ScalarArray(nt) => {
1335 assert_eq!(nt.alarm.severity, 1);
1336 assert_eq!(nt.alarm.status, 4);
1337 assert_eq!(nt.alarm.message, "array fault");
1338 }
1339 other => panic!("expected ScalarArray, got {other:?}"),
1340 }
1341
1342 assert!(!store.set_alarm("M:ALM", 2, 5, "enum fault").await);
1344 }
1345
1346 #[tokio::test]
1347 async fn on_put_callback_travels_to_store() {
1348 let rejected = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1349 let r2 = rejected.clone();
1350 let pv = Pv::ao("SIM:SP", 1.0).on_put(move |_pv, v: f64| {
1351 if v > 100.0 {
1352 r2.store(true, std::sync::atomic::Ordering::SeqCst);
1353 Err("over limit".into())
1354 } else {
1355 Ok(())
1356 }
1357 });
1358 let any: AnyPv = pv.clone().into();
1359 assert!(any.take_validator().is_some());
1360 }
1361
1362 #[tokio::test]
1363 async fn on_put_wrapper_unwraps_structure_wrapped_scalar_put() {
1364 let store = empty_store();
1370 let seen = std::sync::Arc::new(std::sync::Mutex::new(None));
1371 let seen2 = seen.clone();
1372 let pv = Pv::ao("SIM:WRAP", 1.0).on_put(move |_pv, v: f64| {
1373 *seen2.lock().unwrap() = Some(v);
1374 Ok(())
1375 });
1376 let any: AnyPv = pv.clone().into();
1377 let rec = any.take_record().unwrap();
1378 store.insert(rec.name.clone(), rec).await;
1379 let validator = any.take_validator().expect("validator attached");
1380 any.bind(&store);
1381
1382 let dv = spvirit_codec::spvd_decode::DecodedValue::Structure(vec![(
1383 "value".to_string(),
1384 spvirit_codec::spvd_decode::DecodedValue::Float64(42.0),
1385 )]);
1386 let res = validator("SIM:WRAP", &dv);
1387 assert_eq!(res, Ok(()));
1388 assert_eq!(*seen.lock().unwrap(), Some(42.0));
1389 }
1390
1391 #[test]
1392 fn scalar_value_handle_constructors() {
1393 let p = Pv::<ScalarValue>::scalar_out("S:U16", ScalarValue::U16(7));
1394 let rec = p.pending_record().unwrap();
1395 assert_eq!(rec.record_type, crate::types::RecordType::LongOut);
1396 assert_eq!(rec.current_value(), ScalarValue::U16(7));
1397 assert!(rec.writable());
1398
1399 let q = Pv::<ScalarValue>::scalar_in("S:F32", ScalarValue::F32(1.5));
1400 let rec = q.pending_record().unwrap();
1401 assert_eq!(rec.record_type, crate::types::RecordType::Ai);
1402 assert!(!rec.writable());
1403
1404 let s = Pv::<ScalarValue>::scalar_in("S:STR", ScalarValue::Str("x".into()));
1405 assert_eq!(
1406 s.pending_record().unwrap().record_type,
1407 crate::types::RecordType::StringIn
1408 );
1409
1410 let b = Pv::<ScalarValue>::scalar_out("S:B", ScalarValue::Bool(true));
1411 assert_eq!(
1412 b.pending_record().unwrap().record_type,
1413 crate::types::RecordType::Bo
1414 );
1415 }
1416
1417 #[tokio::test]
1418 async fn scalar_value_handle_set_get_preserves_variant() {
1419 let store = empty_store();
1420 let pv = Pv::<ScalarValue>::scalar_out("S:U64", ScalarValue::U64(1));
1421 let any: AnyPv = pv.clone().into();
1422 let rec = any.take_record().unwrap();
1423 store.insert(rec.name.clone(), rec).await;
1424 any.bind(&store);
1425
1426 pv.set(ScalarValue::U64(u64::MAX)).await.unwrap();
1427 assert_eq!(pv.get().await, Ok(ScalarValue::U64(u64::MAX)));
1428 }
1429
1430 #[test]
1431 fn set_scalar_value_same_variant_u64_is_exact() {
1432 let mut rec = make_output_record("S:U64", RecordType::LongOut, ScalarValue::U64(1));
1433 let changed = rec.set_scalar_value(ScalarValue::U64(u64::MAX), true);
1434 assert!(changed);
1435 assert_eq!(rec.current_value(), ScalarValue::U64(u64::MAX));
1436 }
1437
1438 #[test]
1439 fn set_scalar_value_same_variant_f32_is_exact() {
1440 let mut rec = make_output_record("S:F32", RecordType::LongOut, ScalarValue::F32(1.5));
1441 let changed = rec.set_scalar_value(ScalarValue::F32(2.5), true);
1442 assert!(changed);
1443 assert_eq!(rec.current_value(), ScalarValue::F32(2.5));
1444 }
1445
1446 #[test]
1447 fn set_scalar_value_same_variant_i64_is_exact() {
1448 let mut rec = make_output_record("S:I64", RecordType::LongOut, ScalarValue::I64(1));
1449 let changed = rec.set_scalar_value(ScalarValue::I64(i64::MIN), true);
1450 assert!(changed);
1451 assert_eq!(rec.current_value(), ScalarValue::I64(i64::MIN));
1452 }
1453
1454 #[test]
1455 fn set_scalar_value_cross_variant_preserves_target_variant_from_i32() {
1456 let mut rec = make_output_record("S:U16", RecordType::LongOut, ScalarValue::U16(5));
1457 let changed = rec.set_scalar_value(ScalarValue::I32(42), true);
1458 assert!(changed);
1459 assert_eq!(rec.current_value(), ScalarValue::U16(42));
1460 }
1461
1462 #[test]
1463 fn set_scalar_value_cross_variant_preserves_target_variant_from_f64() {
1464 let mut rec = make_output_record("S:U16", RecordType::LongOut, ScalarValue::U16(5));
1465 let changed = rec.set_scalar_value(ScalarValue::F64(7.0), true);
1466 assert!(changed);
1467 assert_eq!(rec.current_value(), ScalarValue::U16(7));
1468 }
1469
1470 #[test]
1471 fn set_scalar_value_unchanged_u64_returns_false() {
1472 let mut rec = make_output_record("S:U64B", RecordType::LongOut, ScalarValue::U64(5));
1473 let changed = rec.set_scalar_value(ScalarValue::U64(5), true);
1474 assert!(!changed);
1475 assert_eq!(rec.current_value(), ScalarValue::U64(5));
1476 }
1477
1478 #[test]
1479 fn scalar_value_from_decoded_maps_one_to_one() {
1480 assert_eq!(
1481 ScalarValue::from_decoded(&DecodedValue::UInt32(7)),
1482 Some(ScalarValue::U32(7))
1483 );
1484 assert_eq!(
1485 ScalarValue::from_decoded(&DecodedValue::Int8(-3)),
1486 Some(ScalarValue::I8(-3))
1487 );
1488 assert_eq!(
1489 ScalarValue::from_decoded(&DecodedValue::Boolean(true)),
1490 Some(ScalarValue::Bool(true))
1491 );
1492 assert_eq!(
1493 ScalarValue::from_decoded(&DecodedValue::String("hi".into())),
1494 Some(ScalarValue::Str("hi".into()))
1495 );
1496 assert!(ScalarValue::from_decoded(&DecodedValue::Null).is_none());
1497 }
1498}