pub mod benchmarks;
pub mod integration;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use async_trait::async_trait;
use serde_json::json;
use tempfile::TempDir;
use crate::core::{
Permission, Plugin, PluginContext, PluginMetadata, PluginOutput, PluginRegistry, PluginResult,
SecurityContext, SecurityManager,
};
#[allow(dead_code)]
pub struct PluginTestHarness {
registry: PluginRegistry,
context: PluginContext,
temp_workspace: TempDir,
security_manager: SecurityManager,
expected_outputs: HashMap<String, serde_json::Value>,
expected_artifacts: Vec<PathBuf>,
expected_permissions: Vec<Permission>,
}
impl PluginTestHarness {
pub fn new() -> PluginResult<Self> {
let temp_workspace =
TempDir::new().map_err(|e| crate::core::PluginError::IoError(e.to_string()))?;
let context = PluginContext::new("test-plugin", temp_workspace.path().to_path_buf());
let registry = PluginRegistry::new();
let security_manager = SecurityManager::new();
Ok(Self {
registry,
context,
temp_workspace,
security_manager,
expected_outputs: HashMap::new(),
expected_artifacts: Vec::new(),
expected_permissions: Vec::new(),
})
}
pub fn with_plugin<P: Plugin + 'static>(mut self, plugin: P) -> PluginResult<Self> {
self.registry.register(plugin)?;
Ok(self)
}
pub fn with_dependency_output(mut self, plugin_name: &str, output: PluginOutput) -> Self {
self.context
.add_dependency_output(plugin_name.to_string(), output);
self
}
pub fn with_config(self, _config: serde_json::Value) -> Self {
self
}
pub fn with_permissions(mut self, permissions: Vec<Permission>) -> PluginResult<Self> {
let permissions_set: std::collections::HashSet<_> = permissions.into_iter().collect();
let security_context =
SecurityContext::new(self.context.plugin_name.clone(), permissions_set);
self.context.set_security_context(security_context);
Ok(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 async fn execute(&mut self, plugin_name: &str) -> PluginResult<PluginOutput> {
let mut plugin = self
.registry
.take_plugin(plugin_name)
.ok_or_else(|| crate::core::PluginError::PluginNotFound(plugin_name.to_string()))?;
plugin.initialize(json!({}), &self.context).await?;
let output = plugin.execute(&mut self.context).await?;
plugin.cleanup(&self.context).await?;
self.registry.put_plugin(plugin)?;
Ok(output)
}
pub async fn execute_with_config(
&mut self,
plugin_name: &str,
config: serde_json::Value,
) -> PluginResult<PluginOutput> {
let mut plugin = self
.registry
.take_plugin(plugin_name)
.ok_or_else(|| crate::core::PluginError::PluginNotFound(plugin_name.to_string()))?;
plugin.initialize(config, &self.context).await?;
let output = plugin.execute(&mut self.context).await?;
plugin.cleanup(&self.context).await?;
self.registry.put_plugin(plugin)?;
Ok(output)
}
pub fn assert_output_contains(
&self,
output: &PluginOutput,
key: &str,
expected: &serde_json::Value,
) -> PluginResult<()> {
let actual = output.data.get(key).ok_or_else(|| {
crate::core::PluginError::ValidationError(format!(
"Output does not contain key '{}'",
key
))
})?;
if actual != expected {
return Err(crate::core::PluginError::ValidationError(format!(
"Expected '{}' = {}, but got {}",
key, expected, actual
)));
}
Ok(())
}
pub fn assert_artifacts_created(
&self,
output: &PluginOutput,
expected_paths: &[&str],
) -> PluginResult<()> {
for expected_path in expected_paths {
let expected_full_path = self.temp_workspace.path().join(expected_path);
if !output.artifacts.contains(&expected_full_path) {
return Err(crate::core::PluginError::ValidationError(format!(
"Expected artifact '{}' was not created",
expected_path
)));
}
if !expected_full_path.exists() {
return Err(crate::core::PluginError::ValidationError(format!(
"Artifact '{}' was listed but file does not exist",
expected_path
)));
}
}
Ok(())
}
pub fn assert_success(&self, output: &PluginOutput) -> PluginResult<()> {
if !output.success {
return Err(crate::core::PluginError::ValidationError(
"Expected plugin execution to succeed".to_string(),
));
}
Ok(())
}
pub fn assert_failure(&self, output: &PluginOutput) -> PluginResult<()> {
if output.success {
return Err(crate::core::PluginError::ValidationError(
"Expected plugin execution to fail".to_string(),
));
}
Ok(())
}
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()))
}
}
impl Default for PluginTestHarness {
fn default() -> Self {
Self::new().expect("Failed to create default PluginTestHarness")
}
}
#[allow(clippy::type_complexity)]
pub struct MockPlugin {
metadata: PluginMetadata,
schema: serde_json::Value,
permissions: Vec<Permission>,
execute_fn:
Arc<Mutex<Box<dyn Fn(&mut PluginContext) -> PluginResult<PluginOutput> + Send + Sync>>>,
initialize_fn: Arc<
Mutex<
Option<
Box<dyn Fn(serde_json::Value, &PluginContext) -> PluginResult<()> + Send + Sync>,
>,
>,
>,
cleanup_fn: Arc<Mutex<Option<Box<dyn Fn(&PluginContext) -> PluginResult<()> + Send + Sync>>>>,
}
impl MockPlugin {
pub fn new(name: &str, version: &str) -> Self {
let metadata = PluginMetadata::new(name, version);
let execute_fn = Arc::new(Mutex::new(Box::new(|_: &mut PluginContext| {
Ok(PluginOutput::success(json!({"mock": true})))
})
as Box<dyn Fn(&mut PluginContext) -> PluginResult<PluginOutput> + Send + Sync>));
Self {
metadata,
schema: json!({"type": "object"}),
permissions: Vec::new(),
execute_fn,
initialize_fn: Arc::new(Mutex::new(None)),
cleanup_fn: Arc::new(Mutex::new(None)),
}
}
pub fn with_execute<F>(self, execute_fn: F) -> Self
where
F: Fn(&mut PluginContext) -> PluginResult<PluginOutput> + Send + Sync + 'static,
{
*self.execute_fn.lock().unwrap() = Box::new(execute_fn);
self
}
pub fn with_initialize<F>(self, initialize_fn: F) -> Self
where
F: Fn(serde_json::Value, &PluginContext) -> PluginResult<()> + Send + Sync + 'static,
{
*self.initialize_fn.lock().unwrap() = Some(Box::new(initialize_fn));
self
}
pub fn with_cleanup<F>(self, cleanup_fn: F) -> Self
where
F: Fn(&PluginContext) -> PluginResult<()> + Send + Sync + 'static,
{
*self.cleanup_fn.lock().unwrap() = Some(Box::new(cleanup_fn));
self
}
pub fn with_permissions(mut self, permissions: Vec<Permission>) -> Self {
self.permissions = permissions;
self
}
pub fn with_dependencies(mut self, dependencies: Vec<String>) -> Self {
self.metadata.dependencies = dependencies;
self
}
pub fn with_schema(mut self, schema: serde_json::Value) -> Self {
self.schema = schema;
self
}
}
#[async_trait]
impl Plugin for MockPlugin {
fn metadata(&self) -> &PluginMetadata {
&self.metadata
}
fn schema(&self) -> serde_json::Value {
self.schema.clone()
}
fn permissions(&self) -> Vec<Permission> {
self.permissions.clone()
}
async fn initialize(
&mut self,
config: serde_json::Value,
context: &PluginContext,
) -> PluginResult<()> {
if let Some(ref initialize_fn) = *self.initialize_fn.lock().unwrap() {
initialize_fn(config, context)
} else {
Ok(())
}
}
async fn execute(&mut self, context: &mut PluginContext) -> PluginResult<PluginOutput> {
let execute_fn = self.execute_fn.lock().unwrap();
execute_fn(context)
}
async fn cleanup(&mut self, context: &PluginContext) -> PluginResult<()> {
if let Some(ref cleanup_fn) = *self.cleanup_fn.lock().unwrap() {
cleanup_fn(context)
} else {
Ok(())
}
}
}
pub mod test_plugins {
use super::*;
pub fn successful_plugin(name: &str) -> MockPlugin {
MockPlugin::new(name, "1.0.0").with_execute(|_| {
Ok(PluginOutput::success(json!({
"status": "completed",
"message": "Plugin executed successfully"
})))
})
}
pub fn failing_plugin(name: &str) -> MockPlugin {
MockPlugin::new(name, "1.0.0").with_execute(|_| {
Err(crate::core::PluginError::ExecutionError(
"Mock plugin failure".to_string(),
))
})
}
pub fn file_creating_plugin(name: &str, filename: &str, content: &str) -> MockPlugin {
let filename = filename.to_string();
let content = content.to_string();
MockPlugin::new(name, "1.0.0").with_execute(move |context| {
let file_path = context.workspace.join(&filename);
std::fs::write(&file_path, &content)
.map_err(|e| crate::core::PluginError::IoError(e.to_string()))?;
Ok(PluginOutput {
success: true,
data: json!({
"file_created": filename,
"content_length": content.len()
}),
artifacts: vec![file_path],
metadata: HashMap::new(),
execution_time: Duration::from_millis(10),
})
})
}
pub fn dependent_plugin(name: &str, dependency: &str) -> MockPlugin {
let dependency = dependency.to_string();
MockPlugin::new(name, "1.0.0")
.with_dependencies(vec![dependency.clone()])
.with_execute(move |context| {
let dep_output = context.get_dependency_output(&dependency).ok_or_else(|| {
crate::core::PluginError::ExecutionError(format!(
"Dependency '{}' output not found",
dependency
))
})?;
Ok(PluginOutput::success(json!({
"dependency_processed": true,
"dependency_data": dep_output.data
})))
})
}
pub fn slow_plugin(name: &str, duration_ms: u64) -> MockPlugin {
MockPlugin::new(name, "1.0.0").with_execute(move |_| {
std::thread::sleep(Duration::from_millis(duration_ms));
Ok(PluginOutput::success(json!({
"execution_time_ms": duration_ms
})))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_plugin_test_harness_basic() {
let mut harness = PluginTestHarness::new().unwrap();
let plugin = test_plugins::successful_plugin("test");
harness = harness.with_plugin(plugin).unwrap();
let output = harness.execute("test").await.unwrap();
harness.assert_success(&output).unwrap();
harness
.assert_output_contains(&output, "status", &json!("completed"))
.unwrap();
}
#[tokio::test]
async fn test_mock_plugin_file_creation() {
let mut harness = PluginTestHarness::new().unwrap();
let plugin =
test_plugins::file_creating_plugin("file-creator", "test.txt", "Hello, World!");
harness = harness.with_plugin(plugin).unwrap();
let output = harness.execute("file-creator").await.unwrap();
harness.assert_success(&output).unwrap();
harness
.assert_artifacts_created(&output, &["test.txt"])
.unwrap();
let content = harness.read_workspace_file("test.txt").unwrap();
assert_eq!(content, "Hello, World!");
}
#[tokio::test]
async fn test_dependency_plugin() {
let mut harness = PluginTestHarness::new().unwrap();
let dep_output = PluginOutput::success(json!({"value": 42}));
harness = harness.with_dependency_output("dependency", dep_output);
let plugin = test_plugins::dependent_plugin("dependent", "dependency");
harness = harness.with_plugin(plugin).unwrap();
let output = harness.execute("dependent").await.unwrap();
harness.assert_success(&output).unwrap();
harness
.assert_output_contains(&output, "dependency_processed", &json!(true))
.unwrap();
}
#[tokio::test]
async fn test_failing_plugin() {
let mut harness = PluginTestHarness::new().unwrap();
let plugin = test_plugins::failing_plugin("failure");
harness = harness.with_plugin(plugin).unwrap();
let result = harness.execute("failure").await;
assert!(result.is_err());
}
#[test]
fn test_workspace_file_operations() {
let harness = PluginTestHarness::new().unwrap();
harness
.with_workspace_file("test/nested/file.txt", "test content")
.unwrap();
let content = harness.read_workspace_file("test/nested/file.txt").unwrap();
assert_eq!(content, "test content");
}
}