use std::path::Path;
use std::path::PathBuf;
use mockforge_plugin_core::{
PluginAuthor, PluginId, PluginInfo, PluginInstance, PluginManifest, PluginVersion,
};
pub mod git;
pub mod installer;
pub mod invocation_metrics;
pub mod loader;
pub mod memory_tracking;
pub mod metadata;
pub mod registry;
pub mod remote;
pub mod runtime_adapter;
pub mod sandbox;
pub mod signature;
pub mod signature_gen;
pub mod validator;
pub use git::*;
pub use installer::*;
pub use invocation_metrics::{
InvocationMetric, InvocationMetricsBus, InvocationStatus, InvocationTimer,
};
pub use loader::*;
pub use memory_tracking::{MemoryStats, MemoryTracker};
pub use metadata::*;
pub use registry::*;
pub use remote::*;
pub use runtime_adapter::*;
pub use sandbox::*;
pub use signature::*;
pub use signature_gen::*;
pub use validator::*;
pub type LoaderResult<T> = Result<T, PluginLoaderError>;
#[derive(Debug, thiserror::Error)]
pub enum PluginLoaderError {
#[error("Plugin loading error: {message}")]
LoadError {
message: String,
},
#[error("Plugin validation error: {message}")]
ValidationError {
message: String,
},
#[error("Security violation: {violation}")]
SecurityViolation {
violation: String,
},
#[error("Plugin manifest error: {message}")]
ManifestError {
message: String,
},
#[error("WebAssembly module error: {message}")]
WasmError {
message: String,
},
#[error("File system error: {message}")]
FsError {
message: String,
},
#[error("Plugin already loaded: {plugin_id}")]
AlreadyLoaded {
plugin_id: PluginId,
},
#[error("Plugin not found: {plugin_id}")]
NotFound {
plugin_id: PluginId,
},
#[error("Plugin dependency error: {message}")]
DependencyError {
message: String,
},
#[error("Resource limit exceeded: {message}")]
ResourceLimit {
message: String,
},
#[error("Plugin execution error: {message}")]
ExecutionError {
message: String,
},
}
impl PluginLoaderError {
pub fn load<S: Into<String>>(message: S) -> Self {
Self::LoadError {
message: message.into(),
}
}
pub fn validation<S: Into<String>>(message: S) -> Self {
Self::ValidationError {
message: message.into(),
}
}
pub fn security<S: Into<String>>(violation: S) -> Self {
Self::SecurityViolation {
violation: violation.into(),
}
}
pub fn manifest<S: Into<String>>(message: S) -> Self {
Self::ManifestError {
message: message.into(),
}
}
pub fn wasm<S: Into<String>>(message: S) -> Self {
Self::WasmError {
message: message.into(),
}
}
pub fn fs<S: Into<String>>(message: S) -> Self {
Self::FsError {
message: message.into(),
}
}
pub fn already_loaded(plugin_id: PluginId) -> Self {
Self::AlreadyLoaded { plugin_id }
}
pub fn not_found(plugin_id: PluginId) -> Self {
Self::NotFound { plugin_id }
}
pub fn dependency<S: Into<String>>(message: S) -> Self {
Self::DependencyError {
message: message.into(),
}
}
pub fn resource_limit<S: Into<String>>(message: S) -> Self {
Self::ResourceLimit {
message: message.into(),
}
}
pub fn execution<S: Into<String>>(message: S) -> Self {
Self::ExecutionError {
message: message.into(),
}
}
pub fn is_security_error(&self) -> bool {
matches!(self, PluginLoaderError::SecurityViolation { .. })
}
}
#[derive(Debug, Clone)]
pub struct PluginLoaderConfig {
pub plugin_dirs: Vec<String>,
pub allow_unsigned: bool,
pub trusted_keys: Vec<String>,
pub key_data: std::collections::HashMap<String, Vec<u8>>,
pub max_plugins: usize,
pub load_timeout_secs: u64,
pub debug_logging: bool,
pub skip_wasm_validation: bool,
}
impl Default for PluginLoaderConfig {
fn default() -> Self {
Self {
plugin_dirs: vec!["~/.mockforge/plugins".to_string(), "./plugins".to_string()],
allow_unsigned: false,
trusted_keys: vec!["trusted-dev-key".to_string()],
key_data: std::collections::HashMap::new(),
max_plugins: 100,
load_timeout_secs: 30,
debug_logging: false,
skip_wasm_validation: false,
}
}
}
#[derive(Debug, Clone)]
pub struct PluginLoadContext {
pub plugin_id: PluginId,
pub manifest: PluginManifest,
pub plugin_path: String,
pub load_time: chrono::DateTime<chrono::Utc>,
pub config: PluginLoaderConfig,
}
impl PluginLoadContext {
pub fn new(
plugin_id: PluginId,
manifest: PluginManifest,
plugin_path: String,
config: PluginLoaderConfig,
) -> Self {
Self {
plugin_id,
manifest,
plugin_path,
load_time: chrono::Utc::now(),
config,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PluginLoadStats {
pub discovered: usize,
pub loaded: usize,
pub failed: usize,
pub skipped: usize,
pub start_time: Option<chrono::DateTime<chrono::Utc>>,
pub end_time: Option<chrono::DateTime<chrono::Utc>>,
}
impl PluginLoadStats {
pub fn start_loading(&mut self) {
self.start_time = Some(chrono::Utc::now());
}
pub fn finish_loading(&mut self) {
self.end_time = Some(chrono::Utc::now());
}
pub fn record_success(&mut self) {
self.loaded += 1;
self.discovered += 1;
}
pub fn record_failure(&mut self) {
self.failed += 1;
self.discovered += 1;
}
pub fn record_skipped(&mut self) {
self.skipped += 1;
self.discovered += 1;
}
pub fn duration(&self) -> Option<chrono::Duration> {
match (self.start_time, self.end_time) {
(Some(start), Some(end)) => Some(end - start),
_ => None,
}
}
pub fn success_rate(&self) -> f64 {
if self.discovered == 0 {
1.0 } else {
(self.loaded as f64 / self.discovered as f64) * 100.0
}
}
pub fn total_plugins(&self) -> usize {
self.loaded + self.failed + self.skipped
}
}
#[derive(Debug, Clone)]
pub struct PluginDiscovery {
pub plugin_id: PluginId,
pub manifest: PluginManifest,
pub path: String,
pub is_valid: bool,
pub errors: Vec<String>,
}
impl PluginDiscovery {
pub fn success(plugin_id: PluginId, manifest: PluginManifest, path: String) -> Self {
Self {
plugin_id,
manifest,
path,
is_valid: true,
errors: Vec::new(),
}
}
pub fn failure(plugin_id: PluginId, path: String, errors: Vec<String>) -> Self {
let plugin_id_clone = PluginId(plugin_id.0.clone());
Self {
plugin_id,
manifest: PluginManifest::new(PluginInfo::new(
plugin_id_clone,
PluginVersion::new(0, 0, 0),
"Unknown",
"Plugin failed to load",
PluginAuthor::new("unknown"),
)),
path,
is_valid: false,
errors,
}
}
pub fn is_success(&self) -> bool {
self.is_valid
}
pub fn first_error(&self) -> Option<&str> {
self.errors.first().map(|s| s.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_loader_error_types() {
let load_error = PluginLoaderError::LoadError {
message: "test error".to_string(),
};
assert!(matches!(load_error, PluginLoaderError::LoadError { .. }));
let validation_error = PluginLoaderError::ValidationError {
message: "validation failed".to_string(),
};
assert!(matches!(validation_error, PluginLoaderError::ValidationError { .. }));
}
#[test]
fn test_error_helper_constructors() {
let load_err = PluginLoaderError::load("load failed");
assert!(matches!(load_err, PluginLoaderError::LoadError { .. }));
let validation_err = PluginLoaderError::validation("validation failed");
assert!(matches!(validation_err, PluginLoaderError::ValidationError { .. }));
let security_err = PluginLoaderError::security("security violation");
assert!(matches!(security_err, PluginLoaderError::SecurityViolation { .. }));
let manifest_err = PluginLoaderError::manifest("manifest error");
assert!(matches!(manifest_err, PluginLoaderError::ManifestError { .. }));
let wasm_err = PluginLoaderError::wasm("wasm error");
assert!(matches!(wasm_err, PluginLoaderError::WasmError { .. }));
let fs_err = PluginLoaderError::fs("fs error");
assert!(matches!(fs_err, PluginLoaderError::FsError { .. }));
let dep_err = PluginLoaderError::dependency("dependency error");
assert!(matches!(dep_err, PluginLoaderError::DependencyError { .. }));
let resource_err = PluginLoaderError::resource_limit("resource limit");
assert!(matches!(resource_err, PluginLoaderError::ResourceLimit { .. }));
let exec_err = PluginLoaderError::execution("execution error");
assert!(matches!(exec_err, PluginLoaderError::ExecutionError { .. }));
}
#[test]
fn test_error_already_loaded() {
let plugin_id = PluginId::new("test-plugin");
let err = PluginLoaderError::already_loaded(plugin_id.clone());
assert!(matches!(err, PluginLoaderError::AlreadyLoaded { .. }));
assert_eq!(err.to_string(), format!("Plugin already loaded: {}", plugin_id));
}
#[test]
fn test_error_not_found() {
let plugin_id = PluginId::new("missing-plugin");
let err = PluginLoaderError::not_found(plugin_id.clone());
assert!(matches!(err, PluginLoaderError::NotFound { .. }));
assert_eq!(err.to_string(), format!("Plugin not found: {}", plugin_id));
}
#[test]
fn test_is_security_error() {
let security_err = PluginLoaderError::security("test");
assert!(security_err.is_security_error());
let load_err = PluginLoaderError::load("test");
assert!(!load_err.is_security_error());
}
#[test]
fn test_error_display() {
let err = PluginLoaderError::load("test message");
let err_str = err.to_string();
assert!(err_str.contains("Plugin loading error"));
assert!(err_str.contains("test message"));
}
#[test]
fn test_plugin_loader_config_default() {
let config = PluginLoaderConfig::default();
assert_eq!(config.plugin_dirs.len(), 2);
assert!(!config.allow_unsigned);
assert_eq!(config.max_plugins, 100);
assert_eq!(config.load_timeout_secs, 30);
assert!(!config.debug_logging);
assert!(!config.skip_wasm_validation);
}
#[test]
fn test_plugin_loader_config_clone() {
let config = PluginLoaderConfig::default();
let cloned = config.clone();
assert_eq!(config.max_plugins, cloned.max_plugins);
assert_eq!(config.load_timeout_secs, cloned.load_timeout_secs);
}
#[test]
fn test_plugin_load_context_creation() {
let plugin_id = PluginId::new("test-plugin");
let manifest = PluginManifest::new(PluginInfo::new(
plugin_id.clone(),
PluginVersion::new(1, 0, 0),
"Test Plugin",
"A test plugin",
PluginAuthor::new("test-author"),
));
let config = PluginLoaderConfig::default();
let context = PluginLoadContext::new(
plugin_id.clone(),
manifest.clone(),
"/tmp/plugin".to_string(),
config.clone(),
);
assert_eq!(context.plugin_id, plugin_id);
assert_eq!(context.plugin_path, "/tmp/plugin");
assert_eq!(context.config.max_plugins, config.max_plugins);
}
#[test]
fn test_plugin_load_stats_default() {
let stats = PluginLoadStats::default();
assert_eq!(stats.discovered, 0);
assert_eq!(stats.loaded, 0);
assert_eq!(stats.failed, 0);
assert_eq!(stats.skipped, 0);
assert!(stats.start_time.is_none());
assert!(stats.end_time.is_none());
}
#[test]
fn test_plugin_load_stats_timing() {
let mut stats = PluginLoadStats::default();
assert!(stats.duration().is_none());
stats.start_loading();
assert!(stats.start_time.is_some());
assert!(stats.duration().is_none());
std::thread::sleep(std::time::Duration::from_millis(10));
stats.finish_loading();
assert!(stats.end_time.is_some());
assert!(stats.duration().is_some());
let duration = stats.duration().unwrap();
assert!(duration.num_milliseconds() >= 10);
}
#[test]
fn test_plugin_load_stats_record_success() {
let mut stats = PluginLoadStats::default();
stats.record_success();
assert_eq!(stats.loaded, 1);
assert_eq!(stats.discovered, 1);
stats.record_success();
assert_eq!(stats.loaded, 2);
assert_eq!(stats.discovered, 2);
}
#[test]
fn test_plugin_load_stats_record_failure() {
let mut stats = PluginLoadStats::default();
stats.record_failure();
assert_eq!(stats.failed, 1);
assert_eq!(stats.discovered, 1);
}
#[test]
fn test_plugin_load_stats_record_skipped() {
let mut stats = PluginLoadStats::default();
stats.record_skipped();
assert_eq!(stats.skipped, 1);
assert_eq!(stats.discovered, 1);
}
#[test]
fn test_plugin_load_stats_success_rate() {
let mut stats = PluginLoadStats::default();
assert_eq!(stats.success_rate(), 1.0);
stats.record_success();
stats.record_success();
stats.record_failure();
stats.record_skipped();
assert_eq!(stats.success_rate(), 50.0);
}
#[test]
fn test_plugin_load_stats_total_plugins() {
let mut stats = PluginLoadStats::default();
assert_eq!(stats.total_plugins(), 0);
stats.record_success();
stats.record_failure();
stats.record_skipped();
assert_eq!(stats.total_plugins(), 3);
}
#[test]
fn test_plugin_load_stats_clone() {
let mut stats = PluginLoadStats::default();
stats.record_success();
stats.start_loading();
let cloned = stats.clone();
assert_eq!(cloned.loaded, stats.loaded);
assert_eq!(cloned.discovered, stats.discovered);
assert_eq!(cloned.start_time, stats.start_time);
}
#[test]
fn test_plugin_discovery_success() {
let plugin_id = PluginId("test-plugin".to_string());
let manifest = PluginManifest::new(PluginInfo::new(
plugin_id.clone(),
PluginVersion::new(1, 0, 0),
"Test Plugin",
"A test plugin",
PluginAuthor::new("test-author"),
));
let result = PluginDiscovery::success(plugin_id, manifest, "/path/to/plugin".to_string());
assert!(result.is_success());
assert!(result.first_error().is_none());
assert!(result.is_valid);
assert!(result.errors.is_empty());
}
#[test]
fn test_plugin_discovery_failure() {
let plugin_id = PluginId("failing-plugin".to_string());
let errors = vec!["Error 1".to_string(), "Error 2".to_string()];
let result =
PluginDiscovery::failure(plugin_id, "/path/to/plugin".to_string(), errors.clone());
assert!(!result.is_success());
assert_eq!(result.first_error(), Some("Error 1"));
assert_eq!(result.errors.len(), 2);
assert!(!result.is_valid);
}
#[test]
fn test_plugin_discovery_failure_with_empty_errors() {
let plugin_id = PluginId("failing-plugin".to_string());
let errors = vec![];
let result = PluginDiscovery::failure(plugin_id, "/path/to/plugin".to_string(), errors);
assert!(!result.is_success());
assert!(result.first_error().is_none());
assert!(result.errors.is_empty());
}
#[test]
fn test_plugin_discovery_clone() {
let plugin_id = PluginId("test-plugin".to_string());
let manifest = PluginManifest::new(PluginInfo::new(
plugin_id.clone(),
PluginVersion::new(1, 0, 0),
"Test",
"Test",
PluginAuthor::new("test"),
));
let discovery = PluginDiscovery::success(plugin_id, manifest, "/path".to_string());
let cloned = discovery.clone();
assert_eq!(discovery.plugin_id, cloned.plugin_id);
assert_eq!(discovery.is_valid, cloned.is_valid);
}
#[test]
fn test_module_exports() {
let _ = std::marker::PhantomData::<PluginLoader>;
let _ = std::marker::PhantomData::<PluginRegistry>;
let _ = std::marker::PhantomData::<PluginValidator>;
}
#[test]
fn test_loader_result_type() {
let success: LoaderResult<i32> = Ok(42);
assert!(success.is_ok());
match success {
Ok(val) => assert_eq!(val, 42),
Err(_) => panic!("expected Ok"),
}
let error: LoaderResult<i32> = Err(PluginLoaderError::load("test"));
assert!(error.is_err());
}
}