#![allow(clippy::field_reassign_with_default)]
use epics_base_rs::server::record::{ProcessAction, Record};
use epics_base_rs::types::EpicsValue;
use std_rs::ThrottleRecord;
#[test]
fn test_record_type() {
let rec = ThrottleRecord::default();
assert_eq!(rec.record_type(), "throttle");
}
#[test]
fn test_default_values() {
let rec = ThrottleRecord::default();
assert_eq!(rec.val, 0.0);
assert_eq!(rec.dly, 0.0);
assert_eq!(rec.drvlh, 0.0);
assert_eq!(rec.drvll, 0.0);
assert_eq!(rec.drvlc, 0); assert_eq!(rec.wait, 0); assert_eq!(rec.sts, 0); }
#[test]
fn test_get_put_val() {
let mut rec = ThrottleRecord::default();
rec.put_field("VAL", EpicsValue::Double(42.0)).unwrap();
assert_eq!(rec.get_field("VAL"), Some(EpicsValue::Double(42.0)));
}
#[test]
fn test_get_put_dly() {
let mut rec = ThrottleRecord::default();
rec.put_field("DLY", EpicsValue::Double(1.5)).unwrap();
assert_eq!(rec.get_field("DLY"), Some(EpicsValue::Double(1.5)));
}
#[test]
fn test_get_put_limits() {
let mut rec = ThrottleRecord::default();
rec.put_field("DRVLH", EpicsValue::Double(100.0)).unwrap();
rec.put_field("DRVLL", EpicsValue::Double(0.0)).unwrap();
assert_eq!(rec.get_field("DRVLH"), Some(EpicsValue::Double(100.0)));
assert_eq!(rec.get_field("DRVLL"), Some(EpicsValue::Double(0.0)));
}
#[test]
fn test_read_only_fields() {
let mut rec = ThrottleRecord::default();
assert!(rec.put_field("OVAL", EpicsValue::Double(1.0)).is_err());
assert!(rec.put_field("SENT", EpicsValue::Double(1.0)).is_err());
assert!(rec.put_field("OSENT", EpicsValue::Double(1.0)).is_err());
assert!(rec.put_field("WAIT", EpicsValue::Short(1)).is_err());
assert!(rec.put_field("DRVLS", EpicsValue::Short(1)).is_err());
assert!(
rec.put_field("VER", EpicsValue::String("x".into()))
.is_err()
);
assert!(rec.put_field("STS", EpicsValue::Short(1)).is_err());
assert!(rec.put_field("OV", EpicsValue::Short(1)).is_err());
assert!(rec.put_field("SIV", EpicsValue::Short(1)).is_err());
}
#[test]
fn test_type_mismatch() {
let mut rec = ThrottleRecord::default();
assert!(
rec.put_field("VAL", EpicsValue::String("bad".into()))
.is_err()
);
assert!(rec.put_field("PREC", EpicsValue::Double(1.0)).is_err());
}
#[test]
fn test_unknown_field() {
let rec = ThrottleRecord::default();
assert!(rec.get_field("NONEXISTENT").is_none());
let mut rec = rec;
assert!(
rec.put_field("NONEXISTENT", EpicsValue::Double(1.0))
.is_err()
);
}
#[test]
fn test_process_sends_value_no_delay() {
let mut rec = ThrottleRecord::default();
rec.dly = 0.0; rec.out = "OUTPUT:PV".to_string();
rec.val = 42.0;
process_ok(&mut rec);
assert_eq!(rec.sent, 42.0);
assert_eq!(rec.sts, 2); assert_eq!(rec.wait, 0); }
#[test]
fn test_process_sends_value_with_delay() {
let mut rec = ThrottleRecord::default();
rec.dly = 1.0; rec.out = "OUTPUT:PV".to_string();
rec.val = 42.0;
let actions = process_ok(&mut rec);
assert_eq!(rec.sent, 42.0);
assert_eq!(rec.wait, 0, "immediate send wrote the value -> WAIT clear");
let has_reprocess = actions
.iter()
.any(|a| matches!(a, ProcessAction::ReprocessAfter(_)));
assert!(has_reprocess, "Should have ReprocessAfter action");
let has_write = actions
.iter()
.any(|a| matches!(a, ProcessAction::WriteDbLink { .. }));
assert!(has_write, "Should have WriteDbLink action for OUT");
}
#[test]
fn test_process_queues_during_delay() {
let mut rec = ThrottleRecord::default();
rec.dly = 10.0; rec.out = "OUTPUT:PV".to_string();
rec.val = 42.0;
process_ok(&mut rec); assert_eq!(rec.sent, 42.0);
assert_eq!(rec.wait, 0, "immediate send -> WAIT clear, none queued");
rec.val = 99.0;
let actions = process_ok(&mut rec);
let has_reprocess = actions
.iter()
.any(|a| matches!(a, ProcessAction::ReprocessAfter(_)));
assert!(
!has_reprocess,
"queuing must not re-arm the cooldown — C `enterValue` requests no callback"
);
assert_eq!(rec.sent, 42.0); assert_eq!(rec.wait, 1, "value queued during the delay -> WAIT set");
}
#[test]
fn test_process_updates_oval() {
let mut rec = ThrottleRecord::default();
rec.dly = 0.0;
rec.val = 10.0;
rec.process().unwrap();
assert_eq!(rec.oval, 10.0);
rec.val = 20.0;
rec.process().unwrap();
assert_eq!(rec.oval, 20.0);
}
#[test]
fn test_process_osent_tracking() {
let mut rec = ThrottleRecord::default();
rec.dly = 0.0;
rec.out = "OUTPUT:PV".to_string();
rec.val = 10.0;
process_ok(&mut rec);
assert_eq!(rec.sent, 10.0);
assert_eq!(rec.osent, 0.0);
rec.val = 20.0;
process_ok(&mut rec);
assert_eq!(rec.sent, 20.0);
assert_eq!(rec.osent, 10.0); }
#[test]
fn test_limit_clipping_on() {
let mut rec = ThrottleRecord::default();
rec.drvlh = 100.0;
rec.drvll = 0.0;
rec.drvlc = 1; rec.dly = 0.0; rec.out = "OUTPUT:PV".to_string();
rec.init_record(1).unwrap();
rec.val = 150.0;
process_ok(&mut rec);
assert_eq!(rec.sent, 100.0);
assert_eq!(rec.drvls, 2); assert_eq!(rec.sts, 2); }
#[test]
fn test_limit_clipping_low() {
let mut rec = ThrottleRecord::default();
rec.drvlh = 100.0;
rec.drvll = 10.0;
rec.drvlc = 1; rec.dly = 0.0;
rec.out = "OUTPUT:PV".to_string();
rec.init_record(1).unwrap();
rec.val = 5.0;
process_ok(&mut rec);
assert_eq!(rec.sent, 10.0);
assert_eq!(rec.drvls, 1); }
#[test]
fn test_limit_rejection() {
let mut rec = ThrottleRecord::default();
rec.drvlh = 100.0;
rec.drvll = 0.0;
rec.drvlc = 0; rec.dly = 0.0;
rec.init_record(1).unwrap();
rec.oval = 50.0; rec.val = 150.0; rec.process().unwrap();
assert_eq!(rec.val, 50.0, "VAL restored to OVAL on rejection");
assert_eq!(
rec.sts, 0,
"STS must stay Unknown — C never sets STS in the limit block"
);
assert_eq!(rec.drvls, 2, "DRVLS reports High limit (C line 272)");
assert_eq!(rec.sent, 0.0, "nothing sent on a rejected value");
}
#[test]
fn test_no_limits_when_equal() {
let mut rec = ThrottleRecord::default();
rec.drvlh = 0.0;
rec.drvll = 0.0; rec.dly = 0.0;
rec.out = "OUTPUT:PV".to_string();
rec.init_record(1).unwrap();
rec.val = 999.0;
process_ok(&mut rec);
assert_eq!(rec.sent, 999.0);
assert_eq!(rec.drvls, 0); }
#[test]
fn test_pending_value_clamped_to_drive_limit_on_drain() {
let mut rec = ThrottleRecord::default();
rec.drvlh = 100.0;
rec.drvll = 0.0;
rec.drvlc = 1; rec.dly = 0.05; rec.out = "OUTPUT:PV".to_string();
rec.init_record(1).unwrap();
rec.val = 50.0;
process_ok(&mut rec);
assert_eq!(rec.sent, 50.0);
assert_eq!(rec.wait, 0, "immediate send -> WAIT clear, none queued");
rec.val = 150.0;
process_ok(&mut rec);
assert_eq!(rec.sent, 50.0, "queued value must not be sent yet");
assert_eq!(rec.wait, 1, "value queued during the delay -> WAIT set");
let actions = fire_timer(&mut rec);
assert_eq!(rec.sent, 100.0, "drained value must be clamped to DRVLH");
assert_eq!(rec.drvls, 2, "DRVLS must report High limit");
let written = actions.iter().find_map(|a| match a {
ProcessAction::WriteDbLink { value, .. } => Some(value),
_ => None,
});
assert_eq!(
written,
Some(&EpicsValue::Double(100.0)),
"value written to OUT must be the clamped 100.0, not raw 150.0"
);
}
#[test]
fn test_pending_value_rejected_on_drain_when_clipping_off() {
let mut rec = ThrottleRecord::default();
rec.drvlh = 100.0;
rec.drvll = 0.0;
rec.drvlc = 0; rec.dly = 0.05;
rec.out = "OUTPUT:PV".to_string();
rec.init_record(1).unwrap();
rec.val = 50.0;
process_ok(&mut rec);
assert_eq!(rec.sent, 50.0);
rec.val = 150.0; process_ok(&mut rec);
assert_eq!(rec.val, 50.0, "out-of-range value restored to OVAL=50");
assert_eq!(rec.drvls, 2, "DRVLS reports High limit");
let actions = fire_timer(&mut rec);
assert_eq!(rec.sent, 50.0, "rejected value must not reach OUT");
assert_eq!(
rec.sts, 2,
"STS stays Success from the first send — C never sets STS on a limit rejection"
);
let has_write = actions
.iter()
.any(|a| matches!(a, ProcessAction::WriteDbLink { .. }));
assert!(!has_write, "drain has nothing queued — no OUT write");
}
#[test]
fn test_special_dly_clamp_negative() {
let mut rec = ThrottleRecord::default();
rec.dly = -5.0;
rec.special("DLY", true).unwrap();
assert_eq!(rec.dly, 0.0);
}
#[test]
fn test_special_dly_positive() {
let mut rec = ThrottleRecord::default();
rec.dly = 2.5;
rec.special("DLY", true).unwrap();
assert_eq!(rec.dly, 2.5); }
#[test]
fn test_special_drvlh_drvll_enables_limits() {
let mut rec = ThrottleRecord::default();
rec.drvlh = 100.0;
rec.drvll = 0.0;
rec.special("DRVLH", true).unwrap();
rec.val = 150.0;
rec.process().unwrap();
assert_eq!(rec.sts, 0, "STS unchanged on a limit rejection");
assert_eq!(rec.drvls, 2, "DRVLS reports High limit");
assert_eq!(rec.sent, 0.0, "rejected value is not sent");
}
#[test]
fn test_special_drvlh_drvll_recomputes_drvls() {
let mut rec = ThrottleRecord::default();
rec.val = 500.0;
rec.drvlh = 100.0;
rec.drvll = 0.0;
rec.special("DRVLH", true).unwrap();
assert_eq!(rec.drvls, 2, "VAL above DRVLH -> DRVLS High");
rec.val = -5.0;
rec.special("DRVLL", true).unwrap();
assert_eq!(rec.drvls, 1, "VAL below DRVLL -> DRVLS Low");
rec.val = 50.0;
rec.special("DRVLH", true).unwrap();
assert_eq!(rec.drvls, 0, "VAL within limits -> DRVLS Normal");
rec.val = 500.0;
rec.drvlh = 0.0;
rec.drvll = 0.0;
rec.special("DRVLH", true).unwrap();
assert_eq!(rec.drvls, 0, "limits disabled -> DRVLS Normal");
}
#[test]
fn test_ver_string() {
let rec = ThrottleRecord::default();
assert_eq!(rec.ver, "0-2-1");
assert_eq!(
rec.get_field("VER"),
Some(EpicsValue::String("0-2-1".into()))
);
}
#[test]
fn test_init_record_resets_state() {
let mut rec = ThrottleRecord::default();
rec.val = 42.0;
rec.sts = 2;
rec.init_record(1).unwrap();
assert_eq!(rec.val, 0.0, "init_record pass 1 resets VAL to 0");
assert_eq!(rec.sts, 0, "init_record pass 1 resets STS to Unknown");
}
#[test]
fn test_limit_clipping_low_bound_order() {
let mut rec = ThrottleRecord::default();
rec.drvlh = 100.0;
rec.drvll = 10.0;
rec.drvlc = 1; rec.dly = 0.0;
rec.out = "OUTPUT:PV".to_string();
rec.init_record(1).unwrap();
rec.val = -50.0; process_ok(&mut rec);
assert_eq!(rec.sent, 10.0, "clamped to DRVLL");
assert_eq!(rec.drvls, 1, "DRVLS Low");
}
#[test]
fn test_can_device_write() {
let rec = ThrottleRecord::default();
assert!(rec.can_device_write());
}
#[test]
fn test_should_fire_forward_link_only_when_out_written() {
let mut rec = ThrottleRecord::default();
rec.dly = 0.0;
rec.out = "OUTPUT:PV".to_string();
rec.val = 42.0;
rec.process().unwrap();
assert!(
rec.should_fire_forward_link(),
"a cycle that wrote OUT must fire FLNK"
);
}
#[test]
fn test_no_forward_link_on_queuing_cycle() {
let mut rec = ThrottleRecord::default();
rec.dly = 10.0; rec.out = "OUTPUT:PV".to_string();
rec.val = 42.0;
rec.process().unwrap(); assert!(
rec.should_fire_forward_link(),
"first send wrote OUT — FLNK fires"
);
rec.val = 99.0;
rec.process().unwrap();
assert!(
!rec.should_fire_forward_link(),
"a queuing-during-delay cycle writes no OUT — C never fires FLNK \
(recGblFwdLink commented out in process(), valuePut not reached)"
);
}
#[test]
fn test_no_forward_link_on_rejected_cycle() {
let mut rec = ThrottleRecord::default();
rec.drvlh = 100.0;
rec.drvll = 0.0;
rec.drvlc = 0; rec.dly = 0.0;
rec.out = "OUTPUT:PV".to_string();
rec.init_record(1).unwrap();
rec.oval = 50.0;
rec.val = 150.0; rec.process().unwrap();
assert!(
!rec.should_fire_forward_link(),
"a rejected out-of-range cycle writes no OUT — no FLNK"
);
}
#[test]
fn test_no_forward_link_on_drain_with_nothing_queued() {
let mut rec = ThrottleRecord::default();
rec.dly = 0.05;
rec.out = "OUTPUT:PV".to_string();
rec.val = 10.0;
rec.process().unwrap(); assert!(rec.should_fire_forward_link());
rec.set_process_continuation(true);
rec.process().unwrap();
assert!(
!rec.should_fire_forward_link(),
"a drain cycle with nothing queued writes no OUT — no FLNK"
);
}
#[test]
fn test_constant_out_link_reports_error_and_no_write() {
let mut rec = ThrottleRecord::default();
rec.dly = 0.0;
rec.out = "5.0".to_string(); rec.val = 42.0;
let outcome = rec.process().unwrap();
assert_eq!(
rec.sts, 1,
"a CONSTANT OUT link must report STS=Error (C valuePut else branch)"
);
assert_eq!(
rec.sent, 0.0,
"a CONSTANT OUT link is never written — SENT must not advance"
);
let has_write = outcome
.actions
.iter()
.any(|a| matches!(a, ProcessAction::WriteDbLink { .. }));
assert!(!has_write, "a CONSTANT OUT link emits no WriteDbLink");
assert!(
!rec.should_fire_forward_link(),
"a CONSTANT OUT cycle writes no OUT — no FLNK (C fires FLNK only \
in valuePut's non-CONSTANT branch)"
);
}
#[test]
fn test_empty_out_link_reports_error_and_no_write() {
let mut rec = ThrottleRecord::default();
rec.dly = 0.0;
rec.val = 42.0;
let outcome = rec.process().unwrap();
assert_eq!(rec.sts, 1, "an empty OUT link must report STS=Error");
assert_eq!(rec.sent, 0.0, "an empty OUT link is never written");
let has_write = outcome
.actions
.iter()
.any(|a| matches!(a, ProcessAction::WriteDbLink { .. }));
assert!(!has_write, "an empty OUT link emits no WriteDbLink");
}
#[test]
fn test_real_out_link_reports_success_and_writes() {
let mut rec = ThrottleRecord::default();
rec.dly = 0.0;
rec.out = "OUTPUT:PV".to_string(); rec.val = 42.0;
let actions = process_ok(&mut rec);
assert_eq!(rec.sts, 2, "a real OUT link write reports STS=Success");
assert_eq!(rec.sent, 42.0, "a real OUT link advances SENT");
let has_write = actions
.iter()
.any(|a| matches!(a, ProcessAction::WriteDbLink { .. }));
assert!(has_write, "a real OUT link emits a WriteDbLink");
}
#[test]
fn test_dly_infinity_rejected() {
let mut rec = ThrottleRecord::default();
assert!(
rec.put_field("DLY", EpicsValue::Double(f64::INFINITY))
.is_err(),
"a CA put of DLY = +inf must be rejected, not stored"
);
assert_eq!(rec.dly, 0.0, "DLY must keep its prior finite value");
}
#[test]
fn test_dly_neg_infinity_rejected() {
let mut rec = ThrottleRecord::default();
assert!(
rec.put_field("DLY", EpicsValue::Double(f64::NEG_INFINITY))
.is_err(),
"a CA put of DLY = -inf must be rejected"
);
assert_eq!(rec.dly, 0.0);
}
#[test]
fn test_dly_nan_rejected() {
let mut rec = ThrottleRecord::default();
assert!(
rec.put_field("DLY", EpicsValue::Double(f64::NAN)).is_err(),
"a CA put of DLY = NaN must be rejected"
);
assert_eq!(rec.dly, 0.0);
}
#[test]
fn test_dly_infinity_does_not_panic_process() {
let mut rec = ThrottleRecord::default();
rec.out = "OUTPUT:PV".to_string();
let _ = rec.put_field("DLY", EpicsValue::Double(f64::INFINITY));
rec.val = 1.0;
rec.process().unwrap();
}
#[test]
fn test_dly_huge_finite_rejected() {
let mut rec = ThrottleRecord::default();
assert!(
rec.put_field("DLY", EpicsValue::Double(1e300)).is_err(),
"a CA put of DLY = 1e300 must be rejected, not stored"
);
assert_eq!(rec.dly, 0.0, "DLY must keep its prior finite value");
}
#[test]
fn test_dly_huge_finite_does_not_panic_process() {
let mut rec = ThrottleRecord::default();
rec.out = "OUTPUT:PV".to_string();
assert!(
rec.put_field("DLY", EpicsValue::Double(1e300)).is_err(),
"DLY = 1e300 must be rejected"
);
assert_eq!(rec.dly, 0.0, "rejected put must leave DLY unchanged");
rec.val = 1.0;
process_ok(&mut rec);
assert_eq!(rec.sent, 1.0, "value must have been sent");
assert_eq!(rec.wait, 0, "no delay armed, WAIT stays clear");
}
#[test]
fn test_dly_huge_finite_assigned_directly_does_not_panic_process() {
let mut rec = ThrottleRecord::default();
rec.out = "OUTPUT:PV".to_string();
rec.dly = 1e300;
rec.special("DLY", true).unwrap();
assert!(
rec.dly.is_finite() && rec.dly <= 86_400.0,
"special() must clamp a huge DLY to a Duration-safe ceiling, got {}",
rec.dly
);
rec.val = 1.0;
let outcome = rec.process().unwrap();
assert_eq!(
rec.wait, 0,
"a positive DLY arms the delay but the value is written -> WAIT clear"
);
let _ = outcome;
}
fn armed(dly: f64) -> ThrottleRecord {
let mut rec = ThrottleRecord::default();
rec.out = "OUTPUT:PV".to_string();
rec.dly = dly;
rec.val = 5.0;
process_ok(&mut rec);
assert_eq!(rec.sent, 5.0, "first value is written immediately");
assert_eq!(rec.wait, 0, "written, nothing waiting");
rec
}
fn put_dly(rec: &mut ThrottleRecord, dly: f64) -> Vec<ProcessAction> {
rec.put_field("DLY", EpicsValue::Double(dly)).unwrap();
rec.special("DLY", true).unwrap();
rec.take_special_actions()
}
fn fire_timer(rec: &mut ThrottleRecord) -> Vec<ProcessAction> {
rec.set_process_continuation(true);
process_ok(rec)
}
fn writes(actions: &[ProcessAction]) -> Vec<f64> {
actions
.iter()
.filter_map(|a| match a {
ProcessAction::WriteDbLink {
value: EpicsValue::Double(v),
..
} => Some(*v),
_ => None,
})
.collect()
}
fn timer_with_nothing_queued(dly_move: Option<f64>) {
let mut rec = armed(10.0);
if let Some(dly) = dly_move {
put_dly(&mut rec, dly);
}
let actions = fire_timer(&mut rec);
assert_eq!(writes(&actions), Vec::<f64>::new(), "nothing to write");
assert_eq!(rec.sent, 5.0, "SENT must not move");
assert_eq!(rec.wait, 0, "nothing is waiting — WAIT must stay clear");
assert!(
!rec.should_fire_forward_link(),
"no OUT write — C never reaches recGblFwdLink"
);
}
#[test]
fn timer_fire_with_nothing_queued_and_dly_unchanged_writes_nothing() {
timer_with_nothing_queued(None);
}
#[test]
fn timer_fire_with_nothing_queued_and_dly_raised_writes_nothing() {
timer_with_nothing_queued(Some(60.0));
}
#[test]
fn timer_fire_with_nothing_queued_and_dly_lowered_writes_nothing() {
timer_with_nothing_queued(Some(0.5));
}
#[test]
fn timer_fire_with_nothing_queued_and_dly_zeroed_writes_nothing() {
timer_with_nothing_queued(Some(0.0));
}
fn timer_with_value_queued(dly_move: Option<f64>) {
let mut rec = armed(10.0);
rec.val = 9.0;
let queued = process_ok(&mut rec);
assert_eq!(writes(&queued), Vec::<f64>::new(), "queued, not sent");
assert_eq!(rec.wait, 1, "a value is waiting");
if let Some(dly) = dly_move {
put_dly(&mut rec, dly);
}
let actions = fire_timer(&mut rec);
assert_eq!(
writes(&actions),
vec![9.0],
"the queued value goes out once"
);
assert_eq!(rec.sent, 9.0);
assert_eq!(rec.wait, 0, "written — WAIT clears (C :575)");
assert!(
rec.should_fire_forward_link(),
"a real OUT write fires FLNK"
);
let again = fire_timer(&mut rec);
assert_eq!(
writes(&again),
Vec::<f64>::new(),
"the queue was drained — a second fire must not resend"
);
assert!(
!rec.should_fire_forward_link(),
"no second OUT write — no second FLNK"
);
}
#[test]
fn timer_fire_with_value_queued_and_dly_unchanged_sends_it_once() {
timer_with_value_queued(None);
}
#[test]
fn timer_fire_with_value_queued_and_dly_raised_sends_it_once() {
timer_with_value_queued(Some(60.0));
}
#[test]
fn timer_fire_with_value_queued_and_dly_lowered_sends_it_once() {
timer_with_value_queued(Some(0.5));
}
#[test]
fn timer_fire_with_value_queued_and_dly_zeroed_sends_it_once() {
timer_with_value_queued(Some(0.0));
}
#[test]
fn dly_put_reanchors_the_running_cooldown() {
let mut rec = armed(10.0);
let actions = put_dly(&mut rec, 2.0);
assert_eq!(
actions
.iter()
.filter_map(|a| match a {
ProcessAction::ReprocessAfter(d) => Some(d.as_secs_f64()),
_ => None,
})
.collect::<Vec<_>>(),
vec![2.0],
"a DLY put during the cooldown re-arms the timer with the NEW delay"
);
}
#[test]
fn dly_put_with_no_cooldown_running_arms_nothing() {
let mut rec = ThrottleRecord::default();
rec.out = "OUTPUT:PV".to_string();
let actions = put_dly(&mut rec, 2.0);
assert!(
actions.is_empty(),
"C only cancels/re-requests when delay_flag is set (:400)"
);
}
fn report_put(rec: &mut ThrottleRecord, actions: &[ProcessAction], failed: bool) {
for a in actions {
if let ProcessAction::WriteDbLink { link_field, value } = a {
rec.set_out_link_write_status(link_field, value, failed);
}
}
}
fn process_ok(rec: &mut ThrottleRecord) -> Vec<ProcessAction> {
let actions = rec.process().unwrap().actions;
report_put(rec, &actions, false);
actions
}
#[test]
fn process_commits_no_sts_or_sent_before_the_out_put_runs() {
let mut rec = ThrottleRecord::default();
rec.out = "OUTPUT:PV".to_string();
rec.val = 5.0;
let outcome = rec.process().unwrap();
assert_eq!(writes(&outcome.actions), vec![5.0], "the put is emitted");
assert_eq!(
rec.sts, 0,
"STS stays Unknown until the put reports — C reads it from dbPutLink"
);
assert_eq!(rec.sent, 0.0, "SENT advances only on a successful put");
}
#[test]
fn successful_out_put_sets_sts_success_and_advances_sent() {
let mut rec = ThrottleRecord::default();
rec.out = "OUTPUT:PV".to_string();
rec.val = 5.0;
let first = rec.process().unwrap();
report_put(&mut rec, &first.actions, false);
assert_eq!(rec.sts, 2, "STS=Success (C :567)");
assert_eq!(rec.sent, 5.0, "SENT takes the value that landed (C :568)");
assert_eq!(rec.osent, 0.0);
rec.val = 7.0;
let second = rec.process().unwrap();
report_put(&mut rec, &second.actions, false);
assert_eq!(rec.sent, 7.0);
assert_eq!(rec.osent, 5.0, "OSENT trails SENT by one send");
}
#[test]
fn failed_out_put_sets_sts_error_and_leaves_sent() {
let mut rec = ThrottleRecord::default();
rec.out = "NO:SUCH:REMOTE:PV".to_string();
rec.val = 5.0;
let outcome = rec.process().unwrap();
report_put(&mut rec, &outcome.actions, true);
assert_eq!(rec.sts, 1, "STS=Error (C :574-575)");
assert_eq!(rec.sent, 0.0, "a put that failed must not advance SENT");
assert_eq!(rec.osent, 0.0);
}
#[test]
fn failed_out_put_still_fires_the_forward_link() {
let mut rec = ThrottleRecord::default();
rec.out = "NO:SUCH:REMOTE:PV".to_string();
rec.val = 5.0;
let outcome = rec.process().unwrap();
report_put(&mut rec, &outcome.actions, true);
assert!(rec.should_fire_forward_link());
}
#[test]
fn a_failed_put_after_a_successful_one_reports_error() {
let mut rec = ThrottleRecord::default();
rec.out = "OUTPUT:PV".to_string();
rec.val = 5.0;
let ok = rec.process().unwrap();
report_put(&mut rec, &ok.actions, false);
assert_eq!(rec.sts, 2);
rec.val = 6.0;
let bad = rec.process().unwrap();
report_put(&mut rec, &bad.actions, true);
assert_eq!(
rec.sts, 1,
"STS follows the latest put, not the last good one"
);
assert_eq!(rec.sent, 5.0, "SENT still reads the value that landed");
}