mod common;
use common::{load, push_record, snapshot, wait_until};
use ek_core::{
AskError, MainEngine, PlugNotice, PluginContext, PluginState, ServiceDesc, envelope::Envelope,
};
use ek_event::Event;
use std::sync::{Arc, Mutex};
use std::time::Duration;
#[derive(Debug)]
pub struct Ping {
pub n: u64,
}
#[derive(Debug, PartialEq)]
pub struct Pong {
pub n: u64,
}
const SVC: &str = "order.place";
const SVC_ALIAS: &str = "order.peg";
fn svc_desc() -> ServiceDesc {
ServiceDesc::describe::<Ping, Pong>(SVC, "Echo: Ping 反射为 Pong")
}
type CtxSlot = Arc<Mutex<Option<PluginContext>>>;
fn ctx_slot() -> CtxSlot {
Arc::new(Mutex::new(None))
}
fn hook_ctx(slot: CtxSlot) -> impl Fn(&PluginContext) + Send + Sync + 'static {
move |ctx: &PluginContext| {
*slot.lock().unwrap() = Some(ctx.clone());
}
}
#[test]
fn dv1_discovery_hit() {
let eng = MainEngine::new(None);
eng.add_plugin(
common::test_plugin("oms")
.init_hook(|ctx: &PluginContext| {
let refs = ctx.find_service(SVC);
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].name, "broker");
assert_eq!(refs[0].version, "1.0.0");
assert_eq!(refs[0].service.id, SVC);
assert_eq!(
refs[0].service.req_type,
std::any::type_name::<Ping>(),
"闸门底座:type_name 现场一致"
);
assert!(refs[0].service.doc.contains("Pong"));
let list = ctx.services_of("broker").expect("broker declared services");
assert_eq!(list.len(), 1);
assert_eq!(list[0].id, SVC);
assert!(ctx.find_service("absent.thing").is_empty());
match ctx.ask_service::<Ping, Pong>(
"absent.thing",
Ping { n: 1 },
Duration::from_millis(100),
) {
Err(AskError::UnknownService { service }) => {
assert_eq!(service, "absent.thing")
}
other => panic!("expected UnknownService, got {other:?}"),
}
})
.build(),
)
.unwrap();
eng.add_plugin(
common::test_plugin("broker")
.version("1.0.0")
.services(vec![svc_desc()])
.build(),
)
.unwrap();
eng.start().unwrap();
eng.close_all(Duration::from_secs(1));
}
#[test]
fn dv2_discovery_miss_then_degrade() {
let eng = MainEngine::new(None);
let degraded = common::counter();
let d2 = Arc::clone(°raded);
eng.add_plugin(
common::test_plugin("oms")
.init_hook(move |ctx: &PluginContext| {
if ctx.find_service(SVC).is_empty() {
common::bump(&d2);
}
})
.build(),
)
.unwrap();
eng.start().unwrap();
assert!(wait_until(Duration::from_secs(2), || load(°raded) == 1));
eng.close_all(Duration::from_secs(1));
}
#[test]
fn dv3_late_binding_via_plug_up() {
let eng = MainEngine::new(None);
let paired = common::records::<String>();
let p2 = Arc::clone(&paired);
eng.add_plugin(
common::test_plugin("a")
.routes_str(&["ePlugUp"])
.mail(move |e: &Event| {
if e.event_type == "ePlugUp" {
if let Some(n) = e.data.downcast_ref::<PlugNotice>() {
if n.name == "broker" {
push_record(&p2, format!("paired:{}/{}", n.name, n.version));
}
}
}
})
.init_hook(|ctx: &PluginContext| {
assert!(
ctx.find_service(SVC).is_empty(),
"no provider yet at a.on_init"
);
assert!(ctx.plugins().iter().any(|p| p.name == "a"));
})
.build(),
)
.unwrap();
eng.start().unwrap();
eng.add_plugin(
common::test_plugin("broker")
.version("2.0.0")
.services(vec![svc_desc()])
.build(),
)
.unwrap();
assert_eq!(eng.get_state("broker"), Some(PluginState::Running));
assert!(wait_until(Duration::from_secs(2), || snapshot(&paired)
== vec!["paired:broker/2.0.0".to_string()]));
eng.close_all(Duration::from_secs(1));
}
struct DynProvider {
catalog: Mutex<Vec<ServiceDesc>>,
ctx: Mutex<Option<PluginContext>>,
}
impl ek_core::Plugin for DynProvider {
fn name(&self) -> &str {
"broker"
}
fn version(&self) -> std::borrow::Cow<'static, str> {
"1.0.0".into()
}
fn routes(&self) -> Vec<std::borrow::Cow<'static, str>> {
vec!["eRepub".into()]
}
fn services(&self) -> Vec<ServiceDesc> {
self.catalog.lock().unwrap().clone()
}
fn on_init(&self, ctx: &PluginContext) {
*self.ctx.lock().unwrap() = Some(ctx.clone());
}
fn on_mail(&self, event: &Event) {
if event.event_type != "eRepub" {
return;
}
{
let mut cat = self.catalog.lock().unwrap();
cat.clear();
cat.push(ServiceDesc::describe::<Ping, Pong>(
SVC_ALIAS,
"改版后只提供 peg",
));
}
let Some(ctx) = self.ctx.lock().unwrap().as_ref().cloned() else {
return;
};
ctx.publish_services(self.services());
}
}
#[test]
fn dv4_catalog_revision() {
let eng = MainEngine::new(None);
let seen = common::records::<String>();
let s2 = Arc::clone(&seen);
eng.add_plugin(Arc::new(DynProvider {
catalog: Mutex::new(vec![svc_desc()]),
ctx: Mutex::new(None),
}))
.unwrap();
let a_ctx = ctx_slot();
let actx = Arc::clone(&a_ctx);
eng.add_plugin(
common::test_plugin("a")
.routes_str(&["eSrvChg"])
.init_hook(hook_ctx(a_ctx))
.mail(move |e: &Event| {
if e.event_type == "eSrvChg" {
if let Some(n) = e.data.downcast_ref::<PlugNotice>() {
if n.name != "broker" {
return;
}
let ctx = actx.lock().unwrap().as_ref().expect("a ctx").clone();
let place = ctx.find_service(SVC).len();
let peg = ctx.find_service(SVC_ALIAS).len();
let ask = ctx.ask_service::<Ping, Pong>(
SVC,
Ping { n: 1 },
Duration::from_millis(300),
);
push_record(
&s2,
format!("rev:ver={}/place={place}/peg={peg}", n.version),
);
match ask {
Err(AskError::UnknownService { service }) => {
assert_eq!(service, SVC);
push_record(&s2, "old-id→UnknownService".to_string());
}
other => {
panic!("expected UnknownService after revision, got {other:?}")
}
}
}
}
})
.build(),
)
.unwrap();
eng.start().unwrap();
eng.event_engine()
.put(Event::with_data("eRepub", Arc::new(0u8)))
.unwrap();
assert!(
wait_until(Duration::from_secs(2), || snapshot(&seen).len() == 2),
"seen={:?}",
snapshot(&seen)
);
let got = snapshot(&seen);
assert!(
got[0].contains("rev:ver=1.0.0/place=0/peg=1"),
"got {got:?}"
);
assert_eq!(got[1], "old-id→UnknownService");
eng.close_all(Duration::from_secs(1));
}
#[test]
fn dv5_failover_version_migration() {
let eng = MainEngine::new(None);
let mig = common::records::<String>();
let a_ctx = ctx_slot();
let paired = Arc::new(Mutex::new(String::new()));
let p_init = Arc::clone(&paired);
let p_mail = Arc::clone(&paired);
let m_init = Arc::clone(&mig);
let m_mail = Arc::clone(&mig);
let a_init = Arc::clone(&a_ctx);
let mail_actx = Arc::clone(&a_ctx);
eng.add_plugin(
common::test_plugin("a")
.routes_str(&["ePlugDown"])
.init_hook(move |ctx: &PluginContext| {
if let Some(first) = ctx.find_service(SVC).first() {
*p_init.lock().unwrap() = first.name.clone();
push_record(&m_init, format!("pair:{}/{}", first.name, first.version));
}
*a_init.lock().unwrap() = Some(ctx.clone());
})
.mail(move |e: &Event| {
if e.event_type == "ePlugDown" {
let Some(n) = e.data.downcast_ref::<PlugNotice>() else {
return;
};
let Some(ctx) = mail_actx.lock().unwrap().as_ref().cloned() else {
return;
};
let mut cur = p_mail.lock().unwrap();
if *cur != n.name {
return; }
if let Some(next) = ctx
.find_service(SVC)
.into_iter()
.find(|p| p.state == PluginState::Running)
{
push_record(&m_mail, format!("move:{}/{}", next.name, next.version));
*cur = next.name.clone();
} else {
push_record(&m_mail, "move:none".to_string());
cur.clear();
}
}
})
.build(),
)
.unwrap();
eng.add_plugin(
common::test_plugin("broker_old")
.version("1.0.0")
.services(vec![svc_desc()])
.build(),
)
.unwrap();
eng.add_plugin(
common::test_plugin("broker_new")
.version("1.1.0")
.services(vec![svc_desc()])
.build(),
)
.unwrap();
eng.start().unwrap();
assert!(wait_until(Duration::from_secs(2), || snapshot(&mig)
== vec!["pair:broker_old/1.0.0".to_string()]));
eng.remove_plugin("broker_old").unwrap();
assert!(wait_until(Duration::from_secs(2), || snapshot(&mig)
== vec![
"pair:broker_old/1.0.0".to_string(),
"move:broker_new/1.1.0".to_string()
]));
let eng_ctx = eng.get_typed::<common::TestPlugin>("a");
assert!(eng_ctx.is_some());
eng.close_all(Duration::from_secs(1));
}
#[test]
fn dv6_ask_service_gates() {
let eng = MainEngine::new(None);
let seen = common::records::<String>();
let s2 = Arc::clone(&seen);
let a_ctx = ctx_slot();
let actx = Arc::clone(&a_ctx);
let kick_no = common::counter();
let k2 = Arc::clone(&kick_no);
eng.add_plugin(
common::test_plugin("broker")
.routes_str(&["eReq.broker"])
.services(vec![ServiceDesc::describe::<Ping, Pong>(
SVC_ALIAS,
"peg 提供",
)])
.mail(|e: &Event| {
Envelope::handle::<Ping, Pong>(e, |req| Pong { n: req.n });
})
.build(),
)
.unwrap();
eng.add_plugin(
common::test_plugin("a")
.routes_str(&["eKick"])
.init_hook(hook_ctx(a_ctx))
.mail(move |e: &Event| {
if e.event_type != "eKick" {
return;
}
let n = load(&k2);
if n == 0 {
let ctx = actx.lock().unwrap().as_ref().expect("a ctx").clone();
let r = ctx.ask_service::<Ping, Pong>(
"absent.svc",
Ping { n: 1 },
Duration::from_millis(200),
);
match r {
Err(AskError::UnknownService { service }) => {
assert_eq!(service, "absent.svc");
push_record(&s2, "g1:UnknownService".to_string());
}
other => panic!("expected UnknownService, got {other:?}"),
}
} else if n == 1 {
let ctx = actx.lock().unwrap().as_ref().expect("a ctx").clone();
let r = ctx.ask_service::<String, Pong>(
SVC_ALIAS,
"wrong-type-payload".to_string(),
Duration::from_millis(200),
);
match r {
Err(AskError::InvalidPayload { target }) => {
assert_eq!(target, "broker");
push_record(&s2, "g2:InvalidPayload".to_string());
}
other => panic!("expected InvalidPayload, got {other:?}"),
}
} else {
let ctx = actx.lock().unwrap().as_ref().expect("a ctx").clone();
let r = ctx.ask_service::<Ping, Pong>(
SVC_ALIAS,
Ping { n: 7 },
Duration::from_millis(500),
);
match r {
Ok(p) => {
assert_eq!(p, Pong { n: 7 });
push_record(&s2, format!("g3:ok={}", p.n));
}
other => panic!("expected Ok(Pong), got {other:?}"),
}
}
common::bump(&k2);
})
.build(),
)
.unwrap();
eng.start().unwrap();
for _ in 0..3u64 {
eng.event_engine()
.put(Event::with_data("eKick", Arc::new(0u8)))
.unwrap();
}
assert!(wait_until(Duration::from_secs(3), || snapshot(&seen).len() == 3));
let got = snapshot(&seen);
assert!(got.iter().any(|s| s == "g1:UnknownService"), "{got:?}");
assert!(got.iter().any(|s| s == "g2:InvalidPayload"), "{got:?}");
assert!(got.iter().any(|s| s == "g3:ok=7"), "{got:?}");
eng.close_all(Duration::from_secs(1));
}
#[test]
fn dv7_dynamic_add_and_close_coverage() {
let eng = MainEngine::new(None);
let seq = common::records::<String>();
let s_start = Arc::clone(&seq);
let s_stop = Arc::clone(&seq);
eng.add_plugin(
common::test_plugin("first")
.start_hook(move || push_record(&s_start, "start:first".to_string()))
.stop_hook(move || push_record(&s_stop, "stop:first".to_string()))
.build(),
)
.unwrap();
eng.start().unwrap();
let l_start = Arc::clone(&seq);
let l_stop = Arc::clone(&seq);
eng.add_plugin(
common::test_plugin("late")
.services(vec![ServiceDesc::describe::<Ping, Pong>(SVC, "动态提供方")])
.start_hook(move || push_record(&l_start, "start:late".to_string()))
.stop_hook(move || push_record(&l_stop, "stop:late".to_string()))
.build(),
)
.unwrap();
assert_eq!(eng.get_state("late"), Some(PluginState::Running));
assert!(snapshot(&seq).contains(&"start:late".to_string()));
eng.close(Duration::from_secs(2));
let got = snapshot(&seq);
let pos_first_stop = got.iter().position(|s| s == "stop:first").unwrap();
let pos_late_stop = got.iter().position(|s| s == "stop:late").unwrap();
assert!(pos_late_stop < pos_first_stop, "late closes first: {got:?}");
let eng2 = MainEngine::new(None);
eng2.start().unwrap();
eng2.close_all(Duration::from_secs(1));
match eng2.add_plugin(common::test_plugin("x3").build()) {
Err(ek_core::PluginError::Stopped) => {}
other => panic!("expected Stopped, got {other:?}"),
}
}