use std::sync::Arc;
use std::time::{Duration, Instant};
use ek_core::main_engine::MainEngine;
use ek_event::EVENT_TIMER;
use ek_event::Event;
use ek_station::{StationConfig, boot};
use ek_ui::state::{ConfirmStep, UiCmd, UiShared, is_quarantined};
fn wait_until(tag: &str, secs: u64, f: impl Fn() -> bool) {
let deadline = Instant::now() + Duration::from_secs(secs);
while Instant::now() < deadline {
if f() {
return;
}
std::thread::sleep(Duration::from_millis(20));
}
panic!("wait_until timeout: {tag}");
}
#[test]
fn u0_cmd_queue_roundtrip() {
let s = UiShared::new();
s.push_cmd(UiCmd::Start {
factory: "dm".to_string(),
});
s.push_cmd(UiCmd::Remove {
name: "dicy".to_string(),
});
assert_eq!(
s.drain_cmds(),
vec![
UiCmd::Start {
factory: "dm".to_string()
},
UiCmd::Remove {
name: "dicy".to_string()
},
]
);
assert!(s.drain_cmds().is_empty(), "drain 是消费性取走");
}
#[test]
fn u1_confirm_steps_and_cancel() {
let s = UiShared::new();
assert_eq!(s.confirm_step("a"), ConfirmStep::Armed);
assert_eq!(s.pending_confirm().as_deref(), Some("a"));
assert_eq!(s.confirm_step("a"), ConfirmStep::Fired);
assert_eq!(s.pending_confirm(), None);
assert_eq!(s.confirm_step("b"), ConfirmStep::Armed);
assert_eq!(s.confirm_step("c"), ConfirmStep::Switched);
assert_eq!(s.pending_confirm().as_deref(), Some("c"));
s.confirm_cancel();
assert_eq!(s.pending_confirm(), None);
assert_eq!(s.confirm_step("b"), ConfirmStep::Armed); }
#[test]
fn u2_quarantined_detection() {
assert!(is_quarantined("Quarantined"));
assert!(is_quarantined("State::Quarantined(panic x3)"));
assert!(!is_quarantined("Running"));
assert!(!is_quarantined("Ready"));
}
fn put_timer(engine: &MainEngine) {
engine.event_engine().put(Event::new(EVENT_TIMER)).unwrap();
}
#[test]
fn t0_remove_then_restart_loop() {
let engine = Arc::new(MainEngine::new(None));
let shared = Arc::new(UiShared::new());
boot(
&engine,
&ek_ui::ui_kinds(&shared, true),
&StationConfig {
plugins: vec!["dm".to_string(), "oms".to_string(), "ui".to_string()],
},
)
.unwrap();
engine.start().unwrap();
wait_until("rows fill", 4, || shared.snapshot().rows.len() >= 4);
assert!(shared.snapshot().rows.iter().any(|r| r.name == "dm"));
shared.push_cmd(UiCmd::Remove {
name: "dm".to_string(),
});
put_timer(&engine);
wait_until("dm removed", 4, || {
shared.snapshot().rows.iter().all(|r| r.name != "dm")
});
assert!(
shared
.logs_snapshot()
.0
.iter()
.any(|l| l.contains("[ops] svctl.remove dm 完成")),
"remove log missing"
);
shared.push_cmd(UiCmd::Start {
factory: "dm".to_string(),
});
put_timer(&engine);
wait_until("dm back", 4, || {
shared
.snapshot()
.rows
.iter()
.any(|r| r.name == "dm" && r.state == "Running")
});
assert!(
shared
.logs_snapshot()
.0
.iter()
.any(|l| l.contains("svctl.start dm → dm 上线")),
"start log missing"
);
engine.close_all(Duration::from_secs(2));
}
#[test]
fn t1_unknown_factory_rejected_logged() {
let engine = Arc::new(MainEngine::new(None));
let shared = Arc::new(UiShared::new());
boot(
&engine,
&ek_ui::ui_kinds(&shared, true),
&StationConfig {
plugins: vec!["dm".to_string(), "oms".to_string(), "ui".to_string()],
},
)
.unwrap();
engine.start().unwrap();
wait_until("rows fill", 4, || shared.snapshot().rows.len() >= 4);
shared.push_cmd(UiCmd::Start {
factory: "ghost".to_string(),
});
put_timer(&engine);
wait_until("rejected log", 4, || {
shared
.logs_snapshot()
.0
.iter()
.any(|l| l.contains("start ghost 被拒"))
});
engine.close_all(Duration::from_secs(2));
}