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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//! 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.
//!
//! `busy` is `bo` transcribed (`busyRecord.c:258-263` is `boRecord.c:257-262`
//! verbatim, module `busy` at `R1-7-4-6-g2dfe92d`), so it carries the same
//! DBF_DOUBLE HIGH and the same arming call. Both are driven through one
//! boundary table here: a fix that reaches only the record that was reported
//! leaves the other one panicking.
//!
//! **This file is coverage, not the fix.** Both records already route the
//! conversion through the saturating `runtime::time::duration_from_secs`, so
//! every case below passes on this file's own parent `33aadac8^`, where the
//! calls are `bo.rs:279`, `:309` and `busy.rs:282`, `:307`. They arrived
//! separately
//! and neither hash covers both: `busy.rs` in `a7c7913c`, `bo.rs` in
//! `0f1a5d33`, and at `a7c7913c` itself that `bo.rs` call is still the
//! panicking
//! `Duration::from_secs_f64`. What the table adds is the second record —
//! the earlier version asserted only `bo`,
//! leaving `busy`'s identical arming call unasserted — and a shape where the
//! next record type that arms a one-shot from a DBF_DOUBLE is one row rather
//! than a second file.
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::server::records::busy::BusyRecord;
use epics_base_rs::types::EpicsValue;
/// A record type that arms a one-shot from a `DBF_DOUBLE` HIGH, named for the
/// assertion messages, plus a constructor for a fresh instance of it.
type ArmingRecord = (&'static str, fn() -> Box<dyn Record>);
/// Every record type whose process arms a one-shot from a `DBF_DOUBLE` HIGH.
fn arming_records() -> [ArmingRecord; 2] {
[
("bo", || Box::new(BoRecord::new(0))),
("busy", || Box::new(BusyRecord::new())),
]
}
/// Every non-representable `HIGH` a `caput` can deliver, one case per
/// boundary of `Duration::try_from_secs_f64`'s single rule.
#[test]
fn a_high_the_network_can_set_never_unwinds_process() {
for (kind, make) in arming_records() {
for high in [f64::INFINITY, 1e300, u64::MAX as f64, f64::MAX] {
let mut rec = make();
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!("{kind} 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!("{kind} HIGH={high} must still arm the one-shot"));
assert_eq!(
delay,
std::time::Duration::MAX,
"{kind} 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 = make();
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!("{kind} HIGH={high} must process, got {e}"));
assert!(
!outcome
.actions
.iter()
.any(|a| matches!(a, ProcessAction::DelayedCallbackAfter(_))),
"{kind} HIGH={high} fails C's `high > 0` test and arms nothing"
);
}
}
}