fix: 修复3个P0遗留 — AutoRollback回退/ConfigReloaded序列化/FfiString跨allocator
This commit is contained in:
@@ -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(®istry).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),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user