1use std::path::Path;
19use std::path::PathBuf;
20
21use mockforge_plugin_core::{
23 PluginAuthor, PluginId, PluginInfo, PluginInstance, PluginManifest, PluginVersion,
24};
25
26pub mod git;
27pub mod installer;
28pub mod invocation_metrics;
29pub mod loader;
30pub mod memory_tracking;
31pub mod metadata;
32pub mod registry;
33pub mod remote;
34pub mod runtime_adapter;
35pub mod sandbox;
36pub mod signature;
37pub mod signature_gen;
38pub mod validator;
39
40pub use git::*;
42pub use installer::*;
43pub use invocation_metrics::{
44 InvocationMetric, InvocationMetricsBus, InvocationStatus, InvocationTimer,
45};
46pub use loader::*;
47pub use memory_tracking::{MemoryStats, MemoryTracker};
48pub use metadata::*;
49pub use registry::*;
50pub use remote::*;
51pub use runtime_adapter::*;
52pub use sandbox::*;
53pub use signature::*;
54pub use signature_gen::*;
55pub use validator::*;
56
57pub type LoaderResult<T> = Result<T, PluginLoaderError>;
59
60#[derive(Debug, thiserror::Error)]
62pub enum PluginLoaderError {
63 #[error("Plugin loading error: {message}")]
65 LoadError {
66 message: String,
68 },
69
70 #[error("Plugin validation error: {message}")]
72 ValidationError {
73 message: String,
75 },
76
77 #[error("Security violation: {violation}")]
79 SecurityViolation {
80 violation: String,
82 },
83
84 #[error("Plugin manifest error: {message}")]
86 ManifestError {
87 message: String,
89 },
90
91 #[error("WebAssembly module error: {message}")]
93 WasmError {
94 message: String,
96 },
97
98 #[error("File system error: {message}")]
100 FsError {
101 message: String,
103 },
104
105 #[error("Plugin already loaded: {plugin_id}")]
107 AlreadyLoaded {
108 plugin_id: PluginId,
110 },
111
112 #[error("Plugin not found: {plugin_id}")]
114 NotFound {
115 plugin_id: PluginId,
117 },
118
119 #[error("Plugin dependency error: {message}")]
121 DependencyError {
122 message: String,
124 },
125
126 #[error("Resource limit exceeded: {message}")]
128 ResourceLimit {
129 message: String,
131 },
132
133 #[error("Plugin execution error: {message}")]
135 ExecutionError {
136 message: String,
138 },
139}
140
141impl PluginLoaderError {
142 pub fn load<S: Into<String>>(message: S) -> Self {
144 Self::LoadError {
145 message: message.into(),
146 }
147 }
148
149 pub fn validation<S: Into<String>>(message: S) -> Self {
151 Self::ValidationError {
152 message: message.into(),
153 }
154 }
155
156 pub fn security<S: Into<String>>(violation: S) -> Self {
158 Self::SecurityViolation {
159 violation: violation.into(),
160 }
161 }
162
163 pub fn manifest<S: Into<String>>(message: S) -> Self {
165 Self::ManifestError {
166 message: message.into(),
167 }
168 }
169
170 pub fn wasm<S: Into<String>>(message: S) -> Self {
172 Self::WasmError {
173 message: message.into(),
174 }
175 }
176
177 pub fn fs<S: Into<String>>(message: S) -> Self {
179 Self::FsError {
180 message: message.into(),
181 }
182 }
183
184 pub fn already_loaded(plugin_id: PluginId) -> Self {
186 Self::AlreadyLoaded { plugin_id }
187 }
188
189 pub fn not_found(plugin_id: PluginId) -> Self {
191 Self::NotFound { plugin_id }
192 }
193
194 pub fn dependency<S: Into<String>>(message: S) -> Self {
196 Self::DependencyError {
197 message: message.into(),
198 }
199 }
200
201 pub fn resource_limit<S: Into<String>>(message: S) -> Self {
203 Self::ResourceLimit {
204 message: message.into(),
205 }
206 }
207
208 pub fn execution<S: Into<String>>(message: S) -> Self {
210 Self::ExecutionError {
211 message: message.into(),
212 }
213 }
214
215 pub fn is_security_error(&self) -> bool {
217 matches!(self, PluginLoaderError::SecurityViolation { .. })
218 }
219}
220
221#[derive(Debug, Clone)]
223pub struct PluginLoaderConfig {
224 pub plugin_dirs: Vec<String>,
226 pub allow_unsigned: bool,
228 pub trusted_keys: Vec<String>,
230 pub key_data: std::collections::HashMap<String, Vec<u8>>,
232 pub max_plugins: usize,
234 pub load_timeout_secs: u64,
236 pub debug_logging: bool,
238 pub skip_wasm_validation: bool,
240}
241
242impl Default for PluginLoaderConfig {
243 fn default() -> Self {
244 Self {
245 plugin_dirs: vec!["~/.mockforge/plugins".to_string(), "./plugins".to_string()],
246 allow_unsigned: false,
247 trusted_keys: vec!["trusted-dev-key".to_string()],
248 key_data: std::collections::HashMap::new(),
249 max_plugins: 100,
250 load_timeout_secs: 30,
251 debug_logging: false,
252 skip_wasm_validation: false,
253 }
254 }
255}
256
257#[derive(Debug, Clone)]
259pub struct PluginLoadContext {
260 pub plugin_id: PluginId,
262 pub manifest: PluginManifest,
264 pub plugin_path: String,
266 pub load_time: chrono::DateTime<chrono::Utc>,
268 pub config: PluginLoaderConfig,
270}
271
272impl PluginLoadContext {
273 pub fn new(
275 plugin_id: PluginId,
276 manifest: PluginManifest,
277 plugin_path: String,
278 config: PluginLoaderConfig,
279 ) -> Self {
280 Self {
281 plugin_id,
282 manifest,
283 plugin_path,
284 load_time: chrono::Utc::now(),
285 config,
286 }
287 }
288}
289
290#[derive(Debug, Clone, Default)]
292pub struct PluginLoadStats {
293 pub discovered: usize,
295 pub loaded: usize,
297 pub failed: usize,
299 pub skipped: usize,
301 pub start_time: Option<chrono::DateTime<chrono::Utc>>,
303 pub end_time: Option<chrono::DateTime<chrono::Utc>>,
305}
306
307impl PluginLoadStats {
308 pub fn start_loading(&mut self) {
310 self.start_time = Some(chrono::Utc::now());
311 }
312
313 pub fn finish_loading(&mut self) {
315 self.end_time = Some(chrono::Utc::now());
316 }
317
318 pub fn record_success(&mut self) {
320 self.loaded += 1;
321 self.discovered += 1;
322 }
323
324 pub fn record_failure(&mut self) {
326 self.failed += 1;
327 self.discovered += 1;
328 }
329
330 pub fn record_skipped(&mut self) {
332 self.skipped += 1;
333 self.discovered += 1;
334 }
335
336 pub fn duration(&self) -> Option<chrono::Duration> {
338 match (self.start_time, self.end_time) {
339 (Some(start), Some(end)) => Some(end - start),
340 _ => None,
341 }
342 }
343
344 pub fn success_rate(&self) -> f64 {
346 if self.discovered == 0 {
347 1.0 } else {
349 (self.loaded as f64 / self.discovered as f64) * 100.0
350 }
351 }
352
353 pub fn total_plugins(&self) -> usize {
355 self.loaded + self.failed + self.skipped
356 }
357}
358
359#[derive(Debug, Clone)]
361pub struct PluginDiscovery {
362 pub plugin_id: PluginId,
364 pub manifest: PluginManifest,
366 pub path: String,
368 pub is_valid: bool,
370 pub errors: Vec<String>,
372}
373
374impl PluginDiscovery {
375 pub fn success(plugin_id: PluginId, manifest: PluginManifest, path: String) -> Self {
377 Self {
378 plugin_id,
379 manifest,
380 path,
381 is_valid: true,
382 errors: Vec::new(),
383 }
384 }
385
386 pub fn failure(plugin_id: PluginId, path: String, errors: Vec<String>) -> Self {
388 let plugin_id_clone = PluginId(plugin_id.0.clone());
389 Self {
390 plugin_id,
391 manifest: PluginManifest::new(PluginInfo::new(
392 plugin_id_clone,
393 PluginVersion::new(0, 0, 0),
394 "Unknown",
395 "Plugin failed to load",
396 PluginAuthor::new("unknown"),
397 )),
398 path,
399 is_valid: false,
400 errors,
401 }
402 }
403
404 pub fn is_success(&self) -> bool {
406 self.is_valid
407 }
408
409 pub fn first_error(&self) -> Option<&str> {
411 self.errors.first().map(|s| s.as_str())
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
422 fn test_plugin_loader_error_types() {
423 let load_error = PluginLoaderError::LoadError {
424 message: "test error".to_string(),
425 };
426 assert!(matches!(load_error, PluginLoaderError::LoadError { .. }));
427
428 let validation_error = PluginLoaderError::ValidationError {
429 message: "validation failed".to_string(),
430 };
431 assert!(matches!(validation_error, PluginLoaderError::ValidationError { .. }));
432 }
433
434 #[test]
435 fn test_error_helper_constructors() {
436 let load_err = PluginLoaderError::load("load failed");
437 assert!(matches!(load_err, PluginLoaderError::LoadError { .. }));
438
439 let validation_err = PluginLoaderError::validation("validation failed");
440 assert!(matches!(validation_err, PluginLoaderError::ValidationError { .. }));
441
442 let security_err = PluginLoaderError::security("security violation");
443 assert!(matches!(security_err, PluginLoaderError::SecurityViolation { .. }));
444
445 let manifest_err = PluginLoaderError::manifest("manifest error");
446 assert!(matches!(manifest_err, PluginLoaderError::ManifestError { .. }));
447
448 let wasm_err = PluginLoaderError::wasm("wasm error");
449 assert!(matches!(wasm_err, PluginLoaderError::WasmError { .. }));
450
451 let fs_err = PluginLoaderError::fs("fs error");
452 assert!(matches!(fs_err, PluginLoaderError::FsError { .. }));
453
454 let dep_err = PluginLoaderError::dependency("dependency error");
455 assert!(matches!(dep_err, PluginLoaderError::DependencyError { .. }));
456
457 let resource_err = PluginLoaderError::resource_limit("resource limit");
458 assert!(matches!(resource_err, PluginLoaderError::ResourceLimit { .. }));
459
460 let exec_err = PluginLoaderError::execution("execution error");
461 assert!(matches!(exec_err, PluginLoaderError::ExecutionError { .. }));
462 }
463
464 #[test]
465 fn test_error_already_loaded() {
466 let plugin_id = PluginId::new("test-plugin");
467 let err = PluginLoaderError::already_loaded(plugin_id.clone());
468 assert!(matches!(err, PluginLoaderError::AlreadyLoaded { .. }));
469 assert_eq!(err.to_string(), format!("Plugin already loaded: {}", plugin_id));
470 }
471
472 #[test]
473 fn test_error_not_found() {
474 let plugin_id = PluginId::new("missing-plugin");
475 let err = PluginLoaderError::not_found(plugin_id.clone());
476 assert!(matches!(err, PluginLoaderError::NotFound { .. }));
477 assert_eq!(err.to_string(), format!("Plugin not found: {}", plugin_id));
478 }
479
480 #[test]
481 fn test_is_security_error() {
482 let security_err = PluginLoaderError::security("test");
483 assert!(security_err.is_security_error());
484
485 let load_err = PluginLoaderError::load("test");
486 assert!(!load_err.is_security_error());
487 }
488
489 #[test]
490 fn test_error_display() {
491 let err = PluginLoaderError::load("test message");
492 let err_str = err.to_string();
493 assert!(err_str.contains("Plugin loading error"));
494 assert!(err_str.contains("test message"));
495 }
496
497 #[test]
500 fn test_plugin_loader_config_default() {
501 let config = PluginLoaderConfig::default();
502 assert_eq!(config.plugin_dirs.len(), 2);
503 assert!(!config.allow_unsigned);
504 assert_eq!(config.max_plugins, 100);
505 assert_eq!(config.load_timeout_secs, 30);
506 assert!(!config.debug_logging);
507 assert!(!config.skip_wasm_validation);
508 }
509
510 #[test]
511 fn test_plugin_loader_config_clone() {
512 let config = PluginLoaderConfig::default();
513 let cloned = config.clone();
514 assert_eq!(config.max_plugins, cloned.max_plugins);
515 assert_eq!(config.load_timeout_secs, cloned.load_timeout_secs);
516 }
517
518 #[test]
521 fn test_plugin_load_context_creation() {
522 let plugin_id = PluginId::new("test-plugin");
523 let manifest = PluginManifest::new(PluginInfo::new(
524 plugin_id.clone(),
525 PluginVersion::new(1, 0, 0),
526 "Test Plugin",
527 "A test plugin",
528 PluginAuthor::new("test-author"),
529 ));
530 let config = PluginLoaderConfig::default();
531
532 let context = PluginLoadContext::new(
533 plugin_id.clone(),
534 manifest.clone(),
535 "/tmp/plugin".to_string(),
536 config.clone(),
537 );
538
539 assert_eq!(context.plugin_id, plugin_id);
540 assert_eq!(context.plugin_path, "/tmp/plugin");
541 assert_eq!(context.config.max_plugins, config.max_plugins);
542 }
543
544 #[test]
547 fn test_plugin_load_stats_default() {
548 let stats = PluginLoadStats::default();
549 assert_eq!(stats.discovered, 0);
550 assert_eq!(stats.loaded, 0);
551 assert_eq!(stats.failed, 0);
552 assert_eq!(stats.skipped, 0);
553 assert!(stats.start_time.is_none());
554 assert!(stats.end_time.is_none());
555 }
556
557 #[test]
558 fn test_plugin_load_stats_timing() {
559 let mut stats = PluginLoadStats::default();
560 assert!(stats.duration().is_none());
561
562 stats.start_loading();
563 assert!(stats.start_time.is_some());
564 assert!(stats.duration().is_none());
565
566 std::thread::sleep(std::time::Duration::from_millis(10));
567
568 stats.finish_loading();
569 assert!(stats.end_time.is_some());
570 assert!(stats.duration().is_some());
571 let duration = stats.duration().unwrap();
572 assert!(duration.num_milliseconds() >= 10);
573 }
574
575 #[test]
576 fn test_plugin_load_stats_record_success() {
577 let mut stats = PluginLoadStats::default();
578 stats.record_success();
579 assert_eq!(stats.loaded, 1);
580 assert_eq!(stats.discovered, 1);
581
582 stats.record_success();
583 assert_eq!(stats.loaded, 2);
584 assert_eq!(stats.discovered, 2);
585 }
586
587 #[test]
588 fn test_plugin_load_stats_record_failure() {
589 let mut stats = PluginLoadStats::default();
590 stats.record_failure();
591 assert_eq!(stats.failed, 1);
592 assert_eq!(stats.discovered, 1);
593 }
594
595 #[test]
596 fn test_plugin_load_stats_record_skipped() {
597 let mut stats = PluginLoadStats::default();
598 stats.record_skipped();
599 assert_eq!(stats.skipped, 1);
600 assert_eq!(stats.discovered, 1);
601 }
602
603 #[test]
604 fn test_plugin_load_stats_success_rate() {
605 let mut stats = PluginLoadStats::default();
606 assert_eq!(stats.success_rate(), 1.0);
608
609 stats.record_success();
610 stats.record_success();
611 stats.record_failure();
612 stats.record_skipped();
613 assert_eq!(stats.success_rate(), 50.0);
615 }
616
617 #[test]
618 fn test_plugin_load_stats_total_plugins() {
619 let mut stats = PluginLoadStats::default();
620 assert_eq!(stats.total_plugins(), 0);
621
622 stats.record_success();
623 stats.record_failure();
624 stats.record_skipped();
625 assert_eq!(stats.total_plugins(), 3);
626 }
627
628 #[test]
629 fn test_plugin_load_stats_clone() {
630 let mut stats = PluginLoadStats::default();
631 stats.record_success();
632 stats.start_loading();
633
634 let cloned = stats.clone();
635 assert_eq!(cloned.loaded, stats.loaded);
636 assert_eq!(cloned.discovered, stats.discovered);
637 assert_eq!(cloned.start_time, stats.start_time);
638 }
639
640 #[test]
643 fn test_plugin_discovery_success() {
644 let plugin_id = PluginId("test-plugin".to_string());
645 let manifest = PluginManifest::new(PluginInfo::new(
646 plugin_id.clone(),
647 PluginVersion::new(1, 0, 0),
648 "Test Plugin",
649 "A test plugin",
650 PluginAuthor::new("test-author"),
651 ));
652
653 let result = PluginDiscovery::success(plugin_id, manifest, "/path/to/plugin".to_string());
654
655 assert!(result.is_success());
656 assert!(result.first_error().is_none());
657 assert!(result.is_valid);
658 assert!(result.errors.is_empty());
659 }
660
661 #[test]
662 fn test_plugin_discovery_failure() {
663 let plugin_id = PluginId("failing-plugin".to_string());
664 let errors = vec!["Error 1".to_string(), "Error 2".to_string()];
665
666 let result =
667 PluginDiscovery::failure(plugin_id, "/path/to/plugin".to_string(), errors.clone());
668
669 assert!(!result.is_success());
670 assert_eq!(result.first_error(), Some("Error 1"));
671 assert_eq!(result.errors.len(), 2);
672 assert!(!result.is_valid);
673 }
674
675 #[test]
676 fn test_plugin_discovery_failure_with_empty_errors() {
677 let plugin_id = PluginId("failing-plugin".to_string());
678 let errors = vec![];
679
680 let result = PluginDiscovery::failure(plugin_id, "/path/to/plugin".to_string(), errors);
681
682 assert!(!result.is_success());
683 assert!(result.first_error().is_none());
684 assert!(result.errors.is_empty());
685 }
686
687 #[test]
688 fn test_plugin_discovery_clone() {
689 let plugin_id = PluginId("test-plugin".to_string());
690 let manifest = PluginManifest::new(PluginInfo::new(
691 plugin_id.clone(),
692 PluginVersion::new(1, 0, 0),
693 "Test",
694 "Test",
695 PluginAuthor::new("test"),
696 ));
697
698 let discovery = PluginDiscovery::success(plugin_id, manifest, "/path".to_string());
699 let cloned = discovery.clone();
700
701 assert_eq!(discovery.plugin_id, cloned.plugin_id);
702 assert_eq!(discovery.is_valid, cloned.is_valid);
703 }
704
705 #[test]
708 fn test_module_exports() {
709 let _ = std::marker::PhantomData::<PluginLoader>;
711 let _ = std::marker::PhantomData::<PluginRegistry>;
712 let _ = std::marker::PhantomData::<PluginValidator>;
713 }
715
716 #[test]
717 fn test_loader_result_type() {
718 let success: LoaderResult<i32> = Ok(42);
719 assert!(success.is_ok());
720 match success {
721 Ok(val) => assert_eq!(val, 42),
722 Err(_) => panic!("expected Ok"),
723 }
724
725 let error: LoaderResult<i32> = Err(PluginLoaderError::load("test"));
726 assert!(error.is_err());
727 }
728}