pluggable 0.1.0

A comprehensive, async plugin system for Rust applications with dependency management and security
Documentation
//! Integration tests for plugin discovery system

#[cfg(test)]
mod tests {
    use crate::core::{
        discovery::{PluginDiscovery, PluginFactory},
        registry::PluginRegistry,
        Plugin, PluginContext, PluginMetadata, PluginOutput, PluginResult,
    };
    use async_trait::async_trait;
    use serde_json::json;
    use std::sync::{Arc, Mutex};

    // Test plugins for discovery testing
    #[derive(Default)]
    struct DiscoveryTestPluginA {
        metadata: PluginMetadata,
        call_count: Arc<Mutex<usize>>,
    }

    impl DiscoveryTestPluginA {
        fn new() -> Self {
            Self {
                metadata: PluginMetadata::new("discovery-test-a", "1.0.0"),
                call_count: Arc::new(Mutex::new(0)),
            }
        }

        #[allow(dead_code)]
        fn get_call_count(&self) -> usize {
            *self.call_count.lock().unwrap()
        }
    }

    #[async_trait]
    impl Plugin for DiscoveryTestPluginA {
        fn metadata(&self) -> &PluginMetadata {
            &self.metadata
        }

        fn schema(&self) -> serde_json::Value {
            json!({"type": "object"})
        }

        async fn initialize(
            &mut self,
            _config: serde_json::Value,
            _context: &PluginContext,
        ) -> PluginResult<()> {
            *self.call_count.lock().unwrap() += 1;
            Ok(())
        }

        async fn execute(&mut self, _context: &mut PluginContext) -> PluginResult<PluginOutput> {
            *self.call_count.lock().unwrap() += 1;
            Ok(PluginOutput::success(
                json!({"plugin": "test-a", "executed": true}),
            ))
        }

        async fn cleanup(&mut self, _context: &PluginContext) -> PluginResult<()> {
            *self.call_count.lock().unwrap() += 1;
            Ok(())
        }
    }

    #[derive(Default)]
    #[allow(dead_code)]
    struct DiscoveryTestPluginB {
        metadata: PluginMetadata,
    }

    impl DiscoveryTestPluginB {
        #[allow(dead_code)]
        fn new() -> Self {
            let mut metadata = PluginMetadata::new("discovery-test-b", "2.0.0");
            metadata.dependencies = vec!["discovery-test-a".to_string()];
            Self { metadata }
        }
    }

    #[async_trait]
    impl Plugin for DiscoveryTestPluginB {
        fn metadata(&self) -> &PluginMetadata {
            &self.metadata
        }

        fn schema(&self) -> serde_json::Value {
            json!({"type": "object"})
        }

        async fn initialize(
            &mut self,
            _config: serde_json::Value,
            _context: &PluginContext,
        ) -> PluginResult<()> {
            Ok(())
        }

        async fn execute(&mut self, _context: &mut PluginContext) -> PluginResult<PluginOutput> {
            Ok(PluginOutput::success(
                json!({"plugin": "test-b", "executed": true}),
            ))
        }

        async fn cleanup(&mut self, _context: &PluginContext) -> PluginResult<()> {
            Ok(())
        }
    }

    #[test]
    fn test_plugin_factory_creation_and_usage() {
        let factory = PluginFactory::new(
            "test-factory",
            || Box::new(DiscoveryTestPluginA::new()),
            || DiscoveryTestPluginA::new().metadata().clone(),
            "1.0.0",
            "Test factory plugin",
        );

        // Test factory properties
        assert_eq!(factory.name, "test-factory");
        assert_eq!(factory.version, "1.0.0");
        assert_eq!(factory.description, "Test factory plugin");

        // Test plugin creation
        let plugin = factory.create_plugin();
        assert_eq!(plugin.metadata().name, "discovery-test-a");
        assert_eq!(plugin.metadata().version, "1.0.0");

        // Test metadata retrieval
        let metadata = factory.get_metadata();
        assert_eq!(metadata.name, "discovery-test-a");
        assert_eq!(metadata.version, "1.0.0");
    }

    #[test]
    fn test_plugin_discovery_factory_management() {
        let discovery = PluginDiscovery::new();

        // Initially empty
        assert_eq!(discovery.get_all_factories().len(), 0);
        assert!(!discovery.has_plugin("test"));
        assert!(discovery.list_plugin_names().is_empty());

        // Stats should reflect empty state
        let stats = discovery.get_stats();
        assert_eq!(stats.total_factories, 0);
        assert!(!stats.cached);
    }

    #[test]
    fn test_plugin_discovery_search_paths() {
        let mut discovery = PluginDiscovery::new();

        // Add search paths
        discovery.add_search_path("/test/plugins".into());
        discovery.add_search_path("/usr/local/plugins".into());

        let search_paths = discovery.search_paths();
        assert_eq!(search_paths.len(), 2);
        assert_eq!(search_paths[0].to_str().unwrap(), "/test/plugins");
        assert_eq!(search_paths[1].to_str().unwrap(), "/usr/local/plugins");

        // Stats should reflect search paths
        let stats = discovery.get_stats();
        assert_eq!(stats.search_paths, 2);
    }

