use crate::error::{CaError, CaResult};
use crate::server::snapshot::Snapshot;
use crate::types::EpicsValue;
use super::PvDatabase;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProcessMode {
#[default]
Passive,
Force,
Inhibit,
}
fn check_put_disabled(
instance: &crate::server::record::RecordInstance,
field_upper: &str,
) -> CaResult<()> {
if instance.common.disp != 0 && field_upper != "DISP" {
return Err(CaError::PutDisabled(field_upper.to_string()));
}
Ok(())
}
fn check_no_mod(instance: &crate::server::record::RecordInstance, field: &str) -> CaResult<()> {
if instance.is_no_mod(field) {
return Err(CaError::ReadOnlyField(field.to_string()));
}
Ok(())
}
fn check_no_mod_in_db_put(
instance: &crate::server::record::RecordInstance,
field: &str,
) -> CaResult<()> {
check_no_mod(instance, field).inspect_err(|_| {
crate::server::recgbl::rec_gbl_dbaddr_error(
"Attempt to modify noMod field",
&instance.name,
field,
"dbPut",
);
})
}
fn check_not_link_field(
instance: &crate::server::record::RecordInstance,
field: &str,
) -> CaResult<()> {
if crate::types::dbf_link_class(instance.record.record_type(), field).is_some() {
return Err(CaError::BadDbrType(format!(
"dbPut: {field} is a link field; only dbPutField changes link fields"
)));
}
Ok(())
}
fn check_link_put(
record_type: &str,
dtyp: &str,
field: &str,
value: &EpicsValue,
) -> CaResult<Option<EpicsValue>> {
if crate::types::dbf_link_class(record_type, field).is_none() {
return Ok(None);
}
let bad_type = || {
CaError::BadDbrType(format!(
"dbPutFieldLink: {field} takes a string or a NUL-terminated char array"
))
};
let text = match value {
EpicsValue::String(s) => s.as_str_lossy().into_owned(),
EpicsValue::StringArray(v) => v
.first()
.map(|s| s.as_str_lossy().into_owned())
.unwrap_or_default(),
EpicsValue::Char(b) | EpicsValue::UChar(b) => {
if *b != 0 {
return Err(bad_type());
}
String::new()
}
EpicsValue::CharArray(b) | EpicsValue::UCharArray(b) => {
if b.last() != Some(&0) {
return Err(bad_type());
}
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
String::from_utf8_lossy(&b[..end]).into_owned()
}
_ => return Err(bad_type()),
};
crate::server::record::check_link_assignment(record_type, Some(dtyp), field, &text)?;
Ok(Some(EpicsValue::String(text.into())))
}
pub(crate) fn put_drives_processing_of(
instance: &crate::server::record::RecordInstance,
field: &str,
) -> bool {
field == "PROC"
|| (instance.common.scan == crate::server::record::ScanType::Passive
&& (field == "UDF" || instance.record.processes_after_put(field)))
}
fn emit_cycle_posts(
instance: &mut crate::server::record::RecordInstance,
backing: crate::server::database::LinkBacking<'_>,
) {
use crate::server::record::{CyclePostMask, EventMask};
for (sf, cycle_mask) in instance.record.take_cycle_posted_fields() {
let mask = match cycle_mask {
CyclePostMask::Value => EventMask::VALUE,
CyclePostMask::ValueLog | CyclePostMask::MonitorValueLog => {
EventMask::VALUE | EventMask::LOG
}
};
instance.notify_field_backed(sf, mask, backing);
}
}
fn snam_special_after_put(
db: &PvDatabase,
instance: &mut crate::server::record::RecordInstance,
field: &str,
) -> CaResult<()> {
if !instance.record.is_subroutine_name_field(field) {
return Ok(());
}
let Some(EpicsValue::String(stored)) = instance.record.get_field(field) else {
return Ok(());
};
let name = stored.as_str_lossy();
if name.is_empty() {
if instance.record.parks_pact() {
crate::runtime::log::errlog_printf(&format!("{}.SNAM is empty\n", instance.name));
} else {
instance.subroutine = None;
}
return Ok(());
}
let resolved = db.find_subroutine_named(name.as_ref());
let bad_sub = resolved.is_none();
instance.subroutine = resolved;
if bad_sub {
return Err(CaError::BadField("SNAM: Subroutine not found".into()));
}
Ok(())
}
fn special_after_put(
db: &PvDatabase,
instance: &mut crate::server::record::RecordInstance,
field: &str,
out: &mut Vec<crate::server::record::ProcessAction>,
backing: crate::server::database::LinkBacking<'_>,
) -> CaResult<crate::server::record::CommonFieldPutResult> {
let mut status = instance.record.special(field, true);
if status.is_ok() {
status = snam_special_after_put(db, instance, field);
}
out.extend(instance.record.take_special_actions());
if instance.record.take_udf_clear() {
instance.common.udf = 0;
}
if status.is_err() {
emit_cycle_posts(instance, backing);
}
status?;
if let Some(value_field) =
crate::server::record::reseed_constant_input_link(&mut *instance.record, field)
{
let mask = instance.record.special_reseed_post_mask();
instance.notify_field_backed(value_field, mask, backing);
}
if pact_park_field(&*instance.record, field) && instance.record.parks_pact() {
instance.enter_pact();
}
Ok(if field == "SIMM" {
instance.rec_gbl_check_simm()
} else {
crate::server::record::CommonFieldPutResult::NoChange
})
}
fn special_before_put(instance: &mut crate::server::record::RecordInstance, field: &str) {
if field == "SIMM" {
instance.rec_gbl_save_simm();
}
if pact_park_field(&*instance.record, field)
&& instance.record.parks_pact()
&& instance.is_processing()
{
instance.common.rpro = 0;
let _ = instance.leave_pact();
}
}
fn pact_park_field(record: &dyn crate::server::record::Record, field: &str) -> bool {
record
.pact_park_fields()
.iter()
.any(|f| f.eq_ignore_ascii_case(field))
}
fn commit_special_reset_alarm(
instance: &mut crate::server::record::RecordInstance,
) -> crate::server::recgbl::EventMask {
use crate::server::recgbl::EventMask;
let alarm_result = crate::server::recgbl::rec_gbl_reset_alarms(&mut instance.common);
crate::server::database::processing::alarm_field_posts(&instance.common, &alarm_result)
.for_each(|af, mask| instance.notify_field(af, mask));
if alarm_result.alarm_changed || alarm_result.amsg_changed {
EventMask::ALARM
} else {
EventMask::NONE
}
}
fn coerce_write_value(
record: &dyn crate::server::record::Record,
field: &str,
target: crate::types::DbFieldType,
value: EpicsValue,
) -> CaResult<crate::types::c_parse::Converted> {
crate::server::record::dbput_coerce_value(record, field, target, value)
}
enum PutRequest {
Write(EpicsValue),
StoreNothing { alarm: bool },
}
fn dbput_request(
record: &dyn crate::server::record::Record,
field: &str,
value: EpicsValue,
) -> CaResult<PutRequest> {
use crate::server::record::FieldDeclaration;
let dest_is_array = record.get_field(field).is_some_and(|v| v.is_array());
if value.is_empty_array() {
if !record.field_is_dbaddr(field) {
return Ok(PutRequest::StoreNothing { alarm: true });
}
if !dest_is_array {
return Ok(PutRequest::StoreNothing { alarm: false });
}
}
let target = record
.get_field(field)
.map(|v| v.db_field_type())
.or_else(|| crate::server::record::record_instance::declared_field_type_of(record, field));
match target {
Some(target) => match coerce_write_value(record, field, target, value)? {
crate::types::c_parse::Converted::Stored(v) => Ok(PutRequest::Write(v)),
crate::types::c_parse::Converted::Unchanged => {
Ok(PutRequest::StoreNothing { alarm: false })
}
},
None => Ok(PutRequest::Write(value)),
}
}
fn clear_udf_on_value_put(instance: &mut crate::server::record::RecordInstance, field: &str) {
if instance.record.is_udf_defining_put(field) {
instance.common.udf = 0;
}
}
fn set_empty_request_alarm(instance: &mut crate::server::record::RecordInstance) {
crate::server::recgbl::rec_gbl_set_sevr(
&mut instance.common,
crate::server::recgbl::alarm_status::LINK_ALARM,
crate::server::record::AlarmSeverity::Invalid,
);
}
enum NotifyRequest {
None,
New,
Deferred(crate::runtime::sync::oneshot::Sender<()>),
}
#[derive(Clone, Copy)]
enum NotifyArrival {
Fresh,
Replay,
}
impl NotifyArrival {
fn defers(self, record: &crate::server::record::RecordInstance) -> bool {
match self {
NotifyArrival::Fresh => record.notify_put_is_owned(),
NotifyArrival::Replay => record.notify.is_some(),
}
}
}
impl NotifyRequest {
fn wants_notify(&self) -> bool {
!matches!(self, NotifyRequest::None)
}
fn is_restart(&self) -> bool {
matches!(self, NotifyRequest::Deferred(_))
}
#[allow(clippy::type_complexity)]
fn into_completion(
self,
) -> Option<(
crate::runtime::sync::oneshot::Sender<()>,
Option<crate::runtime::sync::oneshot::Receiver<()>>,
)> {
match self {
NotifyRequest::None => None,
NotifyRequest::New => {
let (tx, rx) = crate::runtime::sync::oneshot::channel();
Some((tx, Some(rx)))
}
NotifyRequest::Deferred(tx) => Some((tx, None)),
}
}
}
struct ClientAwaitingNotify<'a> {
db: &'a PvDatabase,
record: &'a str,
rx: crate::runtime::sync::oneshot::Receiver<()>,
answered: bool,
}
impl Drop for ClientAwaitingNotify<'_> {
fn drop(&mut self) {
if self.answered {
return;
}
self.rx.close();
self.db.cancel_unanswerable_notify(self.record);
}
}
#[must_use = "dropping the claim releases the record's put-notify slot"]
struct NotifyClaim<'a> {
rec: &'a std::sync::Arc<crate::server::record::RecordCell>,
set: Option<std::sync::Arc<crate::server::record::NotifyWaitSet>>,
}
impl NotifyClaim<'_> {
fn commit(mut self) -> std::sync::Arc<crate::server::record::NotifyWaitSet> {
self.set
.take()
.expect("a claim is committed exactly once, by value")
}
fn commit_without_waiting(self) {
let _ = self.commit();
}
}
impl Drop for NotifyClaim<'_> {
fn drop(&mut self) {
if let Some(set) = self.set.take() {
self.rec.write().abandon_put_notify(&set);
}
}
}
fn array_nord_before_put(
instance: &crate::server::record::RecordInstance,
field: &str,
) -> Option<EpicsValue> {
if field == "VAL" {
instance.record.get_field("NORD")
} else {
None
}
}
fn post_array_info(
instance: &mut crate::server::record::RecordInstance,
old_nord: &Option<EpicsValue>,
origin: u64,
backing: crate::server::database::LinkBacking<'_>,
) {
let Some(old) = old_nord else { return };
let moved = instance
.record
.get_field("NORD")
.is_some_and(|new| new != *old);
if moved {
instance.notify_field_with_origin(
"NORD",
crate::server::recgbl::EventMask::VALUE | crate::server::recgbl::EventMask::LOG,
origin,
backing,
);
}
}
fn dbput_post_put_field(
instance: &mut crate::server::record::RecordInstance,
field: &str,
backing: crate::server::database::LinkBacking<'_>,
) {
let suppress = field == instance.record.primary_field()
&& instance
.record
.process_passive_fields()
.iter()
.any(|f| f.eq_ignore_ascii_case(field));
if !suppress {
instance.cleanup_subscribers();
instance.notify_field_backed(
field,
crate::server::recgbl::EventMask::VALUE | crate::server::recgbl::EventMask::LOG,
backing,
);
}
}
impl PvDatabase {
pub fn get_pv_blocking(&self, name: &str) -> CaResult<EpicsValue> {
self.get_pv(name)
}
pub fn get_pv(&self, name: &str) -> CaResult<EpicsValue> {
let (base, field) = super::parse_pv_name(name);
let field = field.to_ascii_uppercase();
let simple = self.inner.simple_pvs.lock().get(name).cloned();
if let Some(pv) = simple {
return Ok(pv.get());
}
if let Some(rec) = self.get_record(base) {
let instance = rec.read();
return self.read_resolved_field(&instance, &field, name);
}
Err(CaError::ChannelNotFound(name.to_string()))
}
pub(crate) fn read_resolved_field(
&self,
instance: &crate::server::record::RecordInstance,
field: &str,
name: &str,
) -> CaResult<EpicsValue> {
let rdes = crate::server::record::record_instance::RecordDesc::of(&*instance.record);
let addr = crate::server::record::record_instance::FieldAddr::resolve(&rdes, field);
self.read_resolved_field_at(instance, field, name, addr)
}
pub(crate) fn read_resolved_field_at(
&self,
instance: &crate::server::record::RecordInstance,
field: &str,
name: &str,
addr: crate::server::record::record_instance::FieldAddr,
) -> CaResult<EpicsValue> {
if let Some(value) = self.read_field_at(instance, field, addr) {
return Ok(value);
}
Err(if addr.declared {
CaError::BadDbrType(format!(
"dbGet: {name} is declared but has no readable value"
))
} else {
CaError::ChannelNotFound(name.to_string())
})
}
#[inline]
pub(crate) fn read_field_at(
&self,
instance: &crate::server::record::RecordInstance,
field: &str,
addr: crate::server::record::record_instance::FieldAddr,
) -> Option<EpicsValue> {
if !addr.declared
&& let Some(value) = self.record_type_attribute(instance.record.record_type(), field)
{
return Some(EpicsValue::String(value.into()));
}
instance.resolve_field_upper_at(field, addr.desc)
}
pub async fn put_pv(&self, name: &str, value: EpicsValue) -> CaResult<()> {
let (base, field) = super::parse_pv_name(name);
let _record_gate = self.acquire_put_gate(base, field, Some(&value));
self.put_pv_already_locked(name, value)
}
pub fn put_pv_already_locked(&self, name: &str, value: EpicsValue) -> CaResult<()> {
self.put_pv_body(name, value)
}
fn acquire_put_gate(
&self,
record: &str,
field: &str,
value: Option<&EpicsValue>,
) -> Option<PutGate> {
let rec = self.get_record(record)?;
if let Some(target) = value.and_then(|value| self.db_link_put_target(&rec, field, value)) {
let canonical = self
.resolve_alias(record)
.unwrap_or_else(|| record.to_string());
return Some(PutGate::Many {
_held: self.lock_records([canonical, target]),
});
}
Some(PutGate::One {
_held: self.lock_instance(&rec),
})
}
fn db_link_put_target(
&self,
rec: &std::sync::Arc<crate::server::record::RecordCell>,
field: &str,
value: &EpicsValue,
) -> Option<String> {
let EpicsValue::String(text) = value else {
return None;
};
let field_upper = field.to_ascii_uppercase();
let class = {
let guard = rec.read();
crate::types::dbf_link_class(guard.record.record_type(), &field_upper)?
};
let ftype = match class {
crate::types::DbfLinkClass::InLink => crate::server::record::LinkFieldType::In,
crate::types::DbfLinkClass::OutLink => crate::server::record::LinkFieldType::Out,
crate::types::DbfLinkClass::FwdLink => crate::server::record::LinkFieldType::Fwd,
};
let text = text.as_str_lossy();
match crate::server::record::parse_link_field(&text, ftype) {
crate::server::record::ParsedLink::Db(link) => {
let name = self
.resolve_alias(&link.target().record)
.unwrap_or_else(|| link.target().record.clone());
self.get_record_no_resolve(&name).map(|_| name)
}
_ => None,
}
}
pub async fn check_external_put_preconditions(
&self,
record_name: &str,
field: &str,
) -> CaResult<()> {
let field_upper = field.to_ascii_uppercase();
let Some(rec) = self.get_record(record_name) else {
return Ok(());
};
let instance = rec.read();
check_no_mod(&instance, &field_upper)?;
check_put_disabled(&instance, &field_upper)?;
Ok(())
}
pub fn put_drives_processing(&self, record_name: &str, field: &str) -> bool {
let field_upper = field.to_ascii_uppercase();
let Some(rec) = self.get_record(record_name) else {
return false;
};
let instance = rec.read();
put_drives_processing_of(&instance, &field_upper)
}
pub fn is_dbf_link_field(&self, record_name: &str, field: &str) -> bool {
let field_upper = field.to_ascii_uppercase();
let Some(rec) = self.get_record(record_name) else {
return false;
};
let guard = rec.read();
crate::types::dbf_link_class(guard.record.record_type(), &field_upper).is_some()
}
fn put_pv_body(&self, name: &str, value: EpicsValue) -> CaResult<()> {
let (base, field) = super::parse_pv_name(name);
let field = field.to_ascii_uppercase();
let _relink = self.link_field_write(base, &field);
let simple = self.inner.simple_pvs.lock().get(name).cloned();
if let Some(pv) = simple {
pv.set(value);
return Ok(());
}
if let Some(rec) = self.get_record(base) {
let canonical_base: String =
self.resolve_alias(base).unwrap_or_else(|| base.to_string());
use crate::server::record::CommonFieldPutResult;
let link_backing = self.resolve_link_backed_metadata_for_posts(&rec);
let link_backing = link_backing.as_link_backing();
let (common_result, special_actions) = {
let mut instance = rec.write();
check_no_mod_in_db_put(&instance, &field)?;
check_not_link_field(&instance, &field)?;
let request = dbput_request(&*instance.record, &field, value)?;
instance.record.special(&field, false)?;
let prev_value = instance.resolve_field_stored(&field);
let old_nord = array_nord_before_put(&instance, &field);
let mut special_actions = Vec::new();
let common_result = match request {
PutRequest::StoreNothing { alarm } => {
if alarm {
set_empty_request_alarm(&mut instance);
}
clear_udf_on_value_put(&mut instance, &field);
CommonFieldPutResult::NoChange
}
PutRequest::Write(value) => {
special_before_put(&mut instance, &field);
match instance.record.put_field(&field, value.clone()) {
Ok(()) => {
instance.record.on_put(&field);
let result = special_after_put(
self,
&mut instance,
&field,
&mut special_actions,
link_backing,
)?;
clear_udf_on_value_put(&mut instance, &field);
result
}
Err(CaError::FieldNotFound(_)) => {
instance.put_common_field(&field, value)?
}
Err(e) => {
special_after_put(
self,
&mut instance,
&field,
&mut special_actions,
link_backing,
)?;
return Err(e);
}
}
}
};
if instance.record.special_commits_alarms(&field) {
let _ = crate::server::recgbl::rec_gbl_reset_alarms(&mut instance.common);
}
if instance.record.special_checks_alarms(&field) {
let inst = &mut *instance;
inst.record.check_alarms(&mut inst.common);
let _ = crate::server::recgbl::rec_gbl_reset_alarms(&mut inst.common);
}
instance.notify_field_written_if_changed(&field, prev_value.as_ref(), link_backing);
dbput_post_put_field(&mut instance, &field, link_backing);
post_array_info(&mut instance, &old_nord, 0, link_backing);
(common_result, special_actions)
};
match common_result {
CommonFieldPutResult::ScanChanged {
old_scan,
new_scan,
phas,
} => {
self.update_scan_index(&canonical_base, old_scan, new_scan, phas, phas);
}
CommonFieldPutResult::PhasChanged {
scan: s,
old_phas,
new_phas,
} => {
self.update_scan_index(&canonical_base, s, s, old_phas, new_phas);
}
CommonFieldPutResult::NoChange => {}
}
self.run_special_actions(&canonical_base, &rec, special_actions);
if field == "ASG" {
crate::server::access_security::notify_asg_field_changed();
}
return Ok(());
}
Err(CaError::ChannelNotFound(name.to_string()))
}
pub async fn put_pv_and_post(&self, name: &str, value: EpicsValue) -> CaResult<()> {
self.put_pv_and_post_with_origin(name, value, 0).await
}
pub fn post_alarm(&self, name: &str, severity: u16, status: u16) -> CaResult<()> {
let simple = self.inner.simple_pvs.lock().get(name).cloned();
if let Some(pv) = simple {
pv.post_alarm(severity, status);
return Ok(());
}
Err(crate::error::CaError::ChannelNotFound(name.to_string()))
}
pub async fn put_pv_and_post_snapshot(&self, name: &str, snapshot: Snapshot) -> CaResult<()> {
let simple = self.inner.simple_pvs.lock().get(name).cloned();
if let Some(pv) = simple {
pv.set_snapshot(snapshot);
return Ok(());
}
Err(CaError::ChannelNotFound(name.to_string()))
}
pub async fn set_pv_metadata(&self, name: &str, snapshot: &Snapshot) -> CaResult<()> {
let simple = self.inner.simple_pvs.lock().get(name).cloned();
if let Some(pv) = simple {
pv.set_metadata(metadata_from_snapshot(snapshot));
return Ok(());
}
Err(CaError::ChannelNotFound(name.to_string()))
}
pub async fn post_pv_property(&self, name: &str, snapshot: Snapshot) -> CaResult<()> {
let simple = self.inner.simple_pvs.lock().get(name).cloned();
if let Some(pv) = simple {
pv.set_metadata(metadata_from_snapshot(&snapshot));
pv.post_property(snapshot).await;
return Ok(());
}
Err(CaError::ChannelNotFound(name.to_string()))
}
pub async fn put_pv_and_post_with_origin(
&self,
name: &str,
value: EpicsValue,
origin: u64,
) -> CaResult<()> {
let (base, field) = super::parse_pv_name(name);
let field = field.to_ascii_uppercase();
let simple = self.inner.simple_pvs.lock().get(name).cloned();
if let Some(pv) = simple {
pv.set_with_origin(value, origin);
return Ok(());
}
if let Some(rec) = self.get_record(base) {
let _record_gate = self.acquire_put_gate(base, &field, Some(&value));
let _relink = self.link_field_write(base, &field);
let canonical_base: String =
self.resolve_alias(base).unwrap_or_else(|| base.to_string());
use crate::server::record::CommonFieldPutResult;
let link_backing = self.resolve_link_backed_metadata_for_posts(&rec);
let link_backing = link_backing.as_link_backing();
let (common_result, special_actions) = {
let mut instance = rec.write();
check_no_mod_in_db_put(&instance, &field)?;
check_not_link_field(&instance, &field)?;
let request = dbput_request(&*instance.record, &field, value)?;
instance.record.special(&field, false)?;
let old_value = instance.resolve_field_stored(&field);
let old_stat = instance.common.stat;
let old_sevr = instance.common.sevr;
let old_nord = array_nord_before_put(&instance, &field);
let mut special_actions = Vec::new();
let common_result = match request {
PutRequest::StoreNothing { alarm } => {
if alarm {
set_empty_request_alarm(&mut instance);
}
clear_udf_on_value_put(&mut instance, &field);
CommonFieldPutResult::NoChange
}
PutRequest::Write(value) => {
special_before_put(&mut instance, &field);
match instance.record.put_field(&field, value.clone()) {
Ok(()) => {
instance.record.on_put(&field);
let result = special_after_put(
self,
&mut instance,
&field,
&mut special_actions,
link_backing,
)?;
clear_udf_on_value_put(&mut instance, &field);
result
}
Err(CaError::FieldNotFound(_)) => {
instance.put_common_field(&field, value)?
}
Err(e) => {
special_after_put(
self,
&mut instance,
&field,
&mut special_actions,
link_backing,
)?;
return Err(e);
}
}
}
};
instance.notify_field_written_if_changed(&field, old_value.as_ref(), link_backing);
let new_value = instance.resolve_field_stored(&field);
let value_changed = old_value != new_value;
let alarm_changed =
old_stat != instance.common.stat || old_sevr != instance.common.sevr;
let nord_changed =
old_nord.is_some() && instance.record.get_field("NORD") != old_nord;
if value_changed || alarm_changed || nord_changed {
instance.common.time = crate::runtime::general_time::get_current();
instance.cleanup_subscribers();
if value_changed || alarm_changed {
instance.notify_field_with_origin(
&field,
crate::server::recgbl::EventMask::VALUE
| crate::server::recgbl::EventMask::LOG
| crate::server::recgbl::EventMask::ALARM,
origin,
link_backing,
);
}
post_array_info(&mut instance, &old_nord, origin, link_backing);
}
(common_result, special_actions)
};
match common_result {
CommonFieldPutResult::ScanChanged {
old_scan,
new_scan,
phas,
} => {
self.update_scan_index(&canonical_base, old_scan, new_scan, phas, phas);
}
CommonFieldPutResult::PhasChanged {
scan: s,
old_phas,
new_phas,
} => {
self.update_scan_index(&canonical_base, s, s, old_phas, new_phas);
}
CommonFieldPutResult::NoChange => {}
}
self.run_special_actions(&canonical_base, &rec, special_actions);
if field == "ASG" {
crate::server::access_security::notify_asg_field_changed();
}
return Ok(());
}
Err(CaError::ChannelNotFound(name.to_string()))
}
fn run_special_actions(
&self,
record_name: &str,
rec: &std::sync::Arc<crate::server::record::RecordCell>,
actions: Vec<crate::server::record::ProcessAction>,
) {
if actions.is_empty() {
return;
}
let mut visited = crate::server::database::ProcStack::new();
self.execute_process_actions(record_name, rec, actions, &mut visited);
}
pub async fn put_record_field_from_ca(
&self,
record_name: &str,
field: &str,
value: EpicsValue,
) -> CaResult<crate::server::record::ProcessCompletion> {
let _record_gate = self.acquire_put_gate(record_name, field, Some(&value));
self.put_record_field_from_ca_body(record_name, field, value, NotifyRequest::New)
}
pub fn put_record_field_from_ca_already_locked(
&self,
record_name: &str,
field: &str,
value: EpicsValue,
) -> CaResult<crate::server::record::ProcessCompletion> {
self.put_record_field_from_ca_body(record_name, field, value, NotifyRequest::New)
}
pub async fn put_record_field_from_ca_no_notify(
&self,
record_name: &str,
field: &str,
value: EpicsValue,
) -> CaResult<()> {
self.put_record_field_from_ca_no_notify_with_origin(record_name, field, value, 0)
.await
}
pub async fn put_record_field_from_ca_no_notify_with_origin(
&self,
record_name: &str,
field: &str,
value: EpicsValue,
origin: u64,
) -> CaResult<()> {
let _record_gate = self.acquire_put_gate(record_name, field, Some(&value));
let _origin_scope = crate::server::record::ambient_write_origin_scope(origin);
self.put_record_field_from_ca_body(record_name, field, value, NotifyRequest::None)
.map(|_| ())
}
pub async fn put_field_from_client(
&self,
record_name: &str,
field: &str,
value: EpicsValue,
process: ProcessMode,
block: bool,
) -> CaResult<()> {
let process = if self.is_dbf_link_field(record_name, field) {
ProcessMode::Passive
} else {
process
};
self.check_external_put_preconditions(record_name, field)
.await?;
match process {
ProcessMode::Inhibit => self.put_pv(&format!("{record_name}.{field}"), value).await,
ProcessMode::Passive => {
if block {
let completion = self
.put_record_field_from_ca(record_name, field, value)
.await?;
self.await_completion(record_name, completion).await;
Ok(())
} else {
self.put_record_field_from_ca_no_notify(record_name, field, value)
.await
}
}
ProcessMode::Force => {
self.put_pv(&format!("{record_name}.{field}"), value)
.await?;
if block {
let completion = self.process_record_with_notify(record_name).await?;
self.await_completion(record_name, completion).await;
Ok(())
} else {
self.put_driven_process(record_name).await
}
}
}
}
async fn await_completion(
&self,
record_name: &str,
completion: crate::server::record::ProcessCompletion,
) {
let crate::server::record::ProcessCompletion::Async(rx) = completion else {
return;
};
let mut pending = ClientAwaitingNotify {
db: self,
record: record_name,
rx,
answered: false,
};
let _ = (&mut pending.rx).await;
pending.answered = true;
}
pub async fn put_alarm_ack_from_ca(
&self,
record_name: &str,
field: &str,
ack: crate::server::record::AlarmAck,
value: u16,
) -> CaResult<()> {
let field_upper = field.to_ascii_uppercase();
let rec = self
.get_record(record_name)
.ok_or_else(|| CaError::ChannelNotFound(record_name.to_string()))?;
let _record_gate = self.lock_instance(&rec);
let backing = self.resolve_link_backed_metadata_for_posts(&rec);
let backing = backing.as_link_backing();
let mut instance = rec.write();
check_put_disabled(&instance, &field_upper)?;
match ack {
crate::server::record::AlarmAck::Transient => instance.put_ackt(value, backing),
crate::server::record::AlarmAck::Severity => instance.put_acks(value, backing),
}
Ok(())
}
pub fn put_record_field_from_ca_no_notify_already_locked(
&self,
record_name: &str,
field: &str,
value: EpicsValue,
) -> CaResult<()> {
self.put_record_field_from_ca_body(record_name, field, value, NotifyRequest::None)
.map(|_| ())
}
pub async fn process_record_with_notify(
&self,
record_name: &str,
) -> CaResult<crate::server::record::ProcessCompletion> {
let (completion_tx, completion_rx) = crate::runtime::sync::oneshot::channel();
let installed = {
let _record_gate = self.acquire_put_gate(record_name, "", None);
self.install_notify_and_process_already_locked(
record_name,
completion_tx,
NotifyArrival::Fresh,
)
}?;
match installed {
Some(notify) if notify.completed() => {
Ok(crate::server::record::ProcessCompletion::Sync)
}
_ => Ok(crate::server::record::ProcessCompletion::Async(
completion_rx,
)),
}
}
pub async fn put_driven_process(&self, record_name: &str) -> CaResult<()> {
let _record_gate = self.acquire_put_gate(record_name, "", None);
self.put_driven_process_already_locked(record_name)
}
pub fn put_driven_process_already_locked(&self, record_name: &str) -> CaResult<()> {
{
let Some(rec) = self.get_record(record_name) else {
return Ok(());
};
let mut instance = rec.write();
if instance.is_processing() {
instance.common.rpro = 1;
return Ok(());
}
instance.common.putf = true;
}
let mut visited = crate::server::database::ProcStack::new();
self.process_record_with_links_already_locked(record_name, &mut visited)
}
pub(crate) async fn restart_next_notify_put(&self, record_name: &str) {
let _record_gate = self.acquire_put_gate(record_name, "", None);
let Some(rec) = self.get_record(record_name) else {
return;
};
let Some(queued) = rec.write().take_next_notify_restart() else {
return;
};
match queued {
crate::server::record::DeferredNotify::Put(
crate::server::record::DeferredNotifyPut {
field,
value,
completion,
},
) => {
let _ = self.put_record_field_from_ca_body(
record_name,
&field,
value,
NotifyRequest::Deferred(completion),
);
}
crate::server::record::DeferredNotify::Process { completion } => {
let _ = self.install_notify_and_process_already_locked(
record_name,
completion,
NotifyArrival::Replay,
);
}
}
}
fn install_notify_and_process_already_locked(
&self,
record_name: &str,
completion: crate::runtime::sync::oneshot::Sender<()>,
arrival: NotifyArrival,
) -> CaResult<Option<std::sync::Arc<crate::server::record::NotifyWaitSet>>> {
let rec_arc = self
.get_record(record_name)
.ok_or_else(|| CaError::ChannelNotFound(record_name.to_string()))?;
self.cancel_unanswerable_notify(record_name);
let notify = {
let mut guard = rec_arc.write();
if arrival.defers(&guard) {
guard.queue_notify_put(crate::server::record::DeferredNotify::Process {
completion,
});
return Ok(None);
}
match guard.install_or_queue_notify(completion) {
Some(notify) => notify,
None => return Ok(None),
}
};
let mut visited = crate::server::database::ProcStack::new();
self.process_record_with_links_already_locked(record_name, &mut visited)?;
Ok(Some(notify))
}
pub fn cancel_unanswerable_notify(&self, record_name: &str) {
let Some(rec) = self.get_record(record_name) else {
return;
};
let Some(dead) = rec.read().unanswerable_notify() else {
return;
};
for name in dead.joined_records() {
let Some(member) = self.get_record(&name) else {
continue;
};
let exit = {
let mut guard = member.write();
if !guard.release_notify(&dead) {
continue;
}
guard.pact_exit_without_release()
};
self.apply_pact_exit(&name, &member, exit);
}
}
fn claim_put_notify<'a>(
rec: &'a std::sync::Arc<crate::server::record::RecordCell>,
guard: &mut crate::server::record::RecordInstance,
completion: crate::runtime::sync::oneshot::Sender<()>,
) -> NotifyClaim<'a> {
let set = guard
.install_or_queue_notify(completion)
.expect("the ownership test ran in this critical section, so the slot is free");
NotifyClaim {
rec,
set: Some(set),
}
}
fn put_record_field_from_ca_body(
&self,
record_name: &str,
field: &str,
mut value: EpicsValue,
notify_request: NotifyRequest,
) -> CaResult<crate::server::record::ProcessCompletion> {
let field = field.to_ascii_uppercase();
let want_notify = notify_request.wants_notify();
let _relink = self.link_field_write(record_name, &field);
let rec = self
.get_record(record_name)
.ok_or_else(|| CaError::ChannelNotFound(record_name.to_string()))?;
let canonical_owned;
let record_name: &str = if let Some(target) = self.resolve_alias(record_name) {
canonical_owned = target;
&canonical_owned
} else {
record_name
};
{
let instance = rec.read();
check_put_disabled(&instance, &field)?;
if let Some(text) = check_link_put(
instance.record.record_type(),
instance.common.dtyp.as_str(),
&field,
&value,
)? {
value = text;
}
check_no_mod_in_db_put(&instance, &field)?;
}
let is_dbf_link_field = self.is_dbf_link_field(record_name, &field);
let mut claim: Option<(
NotifyClaim<'_>,
Option<crate::runtime::sync::oneshot::Receiver<()>>,
)> = None;
if want_notify {
let is_restart = notify_request.is_restart();
self.cancel_unanswerable_notify(record_name);
let mut guard = rec.write();
let must_wait = if is_restart {
guard.is_processing()
} else if is_dbf_link_field {
guard.notify_put_has_owner()
} else {
guard.notify_put_is_owned()
};
if must_wait {
let Some((completion, completion_rx)) = notify_request.into_completion() else {
return Ok(crate::server::record::ProcessCompletion::Sync);
};
let put = crate::server::record::DeferredNotifyPut {
field,
value,
completion,
};
let put = crate::server::record::DeferredNotify::Put(put);
if is_restart {
guard.requeue_notify_put(put);
} else {
guard.queue_notify_put(put);
}
return Ok(crate::server::record::ProcessCompletion::from_signal(
completion_rx,
));
}
if let Some((completion, completion_rx)) = notify_request.into_completion() {
claim = Some((
Self::claim_put_notify(&rec, &mut guard, completion),
completion_rx,
));
}
}
if field == "PROC" {
let proc_store: CaResult<()> = {
let rec_arc = {
let recs = self.inner.records.read();
recs.get(record_name).cloned()
};
if let Some(rec_arc) = rec_arc {
let mut guard = rec_arc.write();
match guard.put_common_field("PROC", value) {
Ok(_) => {
guard.notify_field(
"PROC",
crate::server::recgbl::EventMask::VALUE
| crate::server::recgbl::EventMask::LOG,
);
Ok(())
}
Err(e) => Err(e),
}
} else {
Ok(())
}
};
if let Err(e) = proc_store {
if want_notify {
if let Some((c, _rx)) = claim.take() {
c.commit_without_waiting();
}
let _ = self.put_driven_process_already_locked(record_name);
}
return Err(e);
}
let parked = claim.take().map(|(c, rx)| (c.commit(), rx));
let _ = self.put_driven_process_already_locked(record_name);
return match parked {
Some((notify, completion_rx)) => {
if notify.completed() {
Ok(crate::server::record::ProcessCompletion::Sync)
} else {
Ok(crate::server::record::ProcessCompletion::from_signal(
completion_rx,
))
}
}
None => Ok(crate::server::record::ProcessCompletion::Sync),
};
}
let mut special_actions = Vec::new();
let link_backing = self.resolve_link_backed_metadata_for_posts(&rec);
let link_backing = link_backing.as_link_backing();
let outcome: Result<crate::server::record::CommonFieldPutResult, (CaError, bool)> = {
let mut instance = rec.write();
let block_result: CaResult<crate::server::record::CommonFieldPutResult> = (|| {
let request = dbput_request(&*instance.record, &field, value)?;
instance.record.special(&field, false)?;
special_before_put(&mut instance, &field);
let prev_value = instance.resolve_field_stored(&field);
let old_nord = array_nord_before_put(&instance, &field);
use crate::server::record::CommonFieldPutResult;
let common_result = match request {
PutRequest::StoreNothing { alarm } => {
if alarm {
set_empty_request_alarm(&mut instance);
}
clear_udf_on_value_put(&mut instance, &field);
CommonFieldPutResult::NoChange
}
PutRequest::Write(value) => {
match instance.record.put_field(&field, value.clone()) {
Ok(()) => {
instance.record.on_put(&field);
let result = special_after_put(
self,
&mut instance,
&field,
&mut special_actions,
link_backing,
)?;
clear_udf_on_value_put(&mut instance, &field);
result
}
Err(CaError::FieldNotFound(_)) => {
instance.put_common_field(&field, value)?
}
Err(e) => {
special_after_put(
self,
&mut instance,
&field,
&mut special_actions,
link_backing,
)?;
return Err(e);
}
}
}
};
if instance.record.special_checks_alarms(&field) {
let inst = &mut *instance;
inst.record.check_alarms(&mut inst.common);
let _ = crate::server::recgbl::rec_gbl_reset_alarms(&mut inst.common);
}
instance.notify_field_written_if_changed(&field, prev_value.as_ref(), link_backing);
dbput_post_put_field(&mut instance, &field, link_backing);
post_array_info(&mut instance, &old_nord, 0, link_backing);
let side_effect_alarm_mask = if instance.record.special_commits_alarms(&field) {
commit_special_reset_alarm(&mut instance)
} else {
crate::server::recgbl::EventMask::NONE
};
let side_effect_value_only = instance.record.value_only_change_fields();
for sf in instance.record.monitor_side_effect_fields(&field) {
use crate::server::recgbl::EventMask;
let mask = if side_effect_value_only
.iter()
.any(|f| f.eq_ignore_ascii_case(sf))
{
EventMask::VALUE
} else {
EventMask::VALUE | EventMask::LOG
};
instance.notify_field(sf, mask | side_effect_alarm_mask);
}
emit_cycle_posts(&mut instance, link_backing);
Ok(common_result)
})(
);
match block_result {
Ok(cr) => Ok(cr),
Err(e) => {
if instance.record.special_commits_alarms(&field) {
let _ = instance.record.special(&field, true);
let alarm_mask = commit_special_reset_alarm(&mut instance);
let value_only = instance.record.value_only_change_fields();
for sf in instance.record.monitor_side_effect_fields(&field) {
use crate::server::recgbl::EventMask;
let mask = if value_only.iter().any(|f| f.eq_ignore_ascii_case(sf)) {
EventMask::VALUE
} else {
EventMask::VALUE | EventMask::LOG
};
instance.notify_field(sf, mask | alarm_mask);
}
}
if instance.record.special_checks_alarms(&field) {
let inst = &mut *instance;
inst.record.check_alarms(&mut inst.common);
let _ = crate::server::recgbl::rec_gbl_reset_alarms(&mut inst.common);
}
if instance.record.is_udf_defining_put(&field)
&& field != instance.record.primary_field()
{
instance.common.udf = 0;
}
let should_process = want_notify && put_drives_processing_of(&instance, &field);
Err((e, should_process))
}
}
};
let common_result = match outcome {
Ok(cr) => cr,
Err((e, should_process)) => {
if should_process {
if let Some((c, _rx)) = claim.take() {
c.commit_without_waiting();
}
let _ = self.put_driven_process_already_locked(record_name);
}
return Err(e);
}
};
if field == "ASG" {
crate::server::access_security::notify_asg_field_changed();
}
self.run_special_actions(record_name, &rec, std::mem::take(&mut special_actions));
match common_result {
crate::server::record::CommonFieldPutResult::ScanChanged {
old_scan,
new_scan,
phas,
} => {
self.update_scan_index(record_name, old_scan, new_scan, phas, phas);
}
crate::server::record::CommonFieldPutResult::PhasChanged {
scan: s,
old_phas,
new_phas,
} => {
self.update_scan_index(record_name, s, s, old_phas, new_phas);
}
crate::server::record::CommonFieldPutResult::NoChange => {}
}
let should_process = {
let instance = rec.read();
put_drives_processing_of(&instance, &field)
};
if !should_process {
return Ok(crate::server::record::ProcessCompletion::Sync);
}
let parked = claim.take().map(|(c, rx)| (c.commit(), rx));
if field == "VAL" {
let rec_arc = {
let recs = self.inner.records.read();
recs.get(record_name).cloned()
};
if let Some(rec_arc) = rec_arc {
let mut guard = rec_arc.write();
if guard.record.soft_channel_skips_convert() {
guard.record.set_device_did_compute(true);
}
}
}
let _ = self.put_driven_process_already_locked(record_name);
let originating_pending = want_notify
&& self
.get_record_no_resolve(record_name)
.is_some_and(|rec_arc| rec_arc.read().notify.is_some());
if !originating_pending {
let rec_arc = {
let recs = self.inner.records.read();
recs.get(record_name).cloned()
};
if let Some(rec_arc) = rec_arc {
let mut guard = rec_arc.write();
if !guard.is_processing() {
guard.common.putf = false;
}
}
}
match parked {
Some((notify, completion_rx)) => {
if notify.completed() {
Ok(crate::server::record::ProcessCompletion::Sync)
} else {
Ok(crate::server::record::ProcessCompletion::from_signal(
completion_rx,
))
}
}
None => Ok(crate::server::record::ProcessCompletion::Sync),
}
}
pub async fn put_pv_no_process(&self, name: &str, mut value: EpicsValue) -> CaResult<()> {
let (base, field) = super::parse_pv_name(name);
let field = field.to_ascii_uppercase();
let simple = self.inner.simple_pvs.lock().get(name).cloned();
if let Some(pv) = simple {
pv.set(value);
return Ok(());
}
if let Some(rec) = self.get_record(base) {
let _record_gate = self.acquire_put_gate(base, &field, Some(&value));
let _relink = self.link_field_write(base, &field);
let canonical_base: String =
self.resolve_alias(base).unwrap_or_else(|| base.to_string());
let link_backing = self.resolve_link_backed_metadata_for_posts(&rec);
let link_backing = link_backing.as_link_backing();
let mut special_actions = Vec::new();
let common_result = {
let mut instance = rec.write();
if let Some(text) = check_link_put(
instance.record.record_type(),
instance.common.dtyp.as_str(),
&field,
&value,
)? {
value = text;
}
check_no_mod_in_db_put(&instance, &field)?;
instance.record.special(&field, false)?;
special_before_put(&mut instance, &field);
let prev_value = instance.resolve_field_stored(&field);
let refused = match instance.record.get_field(&field).map(|v| v.db_field_type()) {
Some(target) => match crate::server::record::dbput_coerce_value(
&*instance.record,
&field,
target,
value,
) {
Ok(crate::types::c_parse::Converted::Stored(v)) => {
match instance.record.put_field(&field, v.clone()) {
Ok(()) => None,
Err(CaError::FieldNotFound(_)) => {
instance.put_common_field(&field, v)?;
None
}
Err(e) => Some(e),
}
}
Ok(crate::types::c_parse::Converted::Unchanged) => None,
Err(e) => Some(e),
},
None => {
let value = crate::server::record::put_value_in_field_shape(
&*instance.record,
&field,
value,
);
match instance.record.put_field(&field, value.clone()) {
Ok(()) => None,
Err(CaError::FieldNotFound(_)) => {
instance.put_common_field(&field, value)?;
None
}
Err(e) => Some(e),
}
}
};
let result = special_after_put(
self,
&mut instance,
&field,
&mut special_actions,
link_backing,
)?;
if let Some(e) = refused {
return Err(e);
}
clear_udf_on_value_put(&mut instance, &field);
instance.notify_field_written_if_changed(&field, prev_value.as_ref(), link_backing);
emit_cycle_posts(&mut instance, link_backing);
result
};
match common_result {
crate::server::record::CommonFieldPutResult::ScanChanged {
old_scan,
new_scan,
phas,
} => {
self.update_scan_index(&canonical_base, old_scan, new_scan, phas, phas);
}
crate::server::record::CommonFieldPutResult::PhasChanged {
scan: s,
old_phas,
new_phas,
} => {
self.update_scan_index(&canonical_base, s, s, old_phas, new_phas);
}
crate::server::record::CommonFieldPutResult::NoChange => {}
}
self.run_special_actions(&canonical_base, &rec, special_actions);
if field == "ASG" {
crate::server::access_security::notify_asg_field_changed();
}
return Ok(());
}
Err(CaError::ChannelNotFound(name.to_string()))
}
}
fn metadata_from_snapshot(snapshot: &Snapshot) -> crate::server::pv::PvMetadata {
crate::server::pv::PvMetadata {
display: snapshot.display.clone(),
control: snapshot.control.clone(),
enums: snapshot.enums.clone(),
}
}
enum PutGate {
One {
_held: super::record_lock::RecordWriteGuard,
},
Many {
_held: super::record_lock::ManyRecordWriteGuard,
},
}
#[cfg(test)]
mod tests {
use super::super::PvDatabase;
use super::NotifyArrival;
use crate::types::EpicsValue;
#[epics_macros_rs::epics_test]
async fn a_declared_field_with_no_value_reads_as_a_failure_not_a_missing_channel() {
use crate::error::CaError;
use crate::server::records::ai::AiRecord;
let db = PvDatabase::new();
db.add_record("T:AI", Box::new(AiRecord::new(1.5)))
.await
.unwrap();
for field in ["TIME", "BKPT"] {
let name = format!("T:AI.{field}");
match db.get_pv(&name) {
Err(CaError::BadDbrType(msg)) => assert!(
msg.contains(&name),
"the status must name the field, got {msg:?}"
),
other => panic!(
"{name} is declared and unreadable, so it must fail the READ; got {other:?}"
),
}
}
for name in ["T:AI.NOSUCH", "T:NOSUCHREC.VAL"] {
assert!(
matches!(db.get_pv(name), Err(CaError::ChannelNotFound(_))),
"{name} resolves to nothing and must stay not-found, got {:?}",
db.get_pv(name)
);
}
assert_eq!(db.get_pv("T:AI.VAL").unwrap(), EpicsValue::Double(1.5));
assert_eq!(db.get_pv("T:AI.UTAG").unwrap(), EpicsValue::UInt64(0));
assert_eq!(
db.get_pv("T:AI.NAME").unwrap(),
EpicsValue::String("T:AI".into())
);
assert_eq!(
db.get_pv("T:AI.RTYP").unwrap(),
EpicsValue::String("ai".into())
);
}
#[epics_macros_rs::epics_test]
async fn put_pv_and_post_handles_simple_pv() {
let db = PvDatabase::new();
db.add_pv("gw:test", EpicsValue::Double(0.0)).await.unwrap();
db.put_pv_and_post("gw:test", EpicsValue::Double(42.0))
.await
.expect("simple PV put_pv_and_post must succeed");
let pv = db.find_pv("gw:test").await.expect("PV exists");
assert!(matches!(pv.get(), EpicsValue::Double(v) if v == 42.0));
}
#[epics_macros_rs::epics_test]
async fn field_io_entry_points_accept_aliases() {
use crate::server::records::ai::AiRecord;
let db = PvDatabase::new();
db.add_record("CANON", Box::new(AiRecord::new(0.0)))
.await
.unwrap();
db.add_alias("ALT", "CANON").await.unwrap();
db.put_pv("CANON.VAL", EpicsValue::Double(1.5))
.await
.unwrap();
let v = db.get_pv("ALT.VAL").unwrap();
assert!(matches!(v, EpicsValue::Double(x) if x == 1.5));
db.put_pv("ALT.VAL", EpicsValue::Double(7.0)).await.unwrap();
let v = db.get_pv("CANON.VAL").unwrap();
assert!(matches!(v, EpicsValue::Double(x) if x == 7.0));
db.put_pv_and_post("ALT.VAL", EpicsValue::Double(11.0))
.await
.unwrap();
let v = db.get_pv("CANON.VAL").unwrap();
assert!(matches!(v, EpicsValue::Double(x) if x == 11.0));
db.put_pv_no_process("ALT.VAL", EpicsValue::Double(13.0))
.await
.unwrap();
let v = db.get_pv("ALT.VAL").unwrap();
assert!(matches!(v, EpicsValue::Double(x) if x == 13.0));
}
#[epics_macros_rs::epics_test]
async fn write_path_menu_label_resolves_against_field_menu() {
use crate::server::records::sel::SelRecord;
let db = PvDatabase::new();
db.add_record("SEL", Box::new(SelRecord::default()))
.await
.unwrap();
db.put_pv("SEL.SELM", EpicsValue::String("Specified".into()))
.await
.unwrap();
assert_eq!(db.get_pv("SEL.SELM").unwrap(), EpicsValue::Enum(0));
db.put_pv_and_post("SEL.SELM", EpicsValue::String("High Signal".into()))
.await
.unwrap();
assert_eq!(db.get_pv("SEL.SELM").unwrap(), EpicsValue::Enum(1));
db.put_pv("SEL.SELM", EpicsValue::String("2".into()))
.await
.unwrap();
assert_eq!(db.get_pv("SEL.SELM").unwrap(), EpicsValue::Enum(2));
}
#[epics_macros_rs::epics_test]
async fn set_pv_metadata_installs_without_posting() {
use crate::error::CaError;
use crate::server::snapshot::{DisplayInfo, Snapshot};
use crate::types::DbFieldType;
use std::time::SystemTime;
let db = PvDatabase::new();
db.add_pv("gw:meta", EpicsValue::Double(0.0)).await.unwrap();
const DBE_PROPERTY: u16 = 8;
let pv = db.find_pv("gw:meta").await.expect("PV exists");
let mut prop_rx = pv
.add_subscriber(1, DbFieldType::Double, DBE_PROPERTY)
.expect("subscriber added");
let mut ctrl = Snapshot::new(EpicsValue::Double(0.0), 0, 0, SystemTime::UNIX_EPOCH);
ctrl.display = Some(DisplayInfo {
units: "mm".into(),
precision: 3,
upper_disp_limit: 10.0,
lower_disp_limit: -10.0,
..Default::default()
});
db.set_pv_metadata("gw:meta", &ctrl)
.await
.expect("simple PV set_pv_metadata must succeed");
let installed = pv.metadata();
assert_eq!(
installed.display.expect("display metadata installed").units,
"mm"
);
assert!(
prop_rx.try_recv().is_err(),
"set_pv_metadata must not post a DBE_PROPERTY event"
);
assert!(matches!(
db.set_pv_metadata("no:such:pv", &ctrl).await,
Err(CaError::ChannelNotFound(_))
));
}
#[epics_macros_rs::epics_test]
async fn post_pv_property_refreshes_and_posts_property_event() {
use crate::error::CaError;
use crate::server::snapshot::{DisplayInfo, Snapshot};
use crate::types::{DbFieldType, WallTime};
const DBE_PROPERTY: u16 = 8;
const DBE_VALUE: u16 = 1;
const MAJOR: u16 = 2;
const HIGH: u16 = 3;
let db = PvDatabase::new();
db.add_pv("gw:prop", EpicsValue::Double(0.0)).await.unwrap();
let pv = db.find_pv("gw:prop").await.expect("PV exists");
let mut prop_rx = pv
.add_subscriber(1, DbFieldType::Double, DBE_PROPERTY)
.expect("property subscriber added");
let mut val_rx = pv
.add_subscriber(2, DbFieldType::Double, DBE_VALUE)
.expect("value subscriber added");
let upstream_ts = WallTime::from_unix(2_000_000, 0);
let mut ctrl = Snapshot::new(EpicsValue::Double(5.0), HIGH, MAJOR, upstream_ts);
ctrl.display = Some(DisplayInfo {
units: "V".into(),
precision: 1,
..Default::default()
});
db.post_pv_property("gw:prop", ctrl)
.await
.expect("simple PV post_pv_property must succeed");
assert_eq!(
pv.metadata().display.expect("metadata refreshed").units,
"V"
);
let ev = prop_rx
.try_recv()
.expect("DBE_PROPERTY subscriber receives the property event");
assert_eq!(
ev.snapshot
.display
.clone()
.expect("event carries metadata")
.units,
"V"
);
assert_eq!(
ev.snapshot.alarm.severity, MAJOR,
"upstream severity preserved"
);
assert_eq!(ev.snapshot.alarm.status, HIGH, "upstream status preserved");
assert_eq!(
ev.snapshot.timestamp, upstream_ts,
"control-DBR timestamp preserved, not a fresh wall clock"
);
assert!(
val_rx.try_recv().is_err(),
"DBE_VALUE-only subscriber must not receive a property post"
);
let again = Snapshot::new(EpicsValue::Double(0.0), 0, 0, WallTime::UNIX_EPOCH);
assert!(matches!(
db.post_pv_property("no:such:pv", again).await,
Err(CaError::ChannelNotFound(_))
));
}
#[epics_macros_rs::epics_test]
async fn put_record_field_from_ca_accepts_alias() {
use crate::server::records::ai::AiRecord;
let db = PvDatabase::new();
db.add_record("CANON", Box::new(AiRecord::new(0.0)))
.await
.unwrap();
db.add_alias("ALT", "CANON").await.unwrap();
let _ = db
.put_record_field_from_ca("ALT", "VAL", EpicsValue::Double(2.5))
.await
.expect("put via alias must succeed");
let v = db.get_pv("CANON.VAL").unwrap();
assert!(matches!(v, EpicsValue::Double(x) if x == 2.5));
}
#[epics_macros_rs::epics_test]
async fn alarm_ack_put_posts_record_wide_dbe_alarm() {
use crate::server::recgbl::EventMask;
use crate::server::records::ai::AiRecord;
use crate::types::DbFieldType;
let db = PvDatabase::new();
db.add_record("A:REC", Box::new(AiRecord::new(1.0)))
.await
.unwrap();
let rec = db.get_record("A:REC").expect("record exists");
let (mut alarm_rx, mut value_rx) = {
let mut inst = rec.write();
let a = inst
.add_subscriber("VAL", 1, DbFieldType::Double, EventMask::ALARM.bits())
.expect("alarm subscriber");
let v = inst
.add_subscriber("VAL", 2, DbFieldType::Double, EventMask::VALUE.bits())
.expect("value subscriber");
(a, v)
};
db.put_alarm_ack_from_ca(
"A:REC",
"VAL",
crate::server::record::AlarmAck::Transient,
0,
)
.await
.expect("ackt put");
assert!(
alarm_rx.try_recv().is_ok(),
"DBE_ALARM subscriber must receive the record-wide alarm post"
);
assert!(
value_rx.try_recv().is_err(),
"DBE_VALUE-only subscriber must not receive the alarm post"
);
db.put_alarm_ack_from_ca(
"A:REC",
"VAL",
crate::server::record::AlarmAck::Transient,
0,
)
.await
.expect("ackt re-put");
assert!(
alarm_rx.try_recv().is_err(),
"unchanged ACKT must post nothing"
);
}
#[epics_macros_rs::epics_test]
async fn noop_put_to_a_common_stored_field_posts_nothing() {
use crate::server::recgbl::EventMask;
use crate::server::records::ai::AiRecord;
use crate::types::DbFieldType;
let db = PvDatabase::new();
db.add_record("NP:REC", Box::new(AiRecord::new(1.0)))
.await
.unwrap();
let rec = db.get_record("NP:REC").expect("record exists");
let mut hihi_rx = rec
.write()
.add_subscriber("HIHI", 1, DbFieldType::Double, EventMask::VALUE.bits())
.expect("HIHI subscriber");
db.put_pv_and_post("NP:REC.HIHI", EpicsValue::Double(10.0))
.await
.expect("first HIHI put");
assert!(hihi_rx.try_recv().is_ok(), "a changed HIHI must post");
let stamped = rec.read().common.time;
db.put_pv_and_post("NP:REC.HIHI", EpicsValue::Double(10.0))
.await
.expect("unchanged HIHI put");
assert!(
hihi_rx.try_recv().is_err(),
"an unchanged HIHI must not post"
);
assert_eq!(
rec.read().common.time,
stamped,
"an unchanged put must not restamp the record"
);
}
#[epics_macros_rs::epics_test]
async fn post_property_writes_the_block_and_posts_dbe_property_on_val() {
use crate::server::device_support::PropertyPost;
use crate::server::recgbl::EventMask;
use crate::server::records::mbbi::MbbiRecord;
use crate::types::DbFieldType;
let db = PvDatabase::new();
db.add_record("M:ENUM", Box::new(MbbiRecord::new(0)))
.await
.unwrap();
let rec = db.get_record("M:ENUM").expect("record exists");
let (mut val_prop_rx, mut val_value_rx, mut zrst_prop_rx) = {
let mut inst = rec.write();
let vp = inst
.add_subscriber("VAL", 1, DbFieldType::Enum, EventMask::PROPERTY.bits())
.expect("VAL property subscriber");
let vv = inst
.add_subscriber("VAL", 2, DbFieldType::Enum, EventMask::VALUE.bits())
.expect("VAL value subscriber");
let zp = inst
.add_subscriber("ZRST", 3, DbFieldType::String, EventMask::PROPERTY.bits())
.expect("ZRST property subscriber");
(vp, vv, zp)
};
let written = db
.post_property(
"M:ENUM",
PropertyPost {
writes: vec![("ZRST".to_string(), EpicsValue::String("LABEL".into()))],
post_field: "VAL".to_string(),
},
)
.expect("post_property succeeds");
assert_eq!(written, vec!["ZRST".to_string()]);
assert_eq!(
db.get_pv("M:ENUM.ZRST").unwrap(),
EpicsValue::String("LABEL".into())
);
assert!(
val_prop_rx.try_recv().is_ok(),
"the DBE_PROPERTY VAL subscriber is the one C posts to"
);
assert!(
val_value_rx.try_recv().is_err(),
"DBE_VALUE-only subscriber must not receive a property post"
);
assert!(
zrst_prop_rx.try_recv().is_err(),
"setEnums rewrites the state fields and posts on none of them"
);
}
#[epics_macros_rs::epics_test]
async fn ca_put_to_non_pp_val_posts_monitor() {
use crate::server::database::db_access::DbSubscription;
use crate::server::records::calc::CalcRecord;
let db = PvDatabase::new();
db.add_record("CALC1", Box::new(CalcRecord::new("0")))
.await
.unwrap();
let mut sub = DbSubscription::subscribe(&db, "CALC1.VAL")
.await
.expect("subscribe to CALC1.VAL");
db.put_record_field_from_ca("CALC1", "VAL", EpicsValue::Double(5.0))
.await
.expect("CA put to CALC1.VAL must succeed");
let got = crate::runtime::task::timeout(std::time::Duration::from_secs(1), sub.recv_f64())
.await
.expect("a DBE_VALUE monitor must fire for a direct VAL put to a non-pp record");
assert_eq!(got, Some(5.0));
}
#[epics_macros_rs::epics_test]
async fn r8_22_db_burst_keeps_earlier_distinct_updates() {
use crate::server::database::db_access::DbSubscription;
use crate::server::event_queue::{event_que_size, events_per_que};
use crate::server::records::calc::CalcRecord;
let db = PvDatabase::new();
db.add_record("CALC1", Box::new(CalcRecord::new("0")))
.await
.unwrap();
let mut sub = DbSubscription::subscribe(&db, "CALC1.VAL")
.await
.expect("subscribe to CALC1.VAL");
let appended = event_que_size() - events_per_que();
let burst = appended + 40;
for i in 1..=burst {
db.put_record_field_from_ca("CALC1", "VAL", EpicsValue::Double(i as f64))
.await
.expect("CA put to CALC1.VAL must succeed");
}
let mut seq = Vec::new();
while let Ok(Some(v)) =
crate::runtime::task::timeout(std::time::Duration::from_millis(200), sub.recv_f64())
.await
{
seq.push(v);
}
let want: Vec<f64> = (1..appended)
.map(|i| i as f64)
.chain(std::iter::once(burst as f64))
.collect();
assert_eq!(
seq, want,
"record burst delivery must be {{earlier distinct backlog…, coalesced tail}}"
);
}
#[epics_macros_rs::epics_test]
async fn a_replay_onto_a_taken_notify_slot_queues_instead_of_overwriting() {
use crate::server::records::ai::AiRecord;
let db = PvDatabase::new();
db.add_record("REPLAY:TAKEN", Box::new(AiRecord::new(0.0)))
.await
.unwrap();
let rec = db.get_record("REPLAY:TAKEN").expect("record exists");
let (owner_tx, _owner_rx) = crate::runtime::sync::oneshot::channel();
let owner = rec
.write()
.install_or_queue_notify(owner_tx)
.expect("a free slot installs");
let (client_tx, _client_rx) = crate::runtime::sync::oneshot::channel();
assert!(
db.install_notify_and_process_already_locked(
"REPLAY:TAKEN",
client_tx,
NotifyArrival::Replay
)
.expect("the record is loaded")
.is_none(),
"the slot is owned, so the replay must drive no process cycle"
);
assert!(
rec.read()
.notify
.as_ref()
.is_some_and(|n| std::sync::Arc::ptr_eq(n, &owner)),
"the owner's wait-set must survive the replay"
);
assert!(
rec.read().notify_restart_pending(),
"the replay must be queued behind the owner, not dropped"
);
}
#[epics_macros_rs::epics_test]
async fn a_put_notify_that_processes_commits_its_claim() {
use crate::server::records::ai::AiRecord;
let db = PvDatabase::new();
db.add_record("CLAIM:PROC", Box::new(AiRecord::new(0.0)))
.await
.unwrap();
let rec = db.get_record("CLAIM:PROC").expect("record exists");
let done = db
.put_record_field_from_ca("CLAIM:PROC", "VAL", EpicsValue::Double(3.5))
.await
.expect("a VAL put is accepted");
assert!(matches!(
done,
crate::server::record::ProcessCompletion::Sync
));
assert!(
!rec.read().has_notify(),
"the completed cycle must leave the slot free"
);
assert_eq!(
rec.read().record.get_field("VAL"),
Some(EpicsValue::Double(3.5))
);
}
#[epics_macros_rs::epics_test]
async fn a_put_notify_that_drives_no_process_leaves_no_owner() {
use crate::server::records::ai::AiRecord;
let db = PvDatabase::new();
db.add_record("CLAIM:NOPROC", Box::new(AiRecord::new(0.0)))
.await
.unwrap();
let rec = db.get_record("CLAIM:NOPROC").expect("record exists");
db.put_record_field_from_ca(
"CLAIM:NOPROC",
"DESC",
EpicsValue::String("a description".into()),
)
.await
.expect("a DESC put is accepted");
assert!(
!rec.read().has_notify(),
"a put that processed nothing must not leave the record owned"
);
assert!(
!rec.read().notify_restart_pending(),
"nothing was queued, so nothing may be left on the restart list"
);
db.put_record_field_from_ca("CLAIM:NOPROC", "VAL", EpicsValue::Double(1.0))
.await
.expect("the next put-notify must not be queued behind a stale owner");
assert_eq!(
rec.read().record.get_field("VAL"),
Some(EpicsValue::Double(1.0))
);
}
#[epics_macros_rs::epics_test]
async fn a_refused_put_notify_leaves_no_owner() {
use crate::server::records::ai::AiRecord;
let db = PvDatabase::new();
db.add_record("CLAIM:REFUSED", Box::new(AiRecord::new(0.0)))
.await
.unwrap();
let rec = db.get_record("CLAIM:REFUSED").expect("record exists");
let refused = db
.put_record_field_from_ca(
"CLAIM:REFUSED",
"VAL",
EpicsValue::String("not a number".into()),
)
.await;
assert!(refused.is_err(), "a non-numeric VAL put must be refused");
assert!(
!rec.read().has_notify(),
"a refused put-notify must not leave the record owned"
);
assert!(
!rec.read().notify_restart_pending(),
"a refused put-notify must queue nothing"
);
}
#[epics_macros_rs::epics_test]
async fn a_refused_put_notify_stays_installed_while_its_cycle_is_async() {
use crate::server::records::calcout::CalcoutRecord;
let db = PvDatabase::new();
db.add_record("CLAIM:ODLY", Box::new(CalcoutRecord::default()))
.await
.unwrap();
let rec = db.get_record("CLAIM:ODLY").expect("record exists");
db.put_record_field_from_ca("CLAIM:ODLY", "ODLY", EpicsValue::Double(3600.0))
.await
.expect("ODLY is settable");
assert!(
!rec.read().has_notify(),
"the ODLY put drives no cycle, so it owns nothing"
);
let refused = db
.put_record_field_from_ca("CLAIM:ODLY", "PROC", EpicsValue::String("nope".into()))
.await;
assert!(refused.is_err(), "a non-numeric PROC put must be refused");
assert!(
rec.read().is_processing(),
"ODLY > 0 defers the calcout output, so the cycle is still in flight"
);
assert!(
rec.read().has_notify(),
"the async cycle runs under the refused put's wait-set: releasing \
the claim before driving it leaves that cycle with no notifier"
);
}
#[epics_macros_rs::epics_test]
async fn a_refused_pp_field_put_stays_installed_while_its_cycle_is_async() {
use crate::server::records::calcout::CalcoutRecord;
let db = PvDatabase::new();
db.add_record("CLAIM:ODLYB", Box::new(CalcoutRecord::default()))
.await
.unwrap();
let rec = db.get_record("CLAIM:ODLYB").expect("record exists");
db.put_record_field_from_ca("CLAIM:ODLYB", "ODLY", EpicsValue::Double(3600.0))
.await
.expect("ODLY is settable");
let refused = db
.put_record_field_from_ca("CLAIM:ODLYB", "A", EpicsValue::String("nope".into()))
.await;
assert!(refused.is_err(), "a non-numeric A put must be refused");
assert!(
rec.read().is_processing(),
"ODLY > 0 defers the calcout output, so the cycle is still in flight"
);
assert!(
rec.read().has_notify(),
"Cause B commits to its cycle for the same reason the PROC refusal \
does: C reaches doProcess with didPut = 1 and assigns precord->ppn"
);
}
#[epics_macros_rs::epics_test]
async fn a_replay_onto_a_free_notify_slot_installs_and_drives_the_cycle() {
use crate::server::records::ai::AiRecord;
let db = PvDatabase::new();
db.add_record("REPLAY:FREE", Box::new(AiRecord::new(0.0)))
.await
.unwrap();
let rec = db.get_record("REPLAY:FREE").expect("record exists");
let (client_tx, _client_rx) = crate::runtime::sync::oneshot::channel();
assert!(
db.install_notify_and_process_already_locked(
"REPLAY:FREE",
client_tx,
NotifyArrival::Replay
)
.expect("the record is loaded")
.is_some(),
"a free slot installs and processes"
);
assert!(
!rec.read().notify_restart_pending(),
"nothing is queued when the replay took the slot"
);
}
#[test]
fn every_declared_link_class_field_is_gated_by_class() {
use crate::server::record::dbd_generated::RECORD_TYPES;
use crate::server::record::{declared_fields, declared_link_type};
let hw = EpicsValue::String("#C0 S0 @p".into());
let mut gated = 0usize;
let mut exempt = Vec::new();
let mut accepted = Vec::new();
for record_type in RECORD_TYPES {
for desc in declared_fields(record_type) {
if crate::types::dbf_link_class(record_type, desc.name).is_none() {
continue;
}
if declared_link_type(record_type, None, desc.name).is_none() {
exempt.push(format!("{record_type}.{}", desc.name));
continue;
}
if super::check_link_put(record_type, "", desc.name, &hw).is_err() {
gated += 1;
} else {
accepted.push(format!("{record_type}.{}", desc.name));
}
}
}
assert!(
accepted.is_empty(),
"a hardware link no vendored device() declares was accepted on {accepted:?}"
);
assert!(
exempt
.iter()
.all(|f| f.ends_with(".INP") || f.ends_with(".OUT")),
"only the device link of a type with no vendored device() may be exempt: {exempt:?}"
);
assert_eq!(
gated + exempt.len(),
LINK_CLASS_FIELD_COUNT,
"the sweep must reach every declared link-class field"
);
assert!(gated > 400, "only {gated} fields reached the gate");
for (record_type, field) in [
("ai", "SDIS"),
("ai", "TSEL"),
("ai", "FLNK"),
("ai", "SIML"),
("ai", "SIOL"),
("ai", "INP"),
("calc", "INPA"),
("ao", "DOL"),
("ao", "OUT"),
("fanout", "LNK1"),
] {
assert!(
super::check_link_put(record_type, "", field, &hw).is_err(),
"{record_type}.{field} must refuse a VME_IO link"
);
}
}
const LINK_CLASS_FIELD_COUNT: usize = 505;
#[test]
fn asub_subl_is_the_only_read_only_link_field() {
use crate::server::record::dbd_generated::RECORD_TYPES;
use crate::server::record::{Special, declared_fields};
let mut offenders = Vec::new();
for record_type in RECORD_TYPES {
for desc in declared_fields(record_type) {
if crate::types::dbf_link_class(record_type, desc.name).is_none() {
continue;
}
if desc.read_only
|| desc.special == Special::NoMod
|| desc.declared_special == Special::NoMod
{
offenders.push(format!("{record_type}.{}", desc.name));
}
}
}
assert_eq!(
offenders,
["aSub.SUBL"],
"the set of link fields where the gate order is observable has changed"
);
}
#[epics_macros_rs::epics_test]
async fn both_db_put_field_bodies_refuse_a_device_link_on_every_class() {
use crate::server::records::ao::AoRecord;
use crate::server::records::calc::CalcRecord;
use crate::server::records::fanout::FanoutRecord;
let db = PvDatabase::new();
db.add_record("LK:CALC", Box::new(CalcRecord::new("A")))
.await
.unwrap();
db.add_record("LK:AO", Box::new(AoRecord::new(0.0)))
.await
.unwrap();
db.add_record("LK:FO", Box::new(FanoutRecord::new()))
.await
.unwrap();
let cases = [
("LK:CALC", "SDIS"), ("LK:CALC", "TSEL"), ("LK:CALC", "INPA"), ("LK:AO", "DOL"), ("LK:AO", "SIML"), ("LK:AO", "SIOL"), ("LK:AO", "OUT"), ("LK:CALC", "FLNK"), ("LK:FO", "LNK1"), ];
for (name, field) in cases {
let before = db
.get_pv(&format!("{name}.{field}"))
.unwrap_or_else(|e| panic!("{name}.{field} must be readable: {e}"));
let ca = db
.put_record_field_from_ca_no_notify(
name,
field,
EpicsValue::String("@instio p".into()),
)
.await;
assert!(
ca.is_err(),
"{name}.{field}: a caput of an INST_IO link must be refused, as C's \
dbPutFieldLink refuses it"
);
let restore = db
.put_pv_no_process(
&format!("{name}.{field}"),
EpicsValue::String("@instio p".into()),
)
.await;
assert!(
restore.is_err(),
"{name}.{field}: an autosave restore is a dbPutField too"
);
let after = db
.get_pv(&format!("{name}.{field}"))
.unwrap_or_else(|e| panic!("{name}.{field} must still be readable: {e}"));
assert_eq!(
format!("{before:?}"),
format!("{after:?}"),
"{name}.{field} must keep the text it had — C leaves the link untouched"
);
}
db.put_record_field_from_ca_no_notify(
"LK:CALC",
"INPA",
EpicsValue::String("SRC.VAL".into()),
)
.await
.expect("a PV_LINK is what CONSTANT accepts (dbStaticLib.c:2408-2416)");
let stored = db.get_pv("LK:CALC.INPA").unwrap();
let EpicsValue::String(stored) = stored else {
panic!("a link field serves as DBF_STRING, got {stored:?}");
};
assert!(
stored.as_str_lossy().starts_with("SRC.VAL"),
"the accepted link is the one that was written, got {stored:?}"
);
}
#[epics_macros_rs::epics_test]
async fn a_link_field_takes_only_a_string_or_a_nul_terminated_char_array() {
use crate::server::records::calc::CalcRecord;
let db = PvDatabase::new();
db.add_record("TY:CALC", Box::new(CalcRecord::new("A")))
.await
.unwrap();
db.put_record_field_from_ca_no_notify(
"TY:CALC",
"SDIS",
EpicsValue::String("SRC.VAL".into()),
)
.await
.expect("a DBR_STRING link put is what C accepts");
for bad in [
EpicsValue::Double(5.0),
EpicsValue::Long(5),
EpicsValue::Short(5),
EpicsValue::Enum(1),
EpicsValue::Float(5.0),
EpicsValue::Int64(5),
EpicsValue::Char(b'S'),
EpicsValue::UChar(b'S'),
EpicsValue::CharArray(b"SRC.VAL".to_vec()),
EpicsValue::CharArray(b"SRC\0VAL".to_vec()),
] {
let ca = db
.put_record_field_from_ca_no_notify("TY:CALC", "SDIS", bad.clone())
.await;
assert!(
matches!(ca, Err(crate::server::database::CaError::BadDbrType(_))),
"a {bad:?} put to a link field is S_db_badDbrtype in C, got {ca:?}"
);
let restore = db.put_pv_no_process("TY:CALC.SDIS", bad.clone()).await;
assert!(
matches!(
restore,
Err(crate::server::database::CaError::BadDbrType(_))
),
"the autosave body is a dbPutField too, got {restore:?} for {bad:?}"
);
let held = db.get_pv("TY:CALC.SDIS").unwrap();
assert!(
matches!(&held, EpicsValue::String(s) if s.as_str_lossy().starts_with("SRC.VAL")),
"a refused put must leave the link text alone, got {held:?}"
);
}
db.put_record_field_from_ca_no_notify(
"TY:CALC",
"SDIS",
EpicsValue::CharArray(b"OTHER.VAL\0".to_vec()),
)
.await
.expect("a NUL-terminated DBR_CHAR buffer is accepted");
let held = db.get_pv("TY:CALC.SDIS").unwrap();
assert!(
matches!(&held, EpicsValue::String(s) if s.as_str_lossy().starts_with("OTHER.VAL")),
"the stored text is the C string in the buffer, got {held:?}"
);
db.put_record_field_from_ca_no_notify("TY:CALC", "SDIS", EpicsValue::Char(0))
.await
.expect("a lone NUL clears the link, as in C");
assert_eq!(
db.get_pv("TY:CALC.SDIS").unwrap(),
EpicsValue::String("".into()),
"C stores the empty C string, not the byte's decimal spelling"
);
db.put_record_field_from_ca_no_notify("TY:CALC", "DESC", EpicsValue::Double(5.0))
.await
.expect("a non-link string field converts, in C and here");
assert!(
matches!(db.get_pv("TY:CALC.DESC").unwrap(),
EpicsValue::String(s) if s.as_str_lossy() == "5"),
"the control must show the refusal is the link route, not the string type"
);
}
#[epics_macros_rs::epics_test]
async fn an_autosave_restore_runs_the_dbputconvertroutine_type_row() {
use crate::server::records::calc::CalcRecord;
let db = PvDatabase::new();
db.add_record("TY:CALC", Box::new(CalcRecord::new("A+1")))
.await
.unwrap();
db.put_pv_no_process("TY:CALC.A", EpicsValue::String("3.5".into()))
.await
.expect("a numeric string restore parses through the convert row");
assert!(
matches!(db.get_pv("TY:CALC.A").unwrap(),
EpicsValue::Double(v) if (v - 3.5).abs() < 1e-9),
"the restored A must be the parsed number 3.5, got {:?}",
db.get_pv("TY:CALC.A")
);
let bad = db
.put_pv_no_process("TY:CALC.A", EpicsValue::String("not_a_number".into()))
.await;
assert!(
bad.is_err(),
"an unparseable numeric restore is refused, got {bad:?}"
);
assert!(
matches!(db.get_pv("TY:CALC.A").unwrap(),
EpicsValue::Double(v) if (v - 3.5).abs() < 1e-9),
"a refused restore leaves A at 3.5, got {:?}",
db.get_pv("TY:CALC.A")
);
}
}