Validate configuration through its owning schemas and keep board declarations separate from shared F722 resource ownership. Bound USB transitions, isolate transport failures and retain restart-only activation. Migrate navigation and flight consumers with the sensor and schema contracts. Require explicit installation data and synchronous output authority; board motor outputs remain inhibited. Validation: assembled-prefix host tests, strict Clippy, platform/USB tests and both ARM release builds. Final-source coverage and scoped Miri/Kani evidence are retained separately from hardware qualification.
269 lines
7.7 KiB
Rust
269 lines
7.7 KiB
Rust
use kaptan_safety::*;
|
|
use kaptan_time::Timestamp;
|
|
|
|
fn time(value: u64) -> Timestamp {
|
|
Timestamp::from_microseconds(value)
|
|
}
|
|
fn healthy() -> SafetyInputs {
|
|
SafetyInputs {
|
|
estimator_healthy: true,
|
|
imu_fresh: true,
|
|
phase_authorized: true,
|
|
output_healthy: true,
|
|
}
|
|
}
|
|
fn supervisor() -> SafetySupervisor {
|
|
SafetySupervisor::new(SafetyConfig::new(100).unwrap())
|
|
}
|
|
fn evaluate(s: &mut SafetySupervisor, now: u64, inputs: SafetyInputs) -> CommitOutcome<(), ()> {
|
|
s.evaluate_and_commit(time(now), inputs, || Ok(()))
|
|
}
|
|
|
|
#[test]
|
|
fn dwell_and_owner_local_commit() {
|
|
assert_eq!(
|
|
SafetyConfig::new(0),
|
|
Err(SafetyConfigError::ZeroPrearmDwell)
|
|
);
|
|
let mut a = supervisor();
|
|
let mut b = supervisor();
|
|
assert_eq!(
|
|
a.request_arm(time(10), healthy()).state(),
|
|
SafetyState::PreArm
|
|
);
|
|
for now in [10, 109] {
|
|
assert!(matches!(
|
|
a.evaluate_and_commit(time(now), healthy(), || -> Result<(), ()> {
|
|
panic!("prearm cannot commit")
|
|
}),
|
|
CommitOutcome::Inhibited(_)
|
|
));
|
|
}
|
|
let mut calls = 0;
|
|
assert_eq!(
|
|
a.evaluate_and_commit(time(110), healthy(), || {
|
|
calls += 1;
|
|
Ok::<_, ()>(42)
|
|
}),
|
|
CommitOutcome::Committed(42)
|
|
);
|
|
assert_eq!(calls, 1);
|
|
// Healthy armed evaluations keep committing without restarting the dwell.
|
|
assert_eq!(
|
|
evaluate(&mut a, 111, healthy()),
|
|
CommitOutcome::Committed(())
|
|
);
|
|
assert!(matches!(
|
|
b.evaluate_and_commit(time(110), healthy(), || -> Result<(), ()> {
|
|
panic!("another owner's armed state cannot commit here")
|
|
}),
|
|
CommitOutcome::Inhibited(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn failed_arm_and_prearm_are_consumed() {
|
|
let mut s = supervisor();
|
|
let mut stale = healthy();
|
|
stale.imu_fresh = false;
|
|
assert_eq!(
|
|
s.request_arm(time(1), stale).failure(),
|
|
Some(SafetyFailure::ImuStale)
|
|
);
|
|
evaluate(&mut s, 200, healthy());
|
|
assert_eq!(s.state(), SafetyState::Disarmed);
|
|
s.request_arm(time(201), healthy());
|
|
evaluate(&mut s, 202, stale);
|
|
evaluate(&mut s, 400, healthy());
|
|
assert_eq!(s.state(), SafetyState::Disarmed);
|
|
}
|
|
|
|
#[test]
|
|
fn failed_arm_health_priority_and_recovery_require_a_new_transaction() {
|
|
for (inputs, expected) in [
|
|
(
|
|
SafetyInputs {
|
|
estimator_healthy: false,
|
|
imu_fresh: false,
|
|
phase_authorized: false,
|
|
output_healthy: false,
|
|
},
|
|
SafetyFailure::ActuatorFault,
|
|
),
|
|
(
|
|
SafetyInputs {
|
|
estimator_healthy: false,
|
|
imu_fresh: false,
|
|
phase_authorized: false,
|
|
..healthy()
|
|
},
|
|
SafetyFailure::EstimatorUnhealthy,
|
|
),
|
|
(
|
|
SafetyInputs {
|
|
imu_fresh: false,
|
|
phase_authorized: false,
|
|
..healthy()
|
|
},
|
|
SafetyFailure::ImuStale,
|
|
),
|
|
(
|
|
SafetyInputs {
|
|
phase_authorized: false,
|
|
..healthy()
|
|
},
|
|
SafetyFailure::PhaseUnauthorized,
|
|
),
|
|
] {
|
|
let mut s = supervisor();
|
|
let expected_state = if expected == SafetyFailure::ActuatorFault {
|
|
SafetyState::Failsafe
|
|
} else {
|
|
SafetyState::Disarmed
|
|
};
|
|
let decision = s.request_arm(time(0), inputs);
|
|
assert_eq!(decision.failure(), Some(expected));
|
|
assert_eq!(decision.state(), expected_state);
|
|
assert!(matches!(
|
|
evaluate(&mut s, 100, healthy()),
|
|
CommitOutcome::Inhibited(_)
|
|
));
|
|
assert_eq!(s.state(), expected_state);
|
|
if expected == SafetyFailure::ActuatorFault {
|
|
assert_eq!(
|
|
s.request_arm(time(101), healthy()).state(),
|
|
SafetyState::Failsafe
|
|
);
|
|
s.request_disarm();
|
|
}
|
|
assert_eq!(
|
|
s.request_arm(time(102), healthy()).state(),
|
|
SafetyState::PreArm
|
|
);
|
|
assert_eq!(
|
|
evaluate(&mut s, 202, healthy()),
|
|
CommitOutcome::Committed(())
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn regressing_arm_request_consumes_prearm_and_requires_explicit_reset() {
|
|
let mut s = supervisor();
|
|
assert_eq!(
|
|
s.request_arm(time(100), healthy()).state(),
|
|
SafetyState::PreArm
|
|
);
|
|
let decision = s.request_arm(time(99), healthy());
|
|
assert_eq!(decision.state(), SafetyState::Failsafe);
|
|
assert_eq!(decision.failure(), Some(SafetyFailure::ClockRegression));
|
|
assert!(matches!(
|
|
evaluate(&mut s, 200, healthy()),
|
|
CommitOutcome::Inhibited(_)
|
|
));
|
|
s.request_disarm();
|
|
s.reset_time_epoch(|| Ok::<_, ()>(())).unwrap();
|
|
assert_eq!(
|
|
s.request_arm(time(0), healthy()).state(),
|
|
SafetyState::PreArm
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn current_health_revokes_before_callback_and_recovery_cannot_rearm() {
|
|
let mut s = supervisor();
|
|
s.request_arm(time(0), healthy());
|
|
assert_eq!(
|
|
evaluate(&mut s, 100, healthy()),
|
|
CommitOutcome::Committed(())
|
|
);
|
|
let diagnostic = s.decision();
|
|
let mut stale = healthy();
|
|
stale.phase_authorized = false;
|
|
assert!(matches!(
|
|
s.evaluate_and_commit(time(101), stale, || -> Result<(), ()> {
|
|
panic!("stale evidence must inhibit before callback")
|
|
}),
|
|
CommitOutcome::Inhibited(_)
|
|
));
|
|
assert_eq!(diagnostic.state(), SafetyState::Armed); // retained diagnostic only
|
|
assert_eq!(
|
|
s.decision().failure(),
|
|
Some(SafetyFailure::PhaseUnauthorized)
|
|
);
|
|
assert!(matches!(
|
|
evaluate(&mut s, 102, healthy()),
|
|
CommitOutcome::Inhibited(_)
|
|
));
|
|
assert_eq!(
|
|
s.request_arm(time(103), healthy()).state(),
|
|
SafetyState::Failsafe
|
|
);
|
|
s.request_disarm();
|
|
assert_eq!(
|
|
s.request_arm(time(104), healthy()).state(),
|
|
SafetyState::PreArm
|
|
);
|
|
assert_eq!(
|
|
evaluate(&mut s, 204, healthy()),
|
|
CommitOutcome::Committed(())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn callback_error_and_actuator_health_require_disarm() {
|
|
let mut s = supervisor();
|
|
s.request_arm(time(0), healthy());
|
|
assert_eq!(
|
|
s.evaluate_and_commit(time(100), healthy(), || Err::<(), _>("commit")),
|
|
CommitOutcome::Failed("commit")
|
|
);
|
|
assert_eq!(s.state(), SafetyState::Failsafe);
|
|
evaluate(&mut s, 101, healthy());
|
|
assert_eq!(
|
|
s.request_arm(time(102), healthy()).state(),
|
|
SafetyState::Failsafe
|
|
);
|
|
s.request_disarm();
|
|
s.request_arm(time(103), healthy());
|
|
let mut broken = healthy();
|
|
broken.output_healthy = false;
|
|
evaluate(&mut s, 104, broken);
|
|
assert_eq!(s.decision().failure(), Some(SafetyFailure::ActuatorFault));
|
|
evaluate(&mut s, 300, healthy());
|
|
assert_eq!(s.state(), SafetyState::Failsafe);
|
|
}
|
|
|
|
#[test]
|
|
fn clock_fault_survives_disarm_and_failed_reset() {
|
|
let mut s = supervisor();
|
|
evaluate(&mut s, 100, healthy());
|
|
evaluate(&mut s, 99, healthy());
|
|
assert_eq!(s.decision().failure(), Some(SafetyFailure::ClockRegression));
|
|
assert_eq!(
|
|
s.reset_time_epoch(|| Ok::<_, ()>(())),
|
|
Err(ResetError::NotDisarmed)
|
|
);
|
|
s.request_disarm();
|
|
assert_eq!(
|
|
s.request_arm(time(101), healthy()).state(),
|
|
SafetyState::Failsafe
|
|
);
|
|
s.request_disarm();
|
|
assert_eq!(
|
|
s.reset_time_epoch(|| Err("inhibit")),
|
|
Err(ResetError::Inhibit("inhibit"))
|
|
);
|
|
s.request_disarm();
|
|
assert_eq!(
|
|
s.request_arm(time(102), healthy()).state(),
|
|
SafetyState::Failsafe
|
|
);
|
|
s.request_disarm();
|
|
s.reset_time_epoch(|| Ok::<_, ()>(())).unwrap();
|
|
assert_eq!(
|
|
s.request_arm(time(0), healthy()).state(),
|
|
SafetyState::PreArm
|
|
);
|
|
}
|