test(v0.4): integration — thermal-trip/abort + MCU fail-safe-cut (session-шина)
+ фикс B09-таймера: last_heartbeat Option<u64> вместо сентинела 0 (monotonic_secs() стартует с 0 → первый heartbeat попадал на 0, guard !=0 ложно трактовал как «не было» → cut не срабатывал). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
@@ -65,7 +65,7 @@ struct MockState {
|
|||||||
soc_decoder: FrameDecoder,
|
soc_decoder: FrameDecoder,
|
||||||
mcu_seq: u8,
|
mcu_seq: u8,
|
||||||
out: Vec<u8>,
|
out: Vec<u8>,
|
||||||
last_heartbeat: u64,
|
last_heartbeat: Option<u64>, // None = heartbeat ещё не было (sentinel 0 коллизировал с monotonic-стартом)
|
||||||
shutdown_at: Option<u64>,
|
shutdown_at: Option<u64>,
|
||||||
safe_to_cut: bool,
|
safe_to_cut: bool,
|
||||||
hung: bool,
|
hung: bool,
|
||||||
@@ -105,7 +105,7 @@ impl Coprocessor for MockCoprocessor {
|
|||||||
match msg {
|
match msg {
|
||||||
SocToMcu::Heartbeat => {
|
SocToMcu::Heartbeat => {
|
||||||
if !s.hung {
|
if !s.hung {
|
||||||
s.last_heartbeat = now;
|
s.last_heartbeat = Some(now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SocToMcu::ShutdownImminent { .. } => s.shutdown_at = Some(now),
|
SocToMcu::ShutdownImminent { .. } => s.shutdown_at = Some(now),
|
||||||
@@ -121,10 +121,10 @@ impl Coprocessor for MockCoprocessor {
|
|||||||
let s = self.st.lock().unwrap();
|
let s = self.st.lock().unwrap();
|
||||||
match s.shutdown_at {
|
match s.shutdown_at {
|
||||||
// running: тишина heartbeat дольше FAILSAFE_MISS окон → SoC завис → cut
|
// running: тишина heartbeat дольше FAILSAFE_MISS окон → SoC завис → cut
|
||||||
None => {
|
// None last_heartbeat = heartbeat ещё не было → не режем (startup, не зависание)
|
||||||
s.last_heartbeat != 0
|
None => s
|
||||||
&& s.now.saturating_sub(s.last_heartbeat) > FAILSAFE_MISS * HEARTBEAT_SECS
|
.last_heartbeat
|
||||||
}
|
.is_some_and(|h| s.now.saturating_sub(h) > FAILSAFE_MISS * HEARTBEAT_SECS),
|
||||||
// shutdown: бюджет истёк без safe-to-cut → cut
|
// shutdown: бюджет истёк без safe-to-cut → cut
|
||||||
Some(t0) => !s.safe_to_cut && s.now.saturating_sub(t0) > HOLDUP_BUDGET_SECS,
|
Some(t0) => !s.safe_to_cut && s.now.saturating_sub(t0) > HOLDUP_BUDGET_SECS,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,3 +104,101 @@ async fn shutdown_imminent_then_abort() {
|
|||||||
aborted.next().await.unwrap();
|
aborted.next().await.unwrap();
|
||||||
assert_eq!(power.power_state().await.unwrap(), PowerState::Running);
|
assert_eq!(power.power_state().await.unwrap(), PowerState::Running);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore = "нужна session-шина: just test-integration"]
|
||||||
|
async fn thermal_trip_then_clear() {
|
||||||
|
let svc = PowerService::new();
|
||||||
|
let fsm = svc.fsm_handle();
|
||||||
|
let thermal_state = svc.thermal_state_handle();
|
||||||
|
let temp = Arc::new(MockTempSource::new(20));
|
||||||
|
let copro = Arc::new(MockCoprocessor::new());
|
||||||
|
|
||||||
|
let server = zbus::Connection::session().await.unwrap();
|
||||||
|
server
|
||||||
|
.object_server()
|
||||||
|
.at(names::power::PATH, svc)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
server.request_name(names::power::NAME).await.unwrap();
|
||||||
|
let iface = server
|
||||||
|
.object_server()
|
||||||
|
.interface::<_, PowerService>(names::power::PATH)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let ctx = iface.signal_context().to_owned();
|
||||||
|
shturman_power::service::spawn_loops(
|
||||||
|
fsm,
|
||||||
|
thermal_state,
|
||||||
|
temp.clone() as Arc<dyn shturman_power::thermal::TempSource>,
|
||||||
|
Arc::new(shturman_power::thermal::NoopThrottler::default())
|
||||||
|
as Arc<dyn shturman_power::thermal::Throttler>,
|
||||||
|
copro as Arc<dyn shturman_power::coprocessor::Coprocessor>,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
let client = zbus::Connection::session().await.unwrap();
|
||||||
|
let power = PowerClient::new(&client).await.unwrap();
|
||||||
|
let mut imminent = power.proxy().receive_shutdown_imminent().await.unwrap();
|
||||||
|
let mut aborted = power.proxy().receive_shutdown_aborted().await.unwrap();
|
||||||
|
|
||||||
|
// перегрев → ShutdownImminent(thermal)
|
||||||
|
temp.set(99);
|
||||||
|
let sig = imminent.next().await.unwrap();
|
||||||
|
assert_eq!(sig.args().unwrap().reason(), "thermal");
|
||||||
|
assert_eq!(power.power_state().await.unwrap(), PowerState::ShuttingDown);
|
||||||
|
|
||||||
|
// остыло до PONR → ShutdownAborted + running
|
||||||
|
temp.set(20);
|
||||||
|
aborted.next().await.unwrap();
|
||||||
|
assert_eq!(power.power_state().await.unwrap(), PowerState::Running);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore = "нужна session-шина: just test-integration"]
|
||||||
|
async fn mcu_failsafe_cuts_on_hang() {
|
||||||
|
let svc = PowerService::new();
|
||||||
|
let fsm = svc.fsm_handle();
|
||||||
|
let thermal_state = svc.thermal_state_handle();
|
||||||
|
let temp = Arc::new(MockTempSource::new(20));
|
||||||
|
let copro = Arc::new(MockCoprocessor::new());
|
||||||
|
|
||||||
|
let server = zbus::Connection::session().await.unwrap();
|
||||||
|
server
|
||||||
|
.object_server()
|
||||||
|
.at(names::power::PATH, svc)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
server.request_name(names::power::NAME).await.unwrap();
|
||||||
|
let iface = server
|
||||||
|
.object_server()
|
||||||
|
.interface::<_, PowerService>(names::power::PATH)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let ctx = iface.signal_context().to_owned();
|
||||||
|
shturman_power::service::spawn_loops(
|
||||||
|
fsm,
|
||||||
|
thermal_state,
|
||||||
|
temp as Arc<dyn shturman_power::thermal::TempSource>,
|
||||||
|
Arc::new(shturman_power::thermal::NoopThrottler::default())
|
||||||
|
as Arc<dyn shturman_power::thermal::Throttler>,
|
||||||
|
copro.clone() as Arc<dyn shturman_power::coprocessor::Coprocessor>,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
let client = zbus::Connection::session().await.unwrap();
|
||||||
|
let power = PowerClient::new(&client).await.unwrap();
|
||||||
|
assert_eq!(power.power_state().await.unwrap(), PowerState::Running);
|
||||||
|
|
||||||
|
// дать coproc-циклу послать ≥1 heartbeat (иначе last_heartbeat=0 и guard не даст cut)
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(1300)).await;
|
||||||
|
copro.hang(); // SoC завис → heartbeat не освежает таймер
|
||||||
|
// ждём, пока coproc-цикл (HEARTBEAT=1с) накопит > FAILSAFE_MISS окон и сделает FailsafeCut
|
||||||
|
for _ in 0..10 {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(700)).await;
|
||||||
|
if power.power_state().await.unwrap() == PowerState::Off {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(power.power_state().await.unwrap(), PowerState::Off);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user