pub mod config;
pub mod fixtures;
pub mod helpers;
pub mod mocks;
pub mod test_mocks;
use crate::abstractions::{ClaudeClient, GitOperations, MockClaudeClient, MockGitOperations};
use anyhow::Result;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
use self::config::TestConfiguration;
pub struct TestContext {
pub git_ops: Box<dyn GitOperations>,
pub claude_client: Box<dyn ClaudeClient>,
pub temp_dir: TempDir,
pub config: Arc<TestConfiguration>,
}
impl TestContext {
pub fn new() -> Result<Self> {
let temp_dir = TempDir::new()?;
let git_ops = Box::new(MockGitOperations::new());
let claude_client = Box::new(MockClaudeClient::new());
let config = Arc::new(TestConfiguration::default());
Ok(Self {
git_ops,
claude_client,
temp_dir,
config,
})
}
pub fn with_mocks(
git_ops: Box<dyn GitOperations>,
claude_client: Box<dyn ClaudeClient>,
) -> Result<Self> {
let temp_dir = TempDir::new()?;
let config = Arc::new(TestConfiguration::default());
Ok(Self {
git_ops,
claude_client,
temp_dir,
config,
})
}
pub fn with_config(config: TestConfiguration) -> Result<Self> {
let temp_dir = TempDir::new()?;
let git_ops = Box::new(MockGitOperations::new());
let claude_client = Box::new(MockClaudeClient::new());
let config = Arc::new(config);
Ok(Self {
git_ops,
claude_client,
temp_dir,
config,
})
}
pub fn with_mocks_and_config(
git_ops: Box<dyn GitOperations>,
claude_client: Box<dyn ClaudeClient>,
config: TestConfiguration,
) -> Result<Self> {
let temp_dir = TempDir::new()?;
let config = Arc::new(config);
Ok(Self {
git_ops,
claude_client,
temp_dir,
config,
})
}
pub fn temp_path(&self) -> PathBuf {
self.temp_dir.path().to_path_buf()
}
pub fn create_test_file(&self, name: &str, content: &str) -> Result<PathBuf> {
use std::fs;
let file_path = self.temp_dir.path().join(name);
fs::write(&file_path, content)?;
Ok(file_path)
}
}
pub struct MockGitBuilder {
mock: MockGitOperations,
}
impl Default for MockGitBuilder {
fn default() -> Self {
Self::new()
}
}
impl MockGitBuilder {
pub fn new() -> Self {
Self {
mock: MockGitOperations::new(),
}
}
pub fn is_repo(mut self, is_repo: bool) -> Self {
self.mock.is_repo = is_repo;
self
}
pub async fn with_success(self, stdout: &str) -> Self {
self.mock.add_success_response(stdout).await;
self
}
pub async fn with_error(self, error: &str) -> Self {
self.mock.add_error_response(error).await;
self
}
pub fn build(self) -> MockGitOperations {
self.mock
}
}
pub struct MockClaudeBuilder {
mock: MockClaudeClient,
}
impl Default for MockClaudeBuilder {
fn default() -> Self {
Self::new()
}
}
impl MockClaudeBuilder {
pub fn new() -> Self {
Self {
mock: MockClaudeClient::new(),
}
}
pub fn is_available(mut self, available: bool) -> Self {
self.mock.is_available = available;
self
}
pub async fn with_success(self, stdout: &str) -> Self {
self.mock.add_success_response(stdout).await;
self
}
pub async fn with_error(self, stderr: &str, exit_code: i32) -> Self {
self.mock.add_error_response(stderr, exit_code).await;
self
}
pub fn build(self) -> MockClaudeClient {
self.mock
}
}
pub struct TestFixtures;
impl TestFixtures {
pub async fn clean_repo_git() -> MockGitOperations {
MockGitBuilder::new()
.is_repo(true)
.with_success("") .await
.with_success("Initial commit") .await
.build()
}
pub async fn dirty_repo_git() -> MockGitOperations {
MockGitBuilder::new()
.is_repo(true)
.with_success("M src/main.rs\nA src/new.rs") .await
.with_success("Previous commit")
.await
.build()
}
pub async fn successful_claude() -> MockClaudeClient {
MockClaudeBuilder::new()
.is_available(true)
.with_success("Review completed successfully")
.await
.with_success("Implementation completed")
.await
.with_success("Linting completed")
.await
.build()
}
pub async fn rate_limited_claude() -> MockClaudeClient {
MockClaudeBuilder::new()
.is_available(true)
.with_error("Error: rate limit exceeded", 1)
.await
.build()
}
pub fn unavailable_claude() -> MockClaudeClient {
let mut mock = MockClaudeClient::new();
mock.is_available = false;
mock
}
}
#[cfg(test)]
pub mod test_helpers {
use super::*;
use std::fs;
use std::path::Path;
pub use tempfile::TempDir;
pub fn setup_test_project(temp_dir: &TempDir) -> PathBuf {
let project_path = temp_dir.path().to_path_buf();
fs::create_dir_all(project_path.join("src")).expect("Failed to create src dir");
fs::create_dir_all(project_path.join("tests")).expect("Failed to create tests dir");
fs::create_dir_all(project_path.join("benches")).expect("Failed to create benches dir");
project_path
}
pub fn create_test_file(dir: &Path, name: &str, content: &str) -> PathBuf {
let file_path = dir.join(name);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent).expect("Failed to create parent directory");
}
fs::write(&file_path, content).expect("Failed to write test file");
file_path
}
pub fn create_test_files(dir: &Path, files: &[(&str, &str)]) {
for (path, content) in files {
create_test_file(dir, path, content);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_context_creation() {
let context = TestContext::new().unwrap();
assert!(context.temp_dir.path().exists());
}
#[tokio::test]
async fn test_create_test_file() {
let context = TestContext::new().unwrap();
let file_path = context
.create_test_file("test.txt", "Hello, world!")
.unwrap();
assert!(file_path.exists());
let content = std::fs::read_to_string(file_path).unwrap();
assert_eq!(content, "Hello, world!");
}
#[tokio::test]
async fn test_mock_builders() {
let git_mock = MockGitBuilder::new()
.is_repo(true)
.with_success("test output")
.await
.build();
assert!(git_mock.is_repo);
let claude_mock = MockClaudeBuilder::new()
.is_available(true)
.with_success("test response")
.await
.build();
assert!(claude_mock.is_available);
}
#[tokio::test]
async fn test_fixtures() {
let clean_git = TestFixtures::clean_repo_git().await;
assert!(clean_git.is_repo);
let dirty_git = TestFixtures::dirty_repo_git().await;
assert!(dirty_git.is_repo);
let successful_claude = TestFixtures::successful_claude().await;
assert!(successful_claude.is_available);
let unavailable_claude = TestFixtures::unavailable_claude();
assert!(!unavailable_claude.is_available);
}
}