Files
ShowenV2/src/core/tests.rs

979 lines
30 KiB
Rust

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<Mutex<Vec<String>>>) -> std::sync::MutexGuard<'_, Vec<String>> {
events.lock().expect("events mutex poisoned")
}
fn has_event(events: &Arc<Mutex<Vec<String>>>, 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<String>,
events: Arc<Mutex<Vec<String>>>,
}
impl TestPlugin {
fn new(id: &str, deps: Vec<&str>, events: Arc<Mutex<Vec<String>>>) -> Self {
Self {
id: id.to_string(),
deps: deps.into_iter().map(|s| s.to_string()).collect(),
events,
}
}
fn record(&self, entry: impl Into<String>) {
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<String> {
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<Box<dyn Plugin>> = 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<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(())
}
}
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()));
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(&registry)
.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);
}
}