use crate::error::{CaError, CaResult};
use crate::server::record::{
FieldMetadataOverride, Ftype, InputFetchPolicy, InputLinkRequest, LinkReadAs, OutTarget,
ProcessOutcome, Record,
};
use crate::types::{EpicsValue, PvString};
const NUM_ARGS: usize = 21;
const FTYPE_DOUBLE: i16 = 10;
const SUFFIX: [char; NUM_ARGS] = [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U',
];
const EFLG_NEVER: i16 = 0;
const EFLG_ON_CHANGE: i16 = 1;
const EFLG_ALWAYS: i16 = 2;
const ASUB_EFLG_CHOICES: &[&str] = &["NEVER", "ON CHANGE", "ALWAYS"];
const ASUB_LFLG_CHOICES: &[&str] = &["IGNORE", "READ"];
const LFLG_IGNORE: i16 = 0;
fn asub_output_event_fields() -> &'static [&'static str] {
use std::sync::OnceLock;
static FIELDS: OnceLock<Vec<&'static str>> = OnceLock::new();
FIELDS.get_or_init(|| {
SUFFIX
.iter()
.flat_map(|&c| {
[
Box::leak(format!("VAL{c}").into_boxed_str()) as &'static str,
Box::leak(format!("NEV{c}").into_boxed_str()) as &'static str,
]
})
.collect()
})
}
fn asub_posted_fields() -> &'static [&'static str] {
use std::sync::OnceLock;
static FIELDS: OnceLock<Vec<&'static str>> = OnceLock::new();
FIELDS.get_or_init(|| {
std::iter::once("VAL")
.chain(asub_output_event_fields().iter().copied())
.collect()
})
}
pub struct ASubRecord {
pub val: i32,
pub snam: PvString,
pub inam: PvString,
pub inp: [String; NUM_ARGS],
pub a: [EpicsValue; NUM_ARGS],
pub vala: [EpicsValue; NUM_ARGS],
pub out: [String; NUM_ARGS],
pub fta: [i16; NUM_ARGS],
pub ftva: [i16; NUM_ARGS],
pub noa: [i32; NUM_ARGS],
pub nova: [i32; NUM_ARGS],
pub nea: [i32; NUM_ARGS],
pub neva: [i32; NUM_ARGS],
pub brsv: i16,
pub prec: i16,
pub eflg: i16,
pub lflg: i16,
pub subl: String,
pub onam: PvString,
sub_status: i64,
}
fn asub_output_links() -> &'static [(&'static str, &'static str)] {
use std::sync::OnceLock;
static PAIRS: OnceLock<Vec<(&'static str, &'static str)>> = OnceLock::new();
PAIRS.get_or_init(|| {
SUFFIX
.iter()
.map(|&c| {
let link: &'static str = Box::leak(format!("OUT{c}").into_boxed_str());
let val: &'static str = Box::leak(format!("VAL{c}").into_boxed_str());
(link, val)
})
.collect()
})
}
impl Default for ASubRecord {
fn default() -> Self {
Self {
val: 0,
snam: PvString::new(),
inam: PvString::new(),
inp: std::array::from_fn(|_| String::new()),
a: std::array::from_fn(|_| channel_default(FTYPE_DOUBLE, 1)),
vala: std::array::from_fn(|_| channel_default(FTYPE_DOUBLE, 1)),
out: std::array::from_fn(|_| String::new()),
fta: [FTYPE_DOUBLE; NUM_ARGS],
ftva: [FTYPE_DOUBLE; NUM_ARGS],
noa: [1; NUM_ARGS],
nova: [1; NUM_ARGS],
nea: [1; NUM_ARGS],
neva: [1; NUM_ARGS],
brsv: 0,
prec: 0,
eflg: EFLG_ON_CHANGE,
lflg: 0,
subl: String::new(),
onam: PvString::new(),
sub_status: SUBROUTINE_NOT_RUN,
}
}
}
const SUBROUTINE_NOT_RUN: i64 = -1;
fn parse_channel(name: &str) -> Option<(&'static str, usize)> {
const PREFIXES: [&str; 7] = ["INP", "OUT", "VAL", "FTV", "FT", "NOV", "NO"];
if name.len() == 1 {
let c = name.chars().next().unwrap();
return SUFFIX.iter().position(|&s| s == c).map(|i| ("", i));
}
if let Some(rest) = name.strip_prefix("NEV") {
if rest.len() == 1 {
let c = rest.chars().next().unwrap();
return SUFFIX.iter().position(|&s| s == c).map(|i| ("NEV", i));
}
return None;
}
if let Some(rest) = name.strip_prefix("NE") {
if rest.len() == 1 {
let c = rest.chars().next().unwrap();
return SUFFIX.iter().position(|&s| s == c).map(|i| ("NE", i));
}
return None;
}
for prefix in PREFIXES {
if let Some(rest) = name.strip_prefix(prefix) {
if rest.len() == 1 {
let c = rest.chars().next().unwrap();
if let Some(i) = SUFFIX.iter().position(|&s| s == c) {
return Some((prefix, i));
}
}
}
}
None
}
fn channel_ftype(ft: i16) -> Ftype {
Ftype::from_index(ft).unwrap_or(Ftype::Char)
}
fn channel_default(ft: i16, no: i32) -> EpicsValue {
let ftype = channel_ftype(ft);
if no > 1 {
ftype.zeroed(no as usize)
} else {
ftype
.zeroed(1)
.first_element()
.expect("zeroed(1) has element 0")
}
}
fn wrap_one_element(scalar: EpicsValue, ftype: Ftype) -> EpicsValue {
match (ftype, scalar) {
(Ftype::String, EpicsValue::String(v)) => EpicsValue::StringArray(vec![v]),
(Ftype::Char, EpicsValue::Char(v)) => EpicsValue::CharArray(vec![v]),
(Ftype::UChar, EpicsValue::UChar(v)) => EpicsValue::UCharArray(vec![v]),
(Ftype::Short, EpicsValue::Short(v)) => EpicsValue::ShortArray(vec![v]),
(Ftype::UShort, EpicsValue::UShort(v)) => EpicsValue::UShortArray(vec![v]),
(Ftype::Long, EpicsValue::Long(v)) => EpicsValue::LongArray(vec![v]),
(Ftype::ULong, EpicsValue::ULong(v)) => EpicsValue::ULongArray(vec![v]),
(Ftype::Int64, EpicsValue::Int64(v)) => EpicsValue::Int64Array(vec![v]),
(Ftype::UInt64, EpicsValue::UInt64(v)) => EpicsValue::UInt64Array(vec![v]),
(Ftype::Float, EpicsValue::Float(v)) => EpicsValue::FloatArray(vec![v]),
(Ftype::Double, EpicsValue::Double(v)) => EpicsValue::DoubleArray(vec![v]),
(Ftype::Enum, EpicsValue::Enum(v)) => EpicsValue::EnumArray(vec![v]),
(ftype, _) => ftype.zeroed(1),
}
}
enum ChannelDelivery {
Delivered { value: EpicsValue, count: i32 },
Empty,
}
fn shape_channel_value(value: EpicsValue, ft: i16, no: i32) -> ChannelDelivery {
let ftype = channel_ftype(ft);
let elem = ftype.element_type();
if no > 1 {
let converted = value.convert_to(elem);
let mut arr = if converted.is_array() {
converted
} else {
wrap_one_element(converted, ftype)
};
arr.truncate(no as usize);
let count = arr.count() as i32;
if count <= 0 {
return ChannelDelivery::Empty;
}
ChannelDelivery::Delivered { value: arr, count }
} else {
let scalar = match value.first_element() {
Some(first) => first,
None if value.is_array() => return ChannelDelivery::Empty,
None => value,
};
let converted = scalar.convert_to(elem);
let v = match converted.first_element() {
Some(first) => first,
None if converted.is_array() => channel_default(ft, no),
None => converted,
};
ChannelDelivery::Delivered { value: v, count: 1 }
}
}
fn channel_get(v: &EpicsValue) -> EpicsValue {
match v {
EpicsValue::DoubleArray(a) if a.is_empty() => EpicsValue::Double(0.0),
EpicsValue::DoubleArray(a) if a.len() == 1 => EpicsValue::Double(a[0]),
other => other.clone(),
}
}
impl Record for ASubRecord {
fn record_type(&self) -> &'static str {
"aSub"
}
fn link_backed_metadata_field(&self, field: &str) -> Option<String> {
use crate::server::record::{arg_letter_offset, arg_link_field};
let n = NUM_ARGS as u8;
arg_letter_offset(field, "", n)
.map(|i| arg_link_field("INP", i))
.or_else(|| arg_letter_offset(field, "VAL", n).map(|i| arg_link_field("OUT", i)))
}
fn clears_udf(&self) -> bool {
false
}
fn raises_udf_alarm(&self) -> bool {
false
}
fn process(&mut self) -> CaResult<ProcessOutcome> {
Ok(ProcessOutcome::complete())
}
fn dbaddr_capacity(&self, field: &str) -> Option<u32> {
let (prefix, idx) = parse_channel(field)?;
match prefix {
"" => Some(self.noa[idx].max(0) as u32),
"VAL" => Some(self.nova[idx].max(0) as u32),
_ => None,
}
}
fn link_text_ref(&self, link_field: &str) -> Option<&str> {
let [b'I', b'N', b'P', slot] = *link_field.as_bytes() else {
return None;
};
let slot = usize::from(slot.checked_sub(b'A')?);
self.inp.get(slot).map(String::as_str)
}
fn get_field(&self, name: &str) -> Option<EpicsValue> {
match name {
"VAL" => return Some(EpicsValue::Long(self.val)),
"SNAM" => return Some(EpicsValue::String(self.snam.clone())),
"INAM" => return Some(EpicsValue::String(self.inam.clone())),
"BRSV" => return Some(EpicsValue::Short(self.brsv)),
"PREC" => return Some(EpicsValue::Short(self.prec)),
"EFLG" => return Some(EpicsValue::Short(self.eflg)),
"LFLG" => return Some(EpicsValue::Short(self.lflg)),
"SUBL" => return Some(EpicsValue::String(self.subl.clone().into())),
"ONAM" => return Some(EpicsValue::String(self.onam.clone())),
_ => {}
}
let (prefix, idx) = parse_channel(name)?;
Some(match prefix {
"" => self.a[idx].clone(), "INP" => EpicsValue::String(self.inp[idx].clone().into()),
"VAL" => channel_get(&self.vala[idx]),
"OUT" => EpicsValue::String(self.out[idx].clone().into()),
"FT" => EpicsValue::Short(self.fta[idx]),
"FTV" => EpicsValue::Short(self.ftva[idx]),
"NO" => EpicsValue::Long(self.noa[idx]),
"NOV" => EpicsValue::Long(self.nova[idx]),
"NE" => EpicsValue::Long(self.nea[idx]),
"NEV" => EpicsValue::Long(self.neva[idx]),
_ => return None,
})
}
fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
match name {
"VAL" => {
self.val = value
.to_dbf_i32()
.ok_or_else(|| CaError::TypeMismatch(name.into()))?;
return Ok(());
}
"SNAM" => {
return match value {
EpicsValue::String(s) => {
self.snam = s;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
};
}
"INAM" => {
return match value {
EpicsValue::String(s) => {
self.inam = s;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
};
}
"BRSV" => {
self.brsv = value
.to_f64()
.ok_or_else(|| CaError::TypeMismatch("BRSV".into()))?
as i16;
return Ok(());
}
"PREC" => {
self.prec = value
.to_f64()
.ok_or_else(|| CaError::TypeMismatch("PREC".into()))?
as i16;
return Ok(());
}
"EFLG" => {
self.eflg = value
.to_f64()
.ok_or_else(|| CaError::TypeMismatch("EFLG".into()))?
as i16;
return Ok(());
}
"LFLG" => {
self.lflg = value
.to_f64()
.ok_or_else(|| CaError::TypeMismatch("LFLG".into()))?
as i16;
return Ok(());
}
"SUBL" => {
return match value {
EpicsValue::String(s) => {
self.subl = s.as_str_lossy().into_owned();
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
};
}
"ONAM" => {
return match value {
EpicsValue::String(s) => {
self.onam = s;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
};
}
_ => {}
}
let (prefix, idx) =
parse_channel(name).ok_or_else(|| CaError::FieldNotFound(name.to_string()))?;
match prefix {
"" => match shape_channel_value(value, self.fta[idx], self.noa[idx]) {
ChannelDelivery::Delivered { value, count } => {
self.nea[idx] = count;
self.a[idx] = value;
}
ChannelDelivery::Empty => self.nea[idx] = 0,
},
"INP" | "OUT" => {
let s = match value {
EpicsValue::String(s) => s.as_str_lossy().into_owned(),
_ => return Err(CaError::TypeMismatch(name.into())),
};
if prefix == "INP" {
self.inp[idx] = s;
} else {
self.out[idx] = s;
}
}
"VAL" => match shape_channel_value(value, self.ftva[idx], self.nova[idx]) {
ChannelDelivery::Delivered { value, count } => {
self.neva[idx] = count;
self.vala[idx] = value;
}
ChannelDelivery::Empty => self.neva[idx] = 0,
},
"FT" | "FTV" | "NO" | "NOV" | "NE" | "NEV" => {
let v = match value {
EpicsValue::Short(v) => v as i32,
EpicsValue::Long(v) => v,
other => other
.to_f64()
.map(|f| f as i32)
.ok_or_else(|| CaError::TypeMismatch(name.into()))?,
};
match prefix {
"FT" => {
self.fta[idx] = channel_ftype(v as i16).index();
self.a[idx] = channel_default(self.fta[idx], self.noa[idx]);
self.nea[idx] = self.noa[idx].max(1);
}
"NO" => {
self.noa[idx] = v.max(1);
self.a[idx] = channel_default(self.fta[idx], self.noa[idx]);
self.nea[idx] = self.noa[idx];
}
"FTV" => {
self.ftva[idx] = channel_ftype(v as i16).index();
self.vala[idx] = channel_default(self.ftva[idx], self.nova[idx]);
self.neva[idx] = self.nova[idx].max(1);
}
"NOV" => {
self.nova[idx] = v.max(1);
self.vala[idx] = channel_default(self.ftva[idx], self.nova[idx]);
self.neva[idx] = self.nova[idx];
}
"NE" => self.nea[idx] = v,
"NEV" => self.neva[idx] = v,
_ => unreachable!(),
}
}
_ => return Err(CaError::FieldNotFound(name.to_string())),
}
Ok(())
}
fn is_subroutine_name_field(&self, field: &str) -> bool {
field == "SNAM" && self.lflg == LFLG_IGNORE
}
fn menu_field_choices(&self, field: &str) -> Option<&'static [&'static str]> {
match field {
"EFLG" => Some(ASUB_EFLG_CHOICES),
"LFLG" => Some(ASUB_LFLG_CHOICES),
_ => None,
}
}
fn force_posted_fields(&self) -> &'static [&'static str] {
if self.eflg == EFLG_ALWAYS {
asub_output_event_fields()
} else {
&[]
}
}
fn process_posted_fields(&self) -> Option<&'static [&'static str]> {
Some(if self.eflg == EFLG_NEVER {
&["VAL"]
} else {
asub_posted_fields()
})
}
fn field_metadata_override(&self, field: &str) -> Option<FieldMetadataOverride> {
if field.eq_ignore_ascii_case("VAL") {
return Some(FieldMetadataOverride {
precision: Some(self.prec),
..Default::default()
});
}
None
}
fn constant_init_links(&self) -> Vec<crate::server::record::ConstantInitLink> {
let mut seeds = vec![crate::server::record::ConstantInitLink::new("SUBL", "SNAM")];
seeds.extend(crate::server::record::seed_input_links(
self.multi_input_links(),
));
seeds
}
fn set_input_link_slots(&self) -> Option<(u64, u64)> {
crate::server::record::input_link_slots_of(&self.inp)
}
fn multi_input_links(&self) -> &'static [(&'static str, &'static str)] {
use std::sync::OnceLock;
static PAIRS: OnceLock<Vec<(&'static str, &'static str)>> = OnceLock::new();
PAIRS.get_or_init(|| {
SUFFIX
.iter()
.map(|&c| {
let link: &'static str = Box::leak(format!("INP{c}").into_boxed_str());
let val: &'static str = Box::leak(c.to_string().into_boxed_str());
(link, val)
})
.collect()
})
}
fn input_fetch_policy(&self) -> InputFetchPolicy {
InputFetchPolicy::AbortOnFirstFailure
}
fn input_link_request(&self, link_field: &str) -> InputLinkRequest {
let string_channel = parse_channel(link_field)
.filter(|(prefix, _)| *prefix == "INP")
.is_some_and(|(_, idx)| channel_ftype(self.fta[idx]) == Ftype::String);
if string_channel {
InputLinkRequest::FromSource
} else {
InputLinkRequest::As(LinkReadAs::Native)
}
}
fn input_link_answers_fixed_at_type(&self) -> bool {
false
}
fn input_link_read_as_from_source(
&self,
_link_field: &str,
source: &OutTarget,
) -> Option<LinkReadAs> {
if source.element_count <= 1 {
Some(LinkReadAs::String)
} else {
Some(LinkReadAs::Native)
}
}
fn set_subroutine_status(&mut self, status: i64) {
self.sub_status = status;
}
fn declares_multi_output_links(&self) -> bool {
true
}
fn multi_output_links(&self) -> &[(&'static str, &'static str)] {
if self.sub_status == 0 {
asub_output_links()
} else {
&[]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn channels_m_through_u_present() {
let mut rec = ASubRecord::default();
for name in ["M", "Q", "U"] {
rec.put_field(name, EpicsValue::Double(3.0)).unwrap();
assert_eq!(rec.get_field(name), Some(EpicsValue::Double(3.0)));
}
for (name, nov) in [("VALM", "NOVM"), ("VALU", "NOVU")] {
rec.put_field(nov, EpicsValue::Long(2)).unwrap();
rec.put_field(name, EpicsValue::DoubleArray(vec![1.0, 2.0]))
.unwrap();
assert_eq!(
rec.get_field(name),
Some(EpicsValue::DoubleArray(vec![1.0, 2.0]))
);
}
for name in ["NOM", "NOVU", "INPR", "OUTT"] {
assert!(rec.get_field(name).is_some(), "channel field must exist");
}
}
#[test]
fn non_double_channel_keeps_native_type() {
let mut rec = ASubRecord::default();
rec.put_field("FTA", EpicsValue::Short(0)).unwrap(); rec.put_field("A", EpicsValue::String("hello".into()))
.unwrap();
assert_eq!(rec.get_field("A"), Some(EpicsValue::String("hello".into())));
assert_eq!(rec.fta[0], 0);
rec.put_field("FTC", EpicsValue::Short(5)).unwrap(); rec.put_field("NOC", EpicsValue::Long(3)).unwrap();
rec.put_field("C", EpicsValue::LongArray(vec![10, 20, 30]))
.unwrap();
assert_eq!(
rec.get_field("C"),
Some(EpicsValue::LongArray(vec![10, 20, 30]))
);
assert_eq!(rec.get_field("NEC"), Some(EpicsValue::Long(3)));
}
#[test]
fn ftx_nox_declare_the_input_cell() {
let mut rec = ASubRecord::default();
rec.put_field("FTA", EpicsValue::Short(10)).unwrap(); rec.put_field("NOA", EpicsValue::Long(4)).unwrap();
assert_eq!(
rec.get_field("A"),
Some(EpicsValue::DoubleArray(vec![0.0; 4]))
);
assert_eq!(
rec.get_field("NEA"),
Some(EpicsValue::Long(4)),
"C initFields: NEx starts at NOx"
);
rec.put_field("FTB", EpicsValue::Short(0)).unwrap(); assert_eq!(rec.get_field("B"), Some(EpicsValue::String("".into())));
rec.put_field("FTC", EpicsValue::Short(99)).unwrap(); rec.put_field("NOC", EpicsValue::Long(2)).unwrap();
assert_eq!(
rec.get_field("FTC"),
Some(EpicsValue::Short(1)),
"aSubRecord.c:186-187: FTx past DBF_ENUM becomes CHAR"
);
assert_eq!(rec.get_field("C"), Some(EpicsValue::CharArray(vec![0, 0])));
}
#[test]
fn input_puts_convert_and_clamp_to_the_declared_buffer() {
let mut rec = ASubRecord::default();
rec.put_field("FTA", EpicsValue::Short(5)).unwrap(); rec.put_field("NOA", EpicsValue::Long(3)).unwrap();
rec.put_field("A", EpicsValue::DoubleArray(vec![1.9, 2.2, 3.7, 4.4]))
.unwrap();
assert_eq!(
rec.get_field("A"),
Some(EpicsValue::LongArray(vec![1, 2, 3]))
);
assert_eq!(rec.get_field("NEA"), Some(EpicsValue::Long(3)));
rec.put_field("A", EpicsValue::Double(7.0)).unwrap();
assert_eq!(rec.get_field("A"), Some(EpicsValue::LongArray(vec![7])));
assert_eq!(rec.get_field("NEA"), Some(EpicsValue::Long(1)));
rec.put_field("B", EpicsValue::DoubleArray(vec![5.5, 6.6]))
.unwrap();
assert_eq!(rec.get_field("B"), Some(EpicsValue::Double(5.5)));
assert_eq!(rec.get_field("NEB"), Some(EpicsValue::Long(1)));
rec.put_field("B", EpicsValue::DoubleArray(vec![])).unwrap();
assert_eq!(
rec.get_field("B"),
Some(EpicsValue::Double(5.5)),
"one-element cell retains the previous delivery"
);
assert_eq!(rec.get_field("NEB"), Some(EpicsValue::Long(0)));
rec.put_field("A", EpicsValue::DoubleArray(vec![])).unwrap();
assert_eq!(
rec.get_field("A"),
Some(EpicsValue::LongArray(vec![7])),
"NOx > 1 cell retains the previous delivery"
);
assert_eq!(rec.get_field("NEA"), Some(EpicsValue::Long(0)));
rec.put_field("A", EpicsValue::DoubleArray(vec![8.0, 9.0]))
.unwrap();
assert_eq!(rec.get_field("A"), Some(EpicsValue::LongArray(vec![8, 9])));
assert_eq!(rec.get_field("NEA"), Some(EpicsValue::Long(2)));
}
#[test]
fn empty_output_write_retains_the_output_cell() {
let mut rec = ASubRecord::default();
rec.put_field("FTVA", EpicsValue::Short(10)).unwrap(); rec.put_field("NOVA", EpicsValue::Long(3)).unwrap();
rec.put_field("VALA", EpicsValue::DoubleArray(vec![1.0, 2.0]))
.unwrap();
assert_eq!(rec.get_field("NEVA"), Some(EpicsValue::Long(2)));
rec.put_field("VALA", EpicsValue::DoubleArray(vec![]))
.unwrap();
assert_eq!(
rec.get_field("VALA"),
Some(EpicsValue::DoubleArray(vec![1.0, 2.0]))
);
assert_eq!(rec.get_field("NEVA"), Some(EpicsValue::Long(0)));
}
#[test]
fn ftvx_novx_declare_the_output_cell() {
let mut rec = ASubRecord::default();
rec.put_field("FTVA", EpicsValue::Short(10)).unwrap(); rec.put_field("NOVA", EpicsValue::Long(3)).unwrap();
assert_eq!(
rec.get_field("VALA"),
Some(EpicsValue::DoubleArray(vec![0.0; 3]))
);
assert_eq!(rec.get_field("NEVA"), Some(EpicsValue::Long(3)));
rec.put_field("VALA", EpicsValue::LongArray(vec![1, 2, 3, 4]))
.unwrap();
assert_eq!(
rec.get_field("VALA"),
Some(EpicsValue::DoubleArray(vec![1.0, 2.0, 3.0])),
"a subroutine write converts element-wise and clamps to NOVx"
);
assert_eq!(rec.get_field("NEVA"), Some(EpicsValue::Long(3)));
rec.put_field("FTVB", EpicsValue::Short(0)).unwrap(); rec.put_field("VALB", EpicsValue::String("done".into()))
.unwrap();
assert_eq!(
rec.get_field("VALB"),
Some(EpicsValue::String("done".into())),
"a declared STRING output keeps its string"
);
}
#[test]
fn string_channels_read_scalar_links_as_strings() {
let mut rec = ASubRecord::default();
rec.put_field("FTC", EpicsValue::Short(0)).unwrap(); let scalar = OutTarget {
element_count: 1,
..OutTarget::UNRESOLVED
};
assert_eq!(
rec.input_link_request("INPC"),
InputLinkRequest::FromSource,
"a STRING channel's read depends on whether the source is scalar"
);
assert_eq!(
rec.input_link_read_as_from_source("INPC", &scalar),
Some(LinkReadAs::String)
);
let array = OutTarget {
element_count: 8,
..OutTarget::UNRESOLVED
};
assert_eq!(
rec.input_link_read_as_from_source("INPC", &array),
Some(LinkReadAs::Native)
);
assert_eq!(
rec.input_link_request("INPA"),
InputLinkRequest::As(LinkReadAs::Native),
"a DOUBLE channel reads native, source unasked"
);
assert_eq!(
rec.input_link_request("SUBL"),
InputLinkRequest::As(LinkReadAs::Native),
"only INPx links carry a channel FTx"
);
}
#[test]
fn twenty_one_multi_input_links() {
let rec = ASubRecord::default();
assert_eq!(rec.multi_input_links().len(), 21);
}
#[test]
fn prec_round_trips_and_serves_val_precision() {
let mut rec = ASubRecord::default();
assert_eq!(rec.get_field("PREC"), Some(EpicsValue::Short(0)));
assert_eq!(
rec.field_metadata_override("VAL").and_then(|o| o.precision),
Some(0)
);
rec.put_field("PREC", EpicsValue::Short(4)).unwrap();
assert_eq!(rec.get_field("PREC"), Some(EpicsValue::Short(4)));
assert_eq!(
rec.field_metadata_override("VAL").and_then(|o| o.precision),
Some(4),
"PREC must be served as VAL display precision"
);
assert!(rec.field_metadata_override("SNAM").is_none());
}
#[test]
fn eflg_drives_output_event_posting_policy() {
let mut rec = ASubRecord::default();
assert_eq!(rec.eflg, EFLG_ON_CHANGE);
assert!(rec.force_posted_fields().is_empty());
let posted = rec.process_posted_fields().expect("aSub closes the set");
assert_eq!(posted.len(), NUM_ARGS * 2 + 1);
assert!(posted.contains(&"VAL"));
assert!(posted.contains(&"VALA"));
assert!(posted.contains(&"NEVU"));
assert!(!posted.contains(&"A"));
assert!(!posted.contains(&"NEA"));
rec.put_field("EFLG", EpicsValue::Short(EFLG_ALWAYS))
.unwrap();
assert_eq!(rec.get_field("EFLG"), Some(EpicsValue::Short(2)));
assert_eq!(rec.force_posted_fields().len(), NUM_ARGS * 2);
assert!(rec.force_posted_fields().contains(&"VALA"));
assert!(rec.force_posted_fields().contains(&"NEVU"));
assert_eq!(
rec.process_posted_fields().map(<[&str]>::len),
Some(NUM_ARGS * 2 + 1)
);
rec.put_field("EFLG", EpicsValue::Short(EFLG_NEVER))
.unwrap();
assert!(rec.force_posted_fields().is_empty());
assert_eq!(rec.process_posted_fields(), Some(&["VAL"][..]));
}
#[test]
fn eflg_loads_from_menu_label() {
use crate::server::db_loader::{DbFieldDef, apply_fields, create_record};
let mut rec = create_record("aSub").unwrap();
let mut common = Vec::new();
apply_fields(&mut rec, &[DbFieldDef::new("EFLG", "ALWAYS")], &mut common)
.expect("EFLG menu label must load");
assert_eq!(rec.get_field("EFLG"), Some(EpicsValue::Short(2)));
}
#[test]
fn brsv_round_trips() {
let mut rec = ASubRecord::default();
assert_eq!(rec.get_field("BRSV"), Some(EpicsValue::Short(0)));
rec.put_field("BRSV", EpicsValue::Short(3)).unwrap();
assert_eq!(rec.get_field("BRSV"), Some(EpicsValue::Short(3)));
}
#[test]
fn lflg_subl_onam_round_trip() {
let mut rec = ASubRecord::default();
assert_eq!(rec.get_field("LFLG"), Some(EpicsValue::Short(0)));
assert_eq!(rec.get_field("SUBL"), Some(EpicsValue::String("".into())));
assert_eq!(rec.get_field("ONAM"), Some(EpicsValue::String("".into())));
rec.put_field("LFLG", EpicsValue::Short(1)).unwrap();
rec.put_field("SUBL", EpicsValue::String("HOLDER".into()))
.unwrap();
rec.put_field("ONAM", EpicsValue::String("prev".into()))
.unwrap();
assert_eq!(rec.get_field("LFLG"), Some(EpicsValue::Short(1)));
assert_eq!(
rec.get_field("SUBL"),
Some(EpicsValue::String("HOLDER".into()))
);
assert_eq!(
rec.get_field("ONAM"),
Some(EpicsValue::String("prev".into()))
);
}
#[test]
fn snam_is_a_subroutine_name_field_only_in_ignore_mode() {
let mut rec = ASubRecord::default();
assert_eq!(rec.lflg, LFLG_IGNORE);
assert!(rec.is_subroutine_name_field("SNAM"));
assert!(!rec.is_subroutine_name_field("INAM"));
assert!(!rec.is_subroutine_name_field("VAL"));
rec.put_field("LFLG", EpicsValue::Short(1)).unwrap(); assert!(
!rec.is_subroutine_name_field("SNAM"),
"READ mode reads the name from SUBL at process time; a SNAM put is not validated"
);
}
#[test]
fn lflg_subl_load_from_db() {
use crate::server::db_loader::{DbFieldDef, apply_fields, create_record};
let mut rec = create_record("aSub").unwrap();
let mut common = Vec::new();
apply_fields(
&mut rec,
&[
DbFieldDef::new("LFLG", "READ"),
DbFieldDef::new("SUBL", "NAME_HOLDER"),
],
&mut common,
)
.expect("LFLG menu label and SUBL link must load");
assert_eq!(rec.get_field("LFLG"), Some(EpicsValue::Short(1)));
assert_eq!(
rec.get_field("SUBL"),
Some(EpicsValue::String("NAME_HOLDER".into()))
);
}
}