use super::*;
use super::runner::CommandRunner;
use crate::cli::session::{
manager::SessionManager,
storage::{JsonFileStorage, StorageConfig},
};
use crate::{cli::error::InteractiveError, cli::error::Result};
use proptest::prelude::*;
use std::sync::Arc;
use std::time::Duration;
use tempfile::tempdir;
use tokio::time::timeout;
pub struct ExecutionTestFixture {
pub session_manager: Arc<SessionManager>,
pub runner: Arc<CommandRunner>,
pub executor: ParallelExecutor,
pub temp_dir: tempfile::TempDir,
}
impl ExecutionTestFixture {
pub async fn new() -> Result<Self> {
let temp_dir = tempdir().unwrap();
let config = StorageConfig {
data_dir: temp_dir.path().to_path_buf(),
sessions_file: "test_sessions.json".to_string(),
current_session_file: "test_current.json".to_string(),
};
let storage = Arc::new(JsonFileStorage::new(config));
let session_manager = Arc::new(SessionManager::new(storage));
let runner = Arc::new(CommandRunner::new(Arc::clone(&session_manager))?);
let executor = ParallelExecutor::new(Arc::clone(&runner));
Ok(Self {
session_manager,
runner,
executor,
temp_dir,
})
}
pub fn create_context(&self, command: &str, args: Vec<String>) -> ExecutionContext {
ExecutionContext {
session_id: None,
command_name: command.to_string(),
args,
parallel: false,
agent_count: 1,
}
}
pub fn create_context_with_session(
&self,
command: &str,
args: Vec<String>,
session_id: uuid::Uuid,
) -> ExecutionContext {
ExecutionContext {
session_id: Some(session_id),
command_name: command.to_string(),
args,
parallel: false,
agent_count: 1,
}
}
}
pub mod execution_strategies {
use super::*;
pub fn command_name() -> impl Strategy<Value = String> {
prop::string::string_regex(r"[a-zA-Z][a-zA-Z0-9_-]{0,20}").unwrap()
}
pub fn command_args() -> impl Strategy<Value = Vec<String>> {
prop::collection::vec(
prop::string::string_regex(r"[a-zA-Z0-9_.-]{1,50}").unwrap(),
0..10,
)
}
pub fn execution_context() -> impl Strategy<Value = ExecutionContext> {
(command_name(), command_args(), prop::bool::ANY, 1usize..=10usize).prop_map(
|(command_name, args, parallel, agent_count)| ExecutionContext {
session_id: None,
command_name,
args,
parallel,
agent_count,
},
)
}
pub fn runner_config() -> impl Strategy<Value = RunnerConfig> {
(
1u64..=300u64, prop::bool::ANY, prop::bool::ANY, 0usize..=10usize, 0u64..=10u64, )
.prop_map(|(timeout_secs, streaming, json_format, max_retries, retry_delay_secs)| {
let stream_format = if json_format {
claude_sdk_rs::StreamFormat::Json
} else {
claude_sdk_rs::StreamFormat::Text
};
RunnerConfig {
timeout: Duration::from_secs(timeout_secs),
streaming,
stream_format,
max_retries,
retry_delay: Duration::from_secs(retry_delay_secs),
}
})
}
pub fn parallel_config() -> impl Strategy<Value = ParallelConfig> {
(
1usize..=20usize, prop::bool::ANY, 30u64..=600u64, 100usize..=5000usize, )
.prop_map(|(max_concurrent, continue_on_error, timeout_secs, buffer_size)| {
ParallelConfig {
max_concurrent,
continue_on_error,
total_timeout: Duration::from_secs(timeout_secs),
output_buffer_size: buffer_size,
}
})
}
}
#[cfg(test)]
mod runner_tests {
use super::*;
#[tokio::test]
async fn test_runner_creation_and_config() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
assert_eq!(fixture.runner.config().timeout, Duration::from_secs(30));
assert!(fixture.runner.config().streaming);
assert_eq!(fixture.runner.config().max_retries, 3);
Ok(())
}
#[tokio::test]
async fn test_command_execution_with_arguments() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let context1 = fixture.create_context("echo", vec!["hello".to_string()]);
let result1 = fixture.runner.execute_command(context1).await;
assert!(result1.is_ok(), "Command execution should succeed");
let context2 = fixture.create_context("echo", vec!["hello".to_string(), "world".to_string()]);
let result2 = fixture.runner.execute_command(context2).await;
assert!(result2.is_ok(), "Command execution with multiple args should succeed");
Ok(())
}
#[tokio::test]
async fn test_config_update() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let mut runner = CommandRunner::new(Arc::clone(&fixture.session_manager))?;
let new_config = RunnerConfig {
timeout: Duration::from_secs(60),
streaming: false,
stream_format: claude_sdk_rs::StreamFormat::Json,
max_retries: 5,
retry_delay: Duration::from_secs(2),
};
runner.set_config(new_config.clone())?;
assert_eq!(runner.config().timeout, Duration::from_secs(60));
assert!(!runner.config().streaming);
assert_eq!(runner.config().max_retries, 5);
Ok(())
}
#[tokio::test]
async fn test_runner_default_implementation() {
let _runner = CommandRunner::default();
}
#[tokio::test]
async fn test_error_handling_for_invalid_commands() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let context = ExecutionContext {
session_id: None,
command_name: "".to_string(),
args: vec![],
parallel: false,
agent_count: 1,
};
let result = fixture.runner.execute_command(context).await;
assert!(result.is_ok() || result.is_err(), "Should handle empty commands predictably");
Ok(())
}
proptest! {
#[test]
fn property_command_execution_behavior(
context in execution_strategies::execution_context()
) {
tokio_test::block_on(async {
let fixture = ExecutionTestFixture::new().await.unwrap();
let result = fixture.runner.execute_command(context.clone()).await;
match result {
Ok(exec_result) => {
prop_assert!(exec_result.timestamp > chrono::Utc::now() - chrono::Duration::minutes(1));
prop_assert!(exec_result.cost >= 0.0);
}
Err(_) => {
prop_assert!(true); }
}
Ok(())
}).unwrap();
}
#[test]
fn property_config_updates(
config in execution_strategies::runner_config()
) {
tokio_test::block_on(async {
let fixture = ExecutionTestFixture::new().await.unwrap();
let mut runner = CommandRunner::new(Arc::clone(&fixture.session_manager)).unwrap();
runner.set_config(config.clone()).unwrap();
prop_assert_eq!(runner.config().timeout, config.timeout);
prop_assert_eq!(runner.config().streaming, config.streaming);
prop_assert_eq!(runner.config().max_retries, config.max_retries);
prop_assert_eq!(runner.config().retry_delay, config.retry_delay);
Ok(())
}).unwrap();
}
}
}
#[cfg(test)]
mod parallel_tests {
use super::*;
#[tokio::test]
async fn test_parallel_executor_creation() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let stats = fixture.executor.get_stats().await;
assert_eq!(stats.active_executions, 0);
assert_eq!(stats.total_executions, 0);
assert_eq!(fixture.executor.config().max_concurrent, 4);
Ok(())
}
#[tokio::test]
async fn test_empty_parallel_execution() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let result = fixture.executor.execute_parallel(vec![]).await?;
assert_eq!(result.successful_count, 0);
assert_eq!(result.failed_count, 0);
assert!(result.results.is_empty());
assert!(result.outputs.is_empty());
Ok(())
}
#[tokio::test]
async fn test_parallel_config_update() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let mut executor = ParallelExecutor::new(Arc::clone(&fixture.runner));
let new_config = ParallelConfig {
max_concurrent: 8,
continue_on_error: false,
total_timeout: Duration::from_secs(600),
output_buffer_size: 2000,
};
executor.set_config(new_config.clone());
assert_eq!(executor.config().max_concurrent, 8);
assert!(!executor.config().continue_on_error);
assert_eq!(executor.config().total_timeout, Duration::from_secs(600));
Ok(())
}
#[tokio::test]
async fn test_cancel_all_executions() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let cancelled_count = fixture.executor.cancel_all().await?;
assert_eq!(cancelled_count, 0);
let stats = fixture.executor.get_stats().await;
assert_eq!(stats.active_executions, 0);
assert_eq!(stats.queued_executions, 0);
Ok(())
}
#[tokio::test]
async fn test_execution_handle_creation() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let context = fixture.create_context("test", vec!["arg".to_string()]);
let (tx, _rx) = tokio::sync::mpsc::channel::<ParallelOutput>(100);
let handle = ExecutionHandle {
id: uuid::Uuid::new_v4(),
agent_id: crate::cli::output::AgentId(1),
context: context.clone(),
};
assert_eq!(handle.context.command_name, "test");
assert_eq!(handle.context.args, vec!["arg"]);
Ok(())
}
#[tokio::test]
async fn test_parallel_output_structure() {
let context = ExecutionContext {
session_id: None,
command_name: "test".to_string(),
args: vec!["arg".to_string()],
parallel: false,
agent_count: 1,
};
let handle = ExecutionHandle {
id: uuid::Uuid::new_v4(),
agent_id: crate::cli::output::AgentId(1),
context,
};
let output = ParallelOutput {
handle: handle.clone(),
content: "test output".to_string(),
is_final: true,
};
assert_eq!(output.content, "test output");
assert!(output.is_final);
assert_eq!(output.handle.agent_id.0, 1);
}
proptest! {
#[test]
fn property_parallel_config_validation(
config in execution_strategies::parallel_config()
) {
tokio_test::block_on(async {
let fixture = ExecutionTestFixture::new().await.unwrap();
let mut executor = ParallelExecutor::new(Arc::clone(&fixture.runner));
executor.set_config(config.clone());
prop_assert_eq!(executor.config().max_concurrent, config.max_concurrent);
prop_assert_eq!(executor.config().continue_on_error, config.continue_on_error);
prop_assert_eq!(executor.config().total_timeout, config.total_timeout);
prop_assert_eq!(executor.config().output_buffer_size, config.output_buffer_size);
prop_assert!(executor.config().max_concurrent >= 1);
prop_assert!(executor.config().output_buffer_size >= 100);
Ok(())
}).unwrap();
}
#[test]
fn property_execution_stats_consistency(
contexts in prop::collection::vec(execution_strategies::execution_context(), 0..10)
) {
tokio_test::block_on(async {
let fixture = ExecutionTestFixture::new().await.unwrap();
let initial_stats = fixture.executor.get_stats().await;
prop_assert_eq!(initial_stats.active_executions, 0);
prop_assert_eq!(initial_stats.total_executions, 0);
prop_assert_eq!(initial_stats.completed_executions, 0);
prop_assert_eq!(initial_stats.failed_executions, 0);
let result = fixture.executor.execute_parallel(vec![]).await.unwrap();
let after_stats = fixture.executor.get_stats().await;
prop_assert_eq!(after_stats.total_executions, initial_stats.total_executions);
prop_assert_eq!(result.successful_count + result.failed_count, 0);
Ok(())
}).unwrap();
}
}
}
#[cfg(test)]
mod integration_tests {
use super::*;
#[tokio::test]
async fn test_execution_context_variations() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let context1 = fixture.create_context("help", vec![]);
assert!(context1.session_id.is_none());
assert_eq!(context1.command_name, "help");
assert!(!context1.parallel);
let session_id = uuid::Uuid::new_v4();
let context2 = fixture.create_context_with_session("test", vec!["arg".to_string()], session_id);
assert_eq!(context2.session_id, Some(session_id));
assert_eq!(context2.command_name, "test");
assert_eq!(context2.args, vec!["arg"]);
Ok(())
}
#[tokio::test]
async fn test_execution_result_structure() {
let result = ExecutionResult {
output: "test output".to_string(),
cost: Some(0.05),
duration: Duration::from_millis(1500),
success: true,
};
assert_eq!(result.output, "test output");
assert_eq!(result.cost, Some(0.05));
assert_eq!(result.duration, Duration::from_millis(1500));
assert!(result.success);
let failed_result = ExecutionResult {
output: "error message".to_string(),
cost: None,
duration: Duration::from_millis(500),
success: false,
};
assert!(!failed_result.success);
assert!(failed_result.cost.is_none());
}
#[tokio::test]
async fn test_runner_with_different_configs() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let config1 = RunnerConfig {
timeout: Duration::from_secs(60),
streaming: true,
stream_format: claude_sdk_rs::StreamFormat::StreamJson,
max_retries: 2,
retry_delay: Duration::from_millis(500),
};
let runner1 = CommandRunner::with_config(Arc::clone(&fixture.session_manager), config1)?;
assert_eq!(runner1.config().timeout, Duration::from_secs(60));
assert_eq!(runner1.config().max_retries, 2);
let config2 = RunnerConfig {
timeout: Duration::from_secs(30),
streaming: false,
stream_format: claude_sdk_rs::StreamFormat::Text,
max_retries: 1,
retry_delay: Duration::from_secs(1),
};
let runner2 = CommandRunner::with_config(Arc::clone(&fixture.session_manager), config2)?;
assert!(!runner2.config().streaming);
assert_eq!(runner2.config().max_retries, 1);
Ok(())
}
#[tokio::test]
async fn test_parallel_result_aggregation() {
let mut results = std::collections::HashMap::new();
results.insert(
uuid::Uuid::new_v4(),
Ok(ExecutionResult {
output: "success".to_string(),
cost: Some(0.01),
duration: Duration::from_millis(1000),
success: true,
}),
);
results.insert(
uuid::Uuid::new_v4(),
Err(InteractiveError::execution("test error".to_string())),
);
let parallel_result = ParallelResult {
results,
total_duration: Duration::from_millis(2000),
successful_count: 1,
failed_count: 1,
outputs: vec![],
};
assert_eq!(parallel_result.successful_count, 1);
assert_eq!(parallel_result.failed_count, 1);
assert_eq!(parallel_result.total_duration, Duration::from_millis(2000));
assert_eq!(parallel_result.results.len(), 2);
}
#[tokio::test]
async fn test_stats_initialization_and_updates() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let stats = fixture.executor.get_stats().await;
assert_eq!(stats.active_executions, 0);
assert_eq!(stats.queued_executions, 0);
assert_eq!(stats.completed_executions, 0);
assert_eq!(stats.failed_executions, 0);
assert_eq!(stats.total_executions, 0);
Ok(())
}
#[tokio::test]
async fn test_execution_timeout_handling() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let config = RunnerConfig {
timeout: Duration::from_millis(100), streaming: false,
stream_format: claude_sdk_rs::StreamFormat::Text,
max_retries: 0,
retry_delay: Duration::from_millis(10),
};
let mut runner = CommandRunner::with_config(Arc::clone(&fixture.session_manager), config)?;
assert_eq!(runner.config().timeout, Duration::from_millis(100));
Ok(())
}
}
#[cfg(test)]
mod error_handling_tests {
use super::*;
#[tokio::test]
async fn test_error_propagation() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let error = InteractiveError::execution("test error".to_string());
let error_string = format!("{}", error);
assert!(error_string.contains("test error"));
Ok(())
}
#[tokio::test]
async fn test_retry_logic_configuration() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let config = RunnerConfig {
timeout: Duration::from_secs(30),
streaming: false,
stream_format: claude_sdk_rs::StreamFormat::Text,
max_retries: 5,
retry_delay: Duration::from_millis(100),
};
let runner = CommandRunner::with_config(Arc::clone(&fixture.session_manager), config)?;
assert_eq!(runner.config().max_retries, 5);
assert_eq!(runner.config().retry_delay, Duration::from_millis(100));
Ok(())
}
#[tokio::test]
async fn test_parallel_executor_error_handling() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let config = ParallelConfig {
max_concurrent: 2,
continue_on_error: false, total_timeout: Duration::from_secs(30),
output_buffer_size: 100,
};
let mut executor = ParallelExecutor::with_config(Arc::clone(&fixture.runner), config);
assert!(!executor.config().continue_on_error);
Ok(())
}
}
#[cfg(test)]
mod edge_case_tests {
use super::*;
#[tokio::test]
async fn test_extremely_large_command_arguments() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let large_args: Vec<String> = (0..100).map(|i| format!("arg{}", i)).collect(); let context = fixture.create_context("echo", large_args.clone());
let result = fixture.runner.execute_command(context).await;
assert!(result.is_ok() || result.is_err(), "Should handle large argument lists gracefully");
Ok(())
}
#[tokio::test]
async fn test_unicode_command_handling() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let context = fixture.create_context("echo", vec!["参数".to_string(), "🚀".to_string()]);
let result = fixture.runner.execute_command(context).await;
assert!(result.is_ok() || result.is_err(), "Should handle unicode characters gracefully");
Ok(())
}
#[tokio::test]
async fn test_zero_timeout_configuration() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let config = RunnerConfig {
timeout: Duration::from_nanos(1), streaming: false,
stream_format: claude_sdk_rs::StreamFormat::Text,
max_retries: 0,
retry_delay: Duration::from_nanos(1),
};
let runner = CommandRunner::with_config(Arc::clone(&fixture.session_manager), config)?;
assert!(runner.config().timeout.as_nanos() > 0);
Ok(())
}
#[tokio::test]
async fn test_maximum_concurrent_executions() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let config = ParallelConfig {
max_concurrent: 10000,
continue_on_error: true,
total_timeout: Duration::from_secs(1),
output_buffer_size: 1,
};
let mut executor = ParallelExecutor::with_config(Arc::clone(&fixture.runner), config);
assert_eq!(executor.config().max_concurrent, 10000);
assert_eq!(executor.config().output_buffer_size, 1);
Ok(())
}
#[tokio::test]
async fn test_empty_and_whitespace_commands() -> Result<()> {
let fixture = ExecutionTestFixture::new().await?;
let context1 = fixture.create_context("", vec![]);
let result1 = fixture.runner.execute_command(context1).await;
assert!(result1.is_ok() || result1.is_err(), "Should handle empty commands gracefully");
let context2 = fixture.create_context(" ", vec![" ".to_string()]);
let result2 = fixture.runner.execute_command(context2).await;
assert!(result2.is_ok() || result2.is_err(), "Should handle whitespace commands gracefully");
Ok(())
}
}