use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use tempfile::TempDir;
use crate::core::{
Plugin, PluginExecutor, PluginOutput, PluginRegistry, PluginResult, PluginSystemConfig,
SecurityManager,
};
#[derive(Debug, Clone)]
pub struct PipelineTestResults {
pub success: bool,
pub plugin_outputs: HashMap<String, PluginOutput>,
pub execution_order: Vec<Vec<String>>,
pub total_duration: Duration,
pub failed_plugins: Vec<String>,
}
impl PipelineTestResults {
pub fn all_succeeded(&self, expected_plugins: &[&str]) -> bool {
expected_plugins.iter().all(|plugin| {
self.plugin_outputs
.get(*plugin)
.map(|output| output.success)
.unwrap_or(false)
})
}
pub fn get_plugin_output(&self, plugin_name: &str) -> Option<&PluginOutput> {
self.plugin_outputs.get(plugin_name)
}
pub fn verify_execution_order(&self, expected_order: &[Vec<&str>]) -> bool {
if self.execution_order.len() != expected_order.len() {
return false;
}
for (actual_batch, expected_batch) in self.execution_order.iter().zip(expected_order.iter())
{
let mut actual_sorted = actual_batch.clone();
actual_sorted.sort();
let mut expected_sorted: Vec<String> =
expected_batch.iter().map(|s| s.to_string()).collect();
expected_sorted.sort();
if actual_sorted != expected_sorted {
return false;
}
}
true
}
}
#[allow(dead_code)]
pub struct PipelineTestSuite {
registry: PluginRegistry,
config: Option<PluginSystemConfig>,
temp_workspace: TempDir,
expected_outputs: HashMap<String, serde_json::Value>,
plugins: Vec<Box<dyn Plugin>>,
}
impl PipelineTestSuite {
pub fn new() -> PluginResult<Self> {
let temp_workspace =
TempDir::new().map_err(|e| crate::core::PluginError::IoError(e.to_string()))?;
Ok(Self {
registry: PluginRegistry::new(),
config: None,
temp_workspace,
expected_outputs: HashMap::new(),
plugins: Vec::new(),
})
}
pub fn from_config(config: PluginSystemConfig) -> PluginResult<Self> {
let mut suite = Self::new()?;
suite.config = Some(config);
Ok(suite)
}
pub fn with_plugin<P: Plugin + 'static>(mut self, plugin: P) -> PluginResult<Self> {
self.registry.register(plugin)?;
Ok(self)
}
pub fn with_plugins<P: Plugin + 'static>(mut self, plugins: Vec<P>) -> PluginResult<Self> {
for plugin in plugins {
self.registry.register(plugin)?;
}
Ok(self)
}
pub fn with_expected_output(
mut self,
plugin_name: &str,
expected_output: serde_json::Value,
) -> Self {
self.expected_outputs
.insert(plugin_name.to_string(), expected_output);
self
}
pub fn with_workspace_file(&self, relative_path: &str, content: &str) -> PluginResult<()> {
let file_path = self.temp_workspace.path().join(relative_path);
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| crate::core::PluginError::IoError(e.to_string()))?;
}
std::fs::write(&file_path, content)
.map_err(|e| crate::core::PluginError::IoError(e.to_string()))?;
Ok(())
}
pub fn with_simple_config(mut self, plugin_configs: Vec<(&str, serde_json::Value)>) -> Self {
let mut plugin_map = std::collections::HashMap::new();
for (name, config) in plugin_configs {
plugin_map.insert(
name.to_string(),
crate::core::config::PluginConfig {
enabled: true,
config,
permissions: vec![],
priority: 0,
sandbox_override: None,
dependencies: vec![],
optional_dependencies: vec![],
retry: crate::core::config::RetryConfig::default(),
},
);
}
self.config = Some(PluginSystemConfig {
system: crate::core::config::SystemConfig {
workspace: Some(self.temp_workspace.path().to_path_buf()),
..Default::default()
},
plugins: plugin_map,
global_permissions: vec![],
autoload_plugins: vec![],
});
self
}
pub async fn run_pipeline_test(&mut self) -> PluginResult<PipelineTestResults> {
let config = self.config.take().ok_or_else(|| {
crate::core::PluginError::ConfigurationError(
"No configuration provided for pipeline test".to_string(),
)
})?;
let mut executor = PluginExecutor::with_config(std::mem::take(&mut self.registry), config);
let start_time = std::time::Instant::now();
let result = executor.execute_pipeline().await?;
self.registry = std::mem::take(executor.registry_mut());
let total_duration = start_time.elapsed();
for (plugin_name, expected_output) in &self.expected_outputs {
if let Some(actual_output) = result.plugin_outputs.get(plugin_name) {
self.verify_output_matches(&actual_output.data, expected_output)?;
} else {
return Err(crate::core::PluginError::ConfigurationError(format!(
"Expected output for plugin '{}' not found",
plugin_name
)));
}
}
Ok(PipelineTestResults {
success: result.success,
plugin_outputs: result.plugin_outputs,
execution_order: result.execution_order,
total_duration,
failed_plugins: result.failed_plugins,
})
}
pub async fn run_plugin_test(&mut self, plugin_name: &str) -> PluginResult<PluginOutput> {
let _security_manager = SecurityManager::new();
let mut executor = PluginExecutor::new(
std::mem::take(&mut self.registry),
self.temp_workspace.path().to_path_buf(),
);
let result = executor.execute_plugin(plugin_name).await;
self.registry = std::mem::take(executor.registry_mut());
result
}
pub fn workspace_path(&self) -> &std::path::Path {
self.temp_workspace.path()
}
pub fn read_workspace_file(&self, relative_path: &str) -> PluginResult<String> {
let file_path = self.temp_workspace.path().join(relative_path);
std::fs::read_to_string(&file_path)
.map_err(|e| crate::core::PluginError::IoError(e.to_string()))
}
fn verify_output_matches(
&self,
actual: &serde_json::Value,
expected: &serde_json::Value,
) -> PluginResult<()> {
if actual != expected {
return Err(crate::core::PluginError::ConfigurationError(format!(
"Output mismatch: expected {}, got {}",
expected, actual
)));
}
Ok(())
}
}
impl Default for PipelineTestSuite {
fn default() -> Self {
Self::new().expect("Failed to create default PipelineTestSuite")
}
}
pub struct TestConfigBuilder {
plugins: Vec<(String, crate::core::config::PluginConfig)>,
workspace: Option<PathBuf>,
}
impl TestConfigBuilder {
pub fn new() -> Self {
Self {
plugins: Vec::new(),
workspace: None,
}
}
pub fn add_plugin(
mut self,
name: &str,
config: serde_json::Value,
dependencies: Vec<&str>,
) -> Self {
self.plugins.push((
name.to_string(),
crate::core::config::PluginConfig {
enabled: true,
config,
dependencies: dependencies.iter().map(|s| s.to_string()).collect(),
permissions: vec![],
priority: 0,
sandbox_override: None,
optional_dependencies: vec![],
retry: crate::core::config::RetryConfig::default(),
},
));
self
}
pub fn with_workspace(mut self, workspace: PathBuf) -> Self {
self.workspace = Some(workspace);
self
}
pub fn build(self) -> PluginSystemConfig {
let mut plugin_map = std::collections::HashMap::new();
for (name, plugin_config) in self.plugins {
plugin_map.insert(name, plugin_config);
}
PluginSystemConfig {
system: crate::core::config::SystemConfig {
workspace: self.workspace,
..Default::default()
},
plugins: plugin_map,
global_permissions: vec![],
autoload_plugins: vec![],
}
}
}
impl Default for TestConfigBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::testing::test_plugins;
use serde_json::json;
#[tokio::test]
async fn test_pipeline_test_suite_basic() {
let mut suite = PipelineTestSuite::new().unwrap();
let plugin1 = test_plugins::successful_plugin("plugin1");
let plugin2 = test_plugins::successful_plugin("plugin2")
.with_dependencies(vec!["plugin1".to_string()]);
suite = suite.with_plugin(plugin1).unwrap();
suite = suite.with_plugin(plugin2).unwrap();
let config = TestConfigBuilder::new()
.add_plugin("plugin1", json!({}), vec![])
.add_plugin("plugin2", json!({}), vec!["plugin1"])
.build();
suite.config = Some(config);
let results = suite.run_pipeline_test().await.unwrap();
assert!(results.success);
assert!(results.all_succeeded(&["plugin1", "plugin2"]));
assert_eq!(results.execution_order.len(), 2); }
#[tokio::test]
async fn test_pipeline_with_file_creation() {
let mut suite = PipelineTestSuite::new().unwrap();
let workspace_path = suite.workspace_path().to_path_buf();
let plugin =
test_plugins::file_creating_plugin("file-creator", "output.txt", "test content")
.with_permissions(vec![
crate::core::Permission::TempDir,
crate::core::Permission::fs_read_write(workspace_path.clone()),
]);
suite = suite.with_plugin(plugin).unwrap();
let config = TestConfigBuilder::new()
.add_plugin("file-creator", json!({}), vec![])
.with_workspace(workspace_path)
.build();
suite.config = Some(config);
let results = suite.run_pipeline_test().await.unwrap();
assert!(results.success);
let content = suite.read_workspace_file("output.txt").unwrap();
assert_eq!(content, "test content");
}
#[tokio::test]
async fn test_execution_order_verification() {
let mut suite = PipelineTestSuite::new().unwrap();
let plugin1 = test_plugins::successful_plugin("plugin1");
let plugin2 = test_plugins::successful_plugin("plugin2")
.with_dependencies(vec!["plugin1".to_string()]);
let plugin3 = test_plugins::successful_plugin("plugin3");
suite = suite.with_plugin(plugin1).unwrap();
suite = suite.with_plugin(plugin2).unwrap();
suite = suite.with_plugin(plugin3).unwrap();
let config = TestConfigBuilder::new()
.add_plugin("plugin1", json!({}), vec![])
.add_plugin("plugin2", json!({}), vec!["plugin1"])
.add_plugin("plugin3", json!({}), vec![])
.build();
suite.config = Some(config);
let results = suite.run_pipeline_test().await.unwrap();
assert!(results.verify_execution_order(&[vec!["plugin1", "plugin3"], vec!["plugin2"]]));
}
}