fix: 修复3个P0遗留 — AutoRollback回退/ConfigReloaded序列化/FfiString跨allocator

This commit is contained in:
showen
2026-03-13 05:15:04 +08:00
parent 1264b94e36
commit 6067c3f0a2
10 changed files with 393 additions and 57 deletions

View File

@@ -1,9 +1,12 @@
use super::config::{parse_str, AppConfig};
use super::message::{Destination, Envelope, Message};
use super::plugin::{CapabilityTestResult, Platform, Plugin, PluginContext, PluginInfo};
use super::plugin_loader::{ErrorPolicy, PluginLoader, PluginRegistry, PluginRegistryEntry};
use super::service_manager::ServiceManager;
use super::plugin_loader::ErrorPolicy;
use super::version_manager::VersionManager;
use anyhow::Result;
use std::fs;
use std::path::Path;
use std::sync::{Arc, Mutex};
fn test_config() -> AppConfig {
@@ -385,7 +388,11 @@ fn all_plugin_ids_must_be_unique() {
let mut ids = HashSet::new();
for plugin in plugins {
let id = plugin.id().to_string();
assert!(ids.insert(id.clone()), "duplicate plugin id detected: '{}'", id);
assert!(
ids.insert(id.clone()),
"duplicate plugin id detected: '{}'",
id
);
}
}
@@ -496,6 +503,81 @@ impl Plugin for TestPluginWithSelfTest {
}
}
struct FailingPlugin {
id: String,
events: Arc<Mutex<Vec<String>>>,
}
impl FailingPlugin {
fn new(id: &str, events: Arc<Mutex<Vec<String>>>) -> Self {
Self {
id: id.to_string(),
events,
}
}
fn record(&self, entry: impl Into<String>) {
lock_events(&self.events).push(entry.into());
}
}
impl Plugin for FailingPlugin {
fn id(&self) -> &str {
&self.id
}
fn info(&self) -> PluginInfo {
PluginInfo {
name: self.id.clone(),
version: "test".to_string(),
description: "failing test plugin".to_string(),
platform: Platform::Any,
}
}
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<()> {
self.record(format!("error:{}", self.id));
Err(anyhow::anyhow!("simulated failure"))
}
fn stop(&mut self) -> Result<()> {
self.record(format!("stop:{}", self.id));
Ok(())
}
}
fn setup_rollback_store(base: &Path, plugin_id: &str) -> VersionManager {
let _ = fs::remove_dir_all(base);
fs::create_dir_all(base.join(plugin_id).join("1.0.0")).unwrap();
fs::create_dir_all(base.join(plugin_id).join("2.0.0")).unwrap();
let loader = PluginLoader::new(base);
let mut registry = PluginRegistry::default();
registry.plugins.insert(
plugin_id.to_string(),
PluginRegistryEntry {
active_version: "2.0.0".to_string(),
last_stable_version: Some("1.0.0".to_string()),
enabled: true,
error_policy: ErrorPolicy::AutoRollback,
max_errors: 1,
},
);
loader.save_registry(&registry).unwrap();
VersionManager::new(loader)
}
#[test]
fn self_test_all_pass_allows_normal_start() {
let events = Arc::new(Mutex::new(Vec::new()));
@@ -521,7 +603,9 @@ fn self_test_all_pass_allows_normal_start() {
true,
);
manager.start_all().expect("start_all should succeed when all tests pass");
manager
.start_all()
.expect("start_all should succeed when all tests pass");
let log = lock_events(&events);
assert!(log.contains(&"init:sensor".to_string()));
@@ -565,7 +649,10 @@ fn self_test_required_capability_fails_disables_dynamic_plugin() {
// Plugin should be disabled
let states = manager.plugin_states();
assert!(!states[0].enabled, "plugin should be disabled after required capability failure");
assert!(
!states[0].enabled,
"plugin should be disabled after required capability failure"
);
}
#[test]
@@ -605,7 +692,10 @@ fn self_test_optional_capability_fails_still_starts() {
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");
assert!(
log.contains(&"start:sensor".to_string()),
"plugin should start despite optional failure"
);
// Test results should be recorded
let states = manager.plugin_states();
@@ -613,3 +703,82 @@ fn self_test_optional_capability_fails_still_starts() {
assert!(states[0].test_results[0].passed);
assert!(!states[0].test_results[1].passed);
}
#[test]
fn auto_rollback_updates_registry_and_marks_pending_when_reload_fails() {
let tmp = std::env::temp_dir().join("showen_test_service_manager_autorollback");
let events = Arc::new(Mutex::new(Vec::new()));
let mut manager = ServiceManager::new(test_config());
manager.set_version_manager(setup_rollback_store(&tmp, "sensor"));
manager.register_dynamic(
Box::new(FailingPlugin::new("sensor", events.clone())),
ErrorPolicy::AutoRollback,
1,
);
manager.start_all().expect("start_all should succeed");
let sender = manager.sender();
sender
.send(Envelope {
from: "test".to_string(),
to: Destination::Plugin("sensor".to_string()),
message: Message::Custom {
kind: "tick".to_string(),
payload: "1".to_string(),
},
})
.expect("failing message should send");
sender
.send(Envelope {
from: "test".to_string(),
to: Destination::Manager,
message: Message::Shutdown,
})
.expect("shutdown should send");
manager.run().expect("run should succeed");
let registry = PluginLoader::new(&tmp).load_registry().unwrap();
assert_eq!(registry.plugins["sensor"].active_version, "1.0.0");
let states = manager.plugin_states();
assert!(
!states[0].enabled,
"plugin should be disabled after rollback failure"
);
assert!(
states[0].needs_rollback,
"plugin should be marked for restart-time reload"
);
assert!(has_event(&events, "stop:sensor"));
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn message_config_reload_request_round_trips_through_json() {
let json = serde_json::to_string(&Message::ConfigReloadRequest)
.expect("ConfigReloadRequest should serialize");
let message: Message =
serde_json::from_str(&json).expect("ConfigReloadRequest should deserialize");
assert!(matches!(message, Message::ConfigReloadRequest));
}
#[test]
fn message_config_reloaded_round_trips_through_json() {
let config = test_config();
let json = serde_json::to_string(&Message::ConfigReloaded(config.clone()))
.expect("ConfigReloaded should serialize");
let message: Message = serde_json::from_str(&json).expect("ConfigReloaded should deserialize");
match message {
Message::ConfigReloaded(decoded) => {
assert_eq!(decoded.display.window_title, config.display.window_title);
assert_eq!(decoded.playlist.len(), config.playlist.len());
assert_eq!(decoded.remote_control.port, config.remote_control.port);
}
other => panic!("unexpected message after round trip: {:?}", other),
}
}