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::version_manager::VersionManager; use anyhow::Result; use std::fs; use std::path::Path; use std::sync::{Arc, Mutex}; fn test_config() -> AppConfig { parse_str( r#"{ "display": { "fullscreen": false, "window_title": "test", "rotation": 0, "flip_horizontal": false, "flip_vertical": false, "perspective_correction": { "enabled": false, "points": [] } }, "playlist": [ { "id": "video-1", "path": "video.mp4" } ], "transition": { "enabled": false, "type": "none", "duration": 0.0 }, "playback": { "loop_playlist": true, "auto_start": false }, "scenes": {}, "remote_control": { "enabled": false, "host": "127.0.0.1", "port": 8080 } }"#, "tests/core-config.json", ) .expect("test config should be valid") } fn lock_events(events: &Arc>>) -> std::sync::MutexGuard<'_, Vec> { events.lock().expect("events mutex poisoned") } fn has_event(events: &Arc>>, expected: &str) -> bool { lock_events(events).iter().any(|event| event == expected) } fn message_label(message: &Message) -> String { match message { Message::Custom { kind, payload } => format!("custom:{kind}:{payload}"), Message::PluginReady(id) => format!("plugin_ready:{id}"), Message::WifiResult(payload) => format!("wifi_result:{payload}"), Message::Shutdown => "shutdown".to_string(), Message::PlayerStatus(_) => "player_status".to_string(), Message::StateChanged { old_state, new_state, } => format!("state_changed:{old_state}->{new_state}"), Message::WifiProvisioned { ssid, ip } => format!("wifi_provisioned:{ssid}:{ip}"), Message::ConfigReloadRequest => "config_reload_request".to_string(), Message::ConfigReloaded(_) => "config_reloaded".to_string(), Message::PlayerCommand(_) => "player_command".to_string(), Message::Trigger { name, value } => format!("trigger:{name}:{value}"), Message::ScreenLockRequest(value) => format!("screen_lock:{value}"), Message::CursorVisibility(value) => format!("cursor_visibility:{value}"), Message::WifiCommand(_) => "wifi_command".to_string(), } } struct TestPlugin { id: String, deps: Vec, events: Arc>>, } impl TestPlugin { fn new(id: &str, deps: Vec<&str>, events: Arc>>) -> Self { Self { id: id.to_string(), deps: deps.into_iter().map(|s| s.to_string()).collect(), events, } } fn record(&self, entry: impl Into) { lock_events(&self.events).push(entry.into()); } } impl Plugin for TestPlugin { fn id(&self) -> &str { &self.id } fn info(&self) -> PluginInfo { PluginInfo { name: self.id.clone(), version: "test".to_string(), description: "test plugin".to_string(), platform: Platform::Any, } } fn dependencies(&self) -> Vec { self.deps.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<()> { self.record(format!("msg:{}:{}", self.id, message_label(&msg))); Ok(()) } fn stop(&mut self) -> Result<()> { self.record(format!("stop:{}", self.id)); Ok(()) } } #[test] fn service_manager_register_start_and_stop_flow() { let events = Arc::new(Mutex::new(Vec::new())); let mut manager = ServiceManager::new(test_config()); manager.register(Box::new(TestPlugin::new("alpha", vec![], events.clone()))); manager.register(Box::new(TestPlugin::new("beta", vec![], events.clone()))); manager.start_all().expect("start_all should succeed"); manager.stop_all().expect("stop_all should succeed"); assert_eq!( lock_events(&events).clone(), vec![ "init:alpha", "init:beta", "start:alpha", "start:beta", "stop:beta", "stop:alpha", ] ); } #[test] fn routes_plugin_broadcast_and_manager_messages() { let events = Arc::new(Mutex::new(Vec::new())); let mut manager = ServiceManager::new(test_config()); manager.register(Box::new(TestPlugin::new("alpha", vec![], events.clone()))); manager.register(Box::new(TestPlugin::new("beta", vec![], events.clone()))); manager.start_all().expect("start_all should succeed"); let sender = manager.sender(); sender .send(Envelope { from: "alpha".to_string(), to: Destination::Plugin("beta".to_string()), message: Message::Custom { kind: "direct".to_string(), payload: "hello".to_string(), }, }) .expect("direct message should send"); sender .send(Envelope { from: "alpha".to_string(), to: Destination::Broadcast, message: Message::Custom { kind: "broadcast".to_string(), payload: "everyone".to_string(), }, }) .expect("broadcast message should send"); sender .send(Envelope { from: "alpha".to_string(), to: Destination::Manager, message: Message::PluginReady("alpha".to_string()), }) .expect("manager 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"); assert!(!has_event(&events, "msg:alpha:custom:direct:hello")); assert!(has_event(&events, "msg:beta:custom:direct:hello")); assert!(has_event(&events, "msg:alpha:custom:broadcast:everyone")); assert!(has_event(&events, "msg:beta:custom:broadcast:everyone")); assert!(has_event(&events, "msg:alpha:plugin_ready:alpha")); assert!(has_event(&events, "msg:beta:plugin_ready:alpha")); } #[test] fn start_all_rejects_missing_dependencies() { let events = Arc::new(Mutex::new(Vec::new())); let mut manager = ServiceManager::new(test_config()); manager.register(Box::new(TestPlugin::new( "dependent", vec!["missing"], events, ))); let error = manager .start_all() .expect_err("missing dependency should fail"); assert!(error .to_string() .contains("plugin 'dependent' depends on missing plugin 'missing'")); } #[test] fn start_all_rejects_dependency_cycles() { let events = Arc::new(Mutex::new(Vec::new())); let mut manager = ServiceManager::new(test_config()); manager.register(Box::new(TestPlugin::new( "alpha", vec!["beta"], events.clone(), ))); manager.register(Box::new(TestPlugin::new("beta", vec!["alpha"], events))); let error = manager .start_all() .expect_err("dependency cycle should fail"); assert!(error .to_string() .contains("plugin dependency cycle detected among")); } #[test] fn start_all_sorts_plugins_topologically() { let events = Arc::new(Mutex::new(Vec::new())); let mut manager = ServiceManager::new(test_config()); manager.register(Box::new(TestPlugin::new( "gamma", vec!["beta"], events.clone(), ))); manager.register(Box::new(TestPlugin::new("alpha", vec![], events.clone()))); manager.register(Box::new(TestPlugin::new( "beta", vec!["alpha"], events.clone(), ))); manager .start_all() .expect("start_all should sort dependencies"); manager.stop_all().expect("stop_all should succeed"); assert_eq!( lock_events(&events).clone(), vec![ "init:alpha", "init:beta", "init:gamma", "start:alpha", "start:beta", "start:gamma", "stop:gamma", "stop:beta", "stop:alpha", ] ); } #[test] fn wifi_result_sent_to_manager_is_broadcast_to_plugins() { let events = Arc::new(Mutex::new(Vec::new())); let mut manager = ServiceManager::new(test_config()); manager.register(Box::new(TestPlugin::new("alpha", vec![], events.clone()))); manager.register(Box::new(TestPlugin::new("beta", vec![], events.clone()))); manager.start_all().expect("start_all should succeed"); let sender = manager.sender(); sender .send(Envelope { from: "wifi".to_string(), to: Destination::Manager, message: Message::WifiResult("connected".to_string()), }) .expect("wifi result 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"); assert!(has_event(&events, "msg:alpha:wifi_result:connected")); assert!(has_event(&events, "msg:beta:wifi_result:connected")); } #[test] fn http_plugin_must_depend_on_video() { use crate::plugins::http::HttpPlugin; let plugin = HttpPlugin::new(); let deps = plugin.dependencies(); assert_eq!(deps, vec!["video"], "http plugin must depend on video"); } #[test] fn ble_plugin_must_have_no_dependencies() { use crate::plugins::ble::BlePlugin; let plugin = BlePlugin::new(); let deps = plugin.dependencies(); assert!(deps.is_empty(), "ble plugin must have no dependencies"); } #[test] fn wifi_plugin_must_have_no_dependencies() { use crate::plugins::wifi::WifiPlugin; let plugin = WifiPlugin::new(); let deps = plugin.dependencies(); assert!(deps.is_empty(), "wifi plugin must have no dependencies"); } #[test] fn video_plugin_must_have_no_dependencies() { use crate::plugins::video::VideoPlugin; let plugin = VideoPlugin::new(); let deps = plugin.dependencies(); assert!(deps.is_empty(), "video plugin must have no dependencies"); } #[test] fn screen_plugin_must_have_no_dependencies() { use crate::plugins::screen::ScreenPlugin; let plugin = ScreenPlugin::new(); let deps = plugin.dependencies(); assert!(deps.is_empty(), "screen plugin must have no dependencies"); } #[test] fn all_plugin_ids_must_be_unique() { use crate::plugins::ble::BlePlugin; use crate::plugins::http::HttpPlugin; use crate::plugins::screen::ScreenPlugin; use crate::plugins::video::VideoPlugin; use crate::plugins::wifi::WifiPlugin; use std::collections::HashSet; let plugins: Vec> = vec![ Box::new(BlePlugin::new()), Box::new(HttpPlugin::new()), Box::new(ScreenPlugin::new()), Box::new(VideoPlugin::new()), Box::new(WifiPlugin::new()), ]; 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 ); } } #[test] fn topological_sort_places_http_after_video() { let events = Arc::new(Mutex::new(Vec::new())); let mut manager = ServiceManager::new(test_config()); manager.register(Box::new(TestPlugin::new( "http", vec!["video"], events.clone(), ))); manager.register(Box::new(TestPlugin::new("video", vec![], events.clone()))); manager .start_all() .expect("start_all should succeed with http depending on video"); let event_log = lock_events(&events).clone(); let http_init_pos = event_log .iter() .position(|e| e == "init:http") .expect("http should be initialized"); let video_init_pos = event_log .iter() .position(|e| e == "init:video") .expect("video should be initialized"); assert!( video_init_pos < http_init_pos, "video must be initialized before http (video at {}, http at {})", video_init_pos, http_init_pos ); } // ── 自测相关测试 ── /// 支持自测的 TestPlugin 变体 struct TestPluginWithSelfTest { id: String, events: Arc>>, caps: Vec, test_results: Vec, } impl TestPluginWithSelfTest { fn new( id: &str, events: Arc>>, caps: Vec, test_results: Vec, ) -> Self { Self { id: id.to_string(), events, caps, test_results, } } fn record(&self, entry: impl Into) { 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 { self.caps.clone() } fn self_test(&mut self) -> Vec { 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(()) } } struct FailingPlugin { id: String, events: Arc>>, } impl FailingPlugin { fn new(id: &str, events: Arc>>) -> Self { Self { id: id.to_string(), events, } } fn record(&self, entry: impl Into) { 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())); 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); } #[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), } } fn unique_test_dir(name: &str) -> std::path::PathBuf { let nanos = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("system time should be after unix epoch") .as_nanos(); std::env::temp_dir().join(format!("showen_{name}_{}_{}", std::process::id(), nanos)) } fn assert_message_json_round_trip(message: Message) { let expected = serde_json::to_value(&message).expect("message should serialize to value"); let decoded: Message = serde_json::from_value(expected.clone()).expect("message should deserialize from value"); let actual = serde_json::to_value(decoded).expect("decoded message should serialize"); assert_eq!(actual, expected); } // NOTE: 动态插件 null vtable 测试已移除 —— 在测试环境中编译动态 .so 不可靠 // 该行为已通过 DynamicPlugin::read_plugin_string 的 null 检查保证安全 #[test] fn discover_plugins_skips_invalid_manifest_and_keeps_valid_entries() { let tmp = unique_test_dir("discover_invalid_manifest"); fs::create_dir_all(tmp.join("valid-plugin").join("1.0.0")) .expect("valid plugin dir should be created"); fs::create_dir_all(tmp.join("broken-plugin").join("1.0.0")) .expect("broken plugin dir should be created"); fs::write( tmp.join("valid-plugin").join("1.0.0").join("manifest.json"), r#"{ "id": "valid-plugin", "version": "1.0.0", "sdk_version": "0.2.0", "so_filename": "libvalid_plugin.so" }"#, ) .expect("valid manifest should be written"); fs::write( tmp.join("broken-plugin") .join("1.0.0") .join("manifest.json"), r#"{"id": "broken-plugin", "version": }"#, ) .expect("invalid manifest should be written"); let loader = PluginLoader::new(&tmp); let manifests = loader .discover_plugins() .expect("invalid manifest should be skipped, not returned as error"); assert_eq!(manifests.len(), 1); assert_eq!(manifests[0].id, "valid-plugin"); assert_eq!(manifests[0].version, "1.0.0"); let _ = fs::remove_dir_all(&tmp); } #[test] fn handle_message_skips_disabled_plugins() { let events = Arc::new(Mutex::new(Vec::new())); let mut manager = ServiceManager::new(test_config()); manager.register(Box::new(TestPlugin::new("alpha", vec![], events.clone()))); manager.register(Box::new(TestPlugin::new("beta", vec![], events.clone()))); manager.start_all().expect("start_all should succeed"); manager .set_plugin_enabled("beta", false) .expect("beta should be disabled"); let sender = manager.sender(); sender .send(Envelope { from: "alpha".to_string(), to: Destination::Plugin("beta".to_string()), message: Message::Custom { kind: "direct".to_string(), payload: "ignored".to_string(), }, }) .expect("direct 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"); assert!(!has_event(&events, "msg:beta:custom:direct:ignored")); assert!(!manager.plugin_states()[1].enabled); } #[test] fn rollback_without_stable_version_returns_error_and_keeps_active_version() { let tmp = unique_test_dir("rollback_without_stable"); fs::create_dir_all(tmp.join("sensor").join("2.0.0")).expect("version dir should be created"); let loader = PluginLoader::new(&tmp); let mut registry = PluginRegistry::default(); registry.plugins.insert( "sensor".to_string(), PluginRegistryEntry { active_version: "2.0.0".to_string(), last_stable_version: None, enabled: true, error_policy: ErrorPolicy::AutoRollback, max_errors: 1, }, ); loader .save_registry(®istry) .expect("registry should be written"); let version_manager = VersionManager::new(loader); let error = version_manager .rollback("sensor") .expect_err("missing stable version should fail"); assert!(error .to_string() .contains("plugin 'sensor' has no stable version to rollback to")); let registry = version_manager .loader() .load_registry() .expect("registry should still load"); assert_eq!(registry.plugins["sensor"].active_version, "2.0.0"); assert!(registry.plugins["sensor"].last_stable_version.is_none()); let _ = fs::remove_dir_all(&tmp); } #[test] fn all_message_variants_round_trip_through_json() { let config = test_config(); let messages = vec![ Message::PlayerCommand(super::message::PlayerCommand::Play), Message::PlayerCommand(super::message::PlayerCommand::Pause), Message::PlayerCommand(super::message::PlayerCommand::Next), Message::PlayerCommand(super::message::PlayerCommand::Previous), Message::PlayerCommand(super::message::PlayerCommand::Goto(3)), Message::PlayerCommand(super::message::PlayerCommand::ChangeScene( "intro".to_string(), )), Message::PlayerStatus(super::message::PlayerStatusData { running: true, paused: false, in_transition: true, current_index: 2, playlist_length: 5, current_video: Some("video.mp4".to_string()), }), Message::Trigger { name: "motion".to_string(), value: "detected".to_string(), }, Message::StateChanged { old_state: "idle".to_string(), new_state: "playing".to_string(), }, Message::ScreenLockRequest(true), Message::CursorVisibility(false), Message::WifiCommand(super::message::WifiCommand::Scan), Message::WifiCommand(super::message::WifiCommand::Connect { ssid: "lab".to_string(), password: "secret".to_string(), }), Message::WifiCommand(super::message::WifiCommand::Status), Message::WifiCommand(super::message::WifiCommand::ApStart { ssid: "showen-ap".to_string(), password: "password".to_string(), }), Message::WifiCommand(super::message::WifiCommand::ApStop), Message::WifiResult("connected".to_string()), Message::WifiProvisioned { ssid: "lab".to_string(), ip: "192.168.1.10".to_string(), }, Message::ConfigReloaded(config), Message::ConfigReloadRequest, Message::Shutdown, Message::PluginReady("sensor".to_string()), Message::Custom { kind: "health".to_string(), payload: "ok".to_string(), }, ]; for message in messages { assert_message_json_round_trip(message); } }