1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//! 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"
);
}
}