epics-base-rs 0.27.0

Pure Rust EPICS IOC core — record system, database, iocsh, calc engine
Documentation
//! A record delay field the network can set must not abort the IOC.
//!
//! C `boRecord.c` process passes `(double)prec->high` to
//! `callbackRequestDelayed`, which reaches `epicsTimeAddSeconds`
//! (`epicsTime.cpp`) and its out-of-range `epicsInt64(seconds*1e9 + …)`
//! conversion: the deadline is garbage, the one-shot fires at the wrong
//! time, and every other PV keeps being served. `caput BO:TEST.HIGH inf`
//! is reachable — `epicsParseDouble` accepts `inf` because `strtod`
//! leaves `errno` unset for it — and the port's
//! `Duration::from_secs_f64(self.high)` panicked on exactly that value,
//! unwinding the record's processing task.

use epics_base_rs::server::record::ProcessAction;
use epics_base_rs::server::record::Record;
use epics_base_rs::server::records::bo::BoRecord;
use epics_base_rs::types::EpicsValue;

/// Every non-representable `HIGH` a `caput` can deliver, one case per
/// boundary of `Duration::try_from_secs_f64`'s single rule.
#[test]
fn a_bo_high_the_network_can_set_never_unwinds_process() {
    for high in [f64::INFINITY, 1e300, u64::MAX as f64, f64::MAX] {
        let mut rec = BoRecord::new(0);
        rec.put_field("HIGH", EpicsValue::Double(high))
            .expect("HIGH accepts any double, as C's dbPutField does");
        rec.put_field("VAL", EpicsValue::Long(1))
            .expect("VAL 1 arms the HIGH one-shot");

        let outcome = rec
            .process()
            .unwrap_or_else(|e| panic!("HIGH={high} must process, got {e}"));

        let delay = outcome
            .actions
            .iter()
            .find_map(|a| match a {
                ProcessAction::DelayedCallbackAfter(d) => Some(*d),
                _ => None,
            })
            .unwrap_or_else(|| panic!("HIGH={high} must still arm the one-shot"));
        assert_eq!(
            delay,
            std::time::Duration::MAX,
            "HIGH={high} is a deadline no comparison reaches — C's garbage \
             deadline that never fires"
        );
    }

    // C arms the one-shot only under `(prec->high>0)`, which is false
    // for a negative and for NaN, so neither reaches the conversion at
    // all — on both sides.
    for high in [f64::NEG_INFINITY, f64::NAN, 0.0] {
        let mut rec = BoRecord::new(0);
        rec.put_field("HIGH", EpicsValue::Double(high)).unwrap();
        rec.put_field("VAL", EpicsValue::Long(1)).unwrap();
        let outcome = rec
            .process()
            .unwrap_or_else(|e| panic!("HIGH={high} must process, got {e}"));
        assert!(
            !outcome
                .actions
                .iter()
                .any(|a| matches!(a, ProcessAction::DelayedCallbackAfter(_))),
            "HIGH={high} fails C's `high > 0` test and arms nothing"
        );
    }
}