    #[test]
    fn test_plugin_discovery_cache_behavior() {
        let mut discovery = PluginDiscovery::new();

        // Initially not cached
        assert!(!discovery.get_stats().cached);

        // Clear cache (should not fail even when empty)
        discovery.clear_cache();
        assert!(!discovery.get_stats().cached);

        // Discover plugins (will cache results)
        let _factories = discovery.discover_plugins().unwrap();
        assert!(discovery.get_stats().cached);

        // Clear cache
        discovery.clear_cache();
        assert!(!discovery.get_stats().cached);
    }

    #[test]
    fn test_plugin_discovery_error_handling() {
        let discovery = PluginDiscovery::new();

        // Test getting non-existent plugin
        let result = discovery.create_plugin("nonexistent");
        assert!(result.is_err());
        if let Err(error) = result {
            assert!(matches!(error, crate::core::PluginError::PluginNotFound(_)));
        }

        // Test getting metadata for non-existent plugin
        let result = discovery.get_plugin_metadata("nonexistent");
        assert!(result.is_err());
        if let Err(error) = result {
            assert!(matches!(error, crate::core::PluginError::PluginNotFound(_)));
        }

        // Test getting factory for non-existent plugin
        assert!(discovery.get_factory("nonexistent").is_none());
    }

    #[test]
    fn test_registry_discovery_integration() {
        let mut registry = PluginRegistry::new();

        // Initially empty
        assert_eq!(registry.len(), 0);
        assert!(registry.is_empty());

        // Manual plugin registration still works
        let plugin = DiscoveryTestPluginA::new();
        registry.register(plugin).unwrap();
        assert_eq!(registry.len(), 1);
        assert!(!registry.is_empty());
    }

    #[test]
    fn test_registry_auto_discovery() {
        let mut registry = PluginRegistry::new();

        // Test auto-discovery (will discover from inventory)
        let result = registry.auto_discover();

        // Should succeed even if no plugins are registered via inventory
        // (since we're not using the inventory::submit! macro in tests)
        assert!(result.is_ok());

        // Count should be 0 since no plugins are registered via inventory in tests
        assert_eq!(result.unwrap(), 0);
    }

    #[test]
    fn test_registry_manual_discovery_integration() {
        let mut registry = PluginRegistry::new();
        let discovery = PluginDiscovery::new();

        // Test registering specific plugins by name
        let plugin_names = vec!["nonexistent".to_string()];
        let result = registry.register_from_discovery(&discovery, &plugin_names);

        // Should succeed but register 0 plugins since "nonexistent" doesn't exist
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), 0);
        assert_eq!(registry.len(), 0);
    }

    #[test]
    fn test_plugin_factory_validation() {
        let discovery = PluginDiscovery::new();

        // Test valid factory
        let valid_factory = PluginFactory::new(
            "valid-plugin",
            || Box::new(DiscoveryTestPluginA::new()),
            || DiscoveryTestPluginA::new().metadata().clone(),
            "1.0.0",
            "Valid test plugin",
        );

        // Should validate successfully
        let validation_result = discovery.validate_factory(&valid_factory);
        assert!(validation_result.is_ok());

        // Test factory with empty name
        let invalid_factory = PluginFactory::new(
            "",
            || Box::new(DiscoveryTestPluginA::new()),
            || DiscoveryTestPluginA::new().metadata().clone(),
            "1.0.0",
            "Invalid plugin with empty name",
        );

        let validation_result = discovery.validate_factory(&invalid_factory);
        assert!(validation_result.is_err());

        // Test factory with empty version
        let invalid_factory2 = PluginFactory::new(
            "invalid-plugin",
            || Box::new(DiscoveryTestPluginA::new()),
            || DiscoveryTestPluginA::new().metadata().clone(),
            "",
            "Invalid plugin with empty version",
        );

        let validation_result = discovery.validate_factory(&invalid_factory2);
        assert!(validation_result.is_err());
    }

    #[test]
    fn test_discovery_stats_comprehensive() {
        let mut discovery = PluginDiscovery::new();

        // Initial stats
        let stats = discovery.get_stats();
        assert_eq!(stats.total_factories, 0);
        assert_eq!(stats.search_paths, 0);
        assert!(!stats.cached);

        // Add search paths
        discovery.add_search_path("/test1".into());
        discovery.add_search_path("/test2".into());

        let stats = discovery.get_stats();
        assert_eq!(stats.search_paths, 2);
        assert!(!stats.cached); // Cache should be cleared when search paths change

        // Discover plugins to populate cache
        let _result = discovery.discover_plugins();

        let stats = discovery.get_stats();
        assert!(stats.cached);
    }

    #[test]
    fn test_plugin_cloning_and_multiple_instances() {
        let factory = PluginFactory::new(
            "clonable-plugin",
            || Box::new(DiscoveryTestPluginA::new()),
            || DiscoveryTestPluginA::new().metadata().clone(),
            "1.0.0",
            "Clonable test plugin",
        );

        // Create multiple instances
        let plugin1 = factory.create_plugin();
        let plugin2 = factory.create_plugin();

        // Both should have the same metadata but be separate instances
        assert_eq!(plugin1.metadata().name, plugin2.metadata().name);
        assert_eq!(plugin1.metadata().version, plugin2.metadata().version);

        // But they should be separate objects (different memory addresses)
        let ptr1 = plugin1.as_ref() as *const dyn Plugin;
        let ptr2 = plugin2.as_ref() as *const dyn Plugin;
        assert_ne!(ptr1, ptr2);
    }
}