feat: core tests, bug fixes, API docs rewrite, HTTP compat routes
- Fix state_machine reset_state_progress: reset sequence index before validation to prevent out-of-bounds error on state transitions - Fix video transformer test: use ±1 tolerance for OpenCV interpolation - Add core integration tests (service_manager, dependencies, messages) - Add HTTP compat routes (/index.html, POST /api/wifi/scan, hotspot aliases) - Rewrite clients/docs/API.md to match actual implementation - Fix BLE unused imports warning - CEO task planning for next round (ConfigReload, playlist snapshot) cargo check: 0 warnings, cargo test: 22/22 passed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
321
src/core/tests.rs
Normal file
321
src/core/tests.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
use super::config::{parse_str, AppConfig};
|
||||
use super::message::{Destination, Envelope, Message};
|
||||
use super::plugin::{Platform, Plugin, PluginContext, PluginInfo};
|
||||
use super::service_manager::ServiceManager;
|
||||
use anyhow::Result;
|
||||
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: &'static str,
|
||||
deps: Vec<&'static str>,
|
||||
events: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl TestPlugin {
|
||||
fn new(id: &'static str, deps: Vec<&'static str>, events: Arc<Mutex<Vec<String>>>) -> Self {
|
||||
Self { id, deps, events }
|
||||
}
|
||||
|
||||
fn record(&self, entry: impl Into<String>) {
|
||||
lock_events(&self.events).push(entry.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for TestPlugin {
|
||||
fn id(&self) -> &'static str {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn info(&self) -> PluginInfo {
|
||||
PluginInfo {
|
||||
name: self.id,
|
||||
version: "test",
|
||||
description: "test plugin",
|
||||
platform: Platform::Any,
|
||||
}
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<&'static str> {
|
||||
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: Destination::Plugin("beta"),
|
||||
message: Message::Custom {
|
||||
kind: "direct".to_string(),
|
||||
payload: "hello".to_string(),
|
||||
},
|
||||
})
|
||||
.expect("direct message should send");
|
||||
sender
|
||||
.send(Envelope {
|
||||
from: "alpha",
|
||||
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: Destination::Manager,
|
||||
message: Message::PluginReady("alpha"),
|
||||
})
|
||||
.expect("manager message should send");
|
||||
sender
|
||||
.send(Envelope {
|
||||
from: "test",
|
||||
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: Destination::Manager,
|
||||
message: Message::WifiResult("connected".to_string()),
|
||||
})
|
||||
.expect("wifi result should send");
|
||||
sender
|
||||
.send(Envelope {
|
||||
from: "test",
|
||||
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"));
|
||||
}
|
||||
Reference in New Issue
Block a user