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:
showen
2026-03-13 04:31:39 +08:00
parent 1863efb0f5
commit 99ee78984c
9 changed files with 694 additions and 123 deletions

View File

@@ -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);
}