feat: 插件自动挂载测试机制 — capabilities + self_test + 3阶段启动
- Plugin trait 增加 capabilities() 和 self_test() 方法 - PluginVTable 增加 get_capabilities 和 self_test FFI - ServiceManager 三阶段启动: init → self_test → start - SendCallback 改为 ctx 参数传递,消除 thread_local - export_plugin! 宏所有 FFI 函数包裹 catch_unwind - PluginManifest 增加 capabilities/required_capabilities/auto_test - 新增 3 个自测相关测试用例 (共 59 测试)
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
use super::config::{parse_str, AppConfig};
|
||||
use super::message::{Destination, Envelope, Message};
|
||||
use super::plugin::{Platform, Plugin, PluginContext, PluginInfo};
|
||||
use super::plugin::{CapabilityTestResult, Platform, Plugin, PluginContext, PluginInfo};
|
||||
use super::service_manager::ServiceManager;
|
||||
use super::plugin_loader::ErrorPolicy;
|
||||
use anyhow::Result;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -421,3 +422,194 @@ fn topological_sort_places_http_after_video() {
|
||||
http_init_pos
|
||||
);
|
||||
}
|
||||
|
||||
// ── 自测相关测试 ──
|
||||
|
||||
/// 支持自测的 TestPlugin 变体
|
||||
struct TestPluginWithSelfTest {
|
||||
id: String,
|
||||
events: Arc<Mutex<Vec<String>>>,
|
||||
caps: Vec<String>,
|
||||
test_results: Vec<CapabilityTestResult>,
|
||||
}
|
||||
|
||||
impl TestPluginWithSelfTest {
|
||||
fn new(
|
||||
id: &str,
|
||||
events: Arc<Mutex<Vec<String>>>,
|
||||
caps: Vec<String>,
|
||||
test_results: Vec<CapabilityTestResult>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.to_string(),
|
||||
events,
|
||||
caps,
|
||||
test_results,
|
||||
}
|
||||
}
|
||||
|
||||
fn record(&self, entry: impl Into<String>) {
|
||||
lock_events(&self.events).push(entry.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for TestPluginWithSelfTest {
|
||||
fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn info(&self) -> PluginInfo {
|
||||
PluginInfo {
|
||||
name: self.id.clone(),
|
||||
version: "test".to_string(),
|
||||
description: "test plugin with self_test".to_string(),
|
||||
platform: Platform::Any,
|
||||
}
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> Vec<String> {
|
||||
self.caps.clone()
|
||||
}
|
||||
|
||||
fn self_test(&mut self) -> Vec<CapabilityTestResult> {
|
||||
self.record(format!("self_test:{}", self.id));
|
||||
self.test_results.clone()
|
||||
}
|
||||
|
||||
fn init(&mut self, _ctx: PluginContext) -> Result<()> {
|
||||
self.record(format!("init:{}", self.id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start(&mut self) -> Result<()> {
|
||||
self.record(format!("start:{}", self.id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_message(&mut self, _msg: Message) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&mut self) -> Result<()> {
|
||||
self.record(format!("stop:{}", self.id));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_test_all_pass_allows_normal_start() {
|
||||
let events = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut manager = ServiceManager::new(test_config());
|
||||
|
||||
let plugin = TestPluginWithSelfTest::new(
|
||||
"sensor",
|
||||
events.clone(),
|
||||
vec!["temperature".into()],
|
||||
vec![CapabilityTestResult {
|
||||
capability: "temperature".into(),
|
||||
passed: true,
|
||||
message: "ok".into(),
|
||||
}],
|
||||
);
|
||||
|
||||
manager.register_dynamic_with_manifest(
|
||||
Box::new(plugin),
|
||||
ErrorPolicy::DisableAndLog,
|
||||
5,
|
||||
vec!["temperature".into()],
|
||||
vec!["temperature".into()],
|
||||
true,
|
||||
);
|
||||
|
||||
manager.start_all().expect("start_all should succeed when all tests pass");
|
||||
|
||||
let log = lock_events(&events);
|
||||
assert!(log.contains(&"init:sensor".to_string()));
|
||||
assert!(log.contains(&"self_test:sensor".to_string()));
|
||||
assert!(log.contains(&"start:sensor".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_test_required_capability_fails_disables_dynamic_plugin() {
|
||||
let events = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut manager = ServiceManager::new(test_config());
|
||||
|
||||
let plugin = TestPluginWithSelfTest::new(
|
||||
"sensor",
|
||||
events.clone(),
|
||||
vec!["temperature".into()],
|
||||
vec![CapabilityTestResult {
|
||||
capability: "temperature".into(),
|
||||
passed: false,
|
||||
message: "sensor not connected".into(),
|
||||
}],
|
||||
);
|
||||
|
||||
manager.register_dynamic_with_manifest(
|
||||
Box::new(plugin),
|
||||
ErrorPolicy::DisableAndLog,
|
||||
5,
|
||||
vec!["temperature".into()],
|
||||
vec!["temperature".into()],
|
||||
true,
|
||||
);
|
||||
|
||||
// Should succeed (dynamic plugin failure doesn't abort)
|
||||
manager.start_all().expect("start_all should succeed");
|
||||
|
||||
let log = lock_events(&events);
|
||||
assert!(log.contains(&"init:sensor".to_string()));
|
||||
assert!(log.contains(&"self_test:sensor".to_string()));
|
||||
// start should NOT have been called
|
||||
assert!(!log.contains(&"start:sensor".to_string()));
|
||||
|
||||
// Plugin should be disabled
|
||||
let states = manager.plugin_states();
|
||||
assert!(!states[0].enabled, "plugin should be disabled after required capability failure");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_test_optional_capability_fails_still_starts() {
|
||||
let events = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut manager = ServiceManager::new(test_config());
|
||||
|
||||
let plugin = TestPluginWithSelfTest::new(
|
||||
"sensor",
|
||||
events.clone(),
|
||||
vec!["temperature".into(), "humidity".into()],
|
||||
vec![
|
||||
CapabilityTestResult {
|
||||
capability: "temperature".into(),
|
||||
passed: true,
|
||||
message: "ok".into(),
|
||||
},
|
||||
CapabilityTestResult {
|
||||
capability: "humidity".into(),
|
||||
passed: false,
|
||||
message: "sensor not calibrated".into(),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// Only temperature is required; humidity is optional
|
||||
manager.register_dynamic_with_manifest(
|
||||
Box::new(plugin),
|
||||
ErrorPolicy::DisableAndLog,
|
||||
5,
|
||||
vec!["temperature".into()],
|
||||
vec!["temperature".into(), "humidity".into()],
|
||||
true,
|
||||
);
|
||||
|
||||
manager.start_all().expect("start_all should succeed");
|
||||
|
||||
let log = lock_events(&events);
|
||||
assert!(log.contains(&"self_test:sensor".to_string()));
|
||||
assert!(log.contains(&"start:sensor".to_string()), "plugin should start despite optional failure");
|
||||
|
||||
// Test results should be recorded
|
||||
let states = manager.plugin_states();
|
||||
assert_eq!(states[0].test_results.len(), 2);
|
||||
assert!(states[0].test_results[0].passed);
|
||||
assert!(!states[0].test_results[1].passed);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user