//! Mock implementation of JjExecutor for testing //! //! This mock allows configuring responses for each method and tracks method calls //! for verification. It's used extensively in workflow tests. use super::JjExecutor; use crate::error::Error; use async_trait::async_trait; use std::sync::{Mutex, atomic::AtomicBool}; /// Mock implementation of JjExecutor for testing #[derive(Debug)] pub struct MockJjExecutor { /// Response to return from is_repository() is_repo_response: Result, /// Response to return from describe() describe_response: Result<(), Error>, /// Track calls to is_repository() is_repo_called: AtomicBool, /// Track calls to describe() with the message passed describe_calls: Mutex>, } impl Default for MockJjExecutor { fn default() -> Self { Self { is_repo_response: Ok(true), describe_response: Ok(()), is_repo_called: AtomicBool::new(false), describe_calls: Mutex::new(Vec::new()), } } } impl MockJjExecutor { /// Create a new mock with default success responses pub fn new() -> Self { Self::default() } /// Configure is_repository() to return a specific value pub fn with_is_repo_response(mut self, response: Result) -> Self { self.is_repo_response = response; self } /// Configure describe() to return a specific value pub fn with_describe_response(mut self, response: Result<(), Error>) -> Self { self.describe_response = response; self } /// Check if is_repository() was called pub fn was_is_repo_called(&self) -> bool { self.is_repo_called .load(std::sync::atomic::Ordering::SeqCst) } /// Get all messages passed to describe() pub fn describe_messages(&self) -> Vec { self.describe_calls.lock().unwrap().clone() } } #[async_trait(?Send)] impl JjExecutor for MockJjExecutor { async fn is_repository(&self) -> Result { self.is_repo_called .store(true, std::sync::atomic::Ordering::SeqCst); match &self.is_repo_response { Ok(v) => Ok(*v), Err(e) => Err(e.clone()), } } async fn describe(&self, message: &str) -> Result<(), Error> { self.describe_calls .lock() .unwrap() .push(message.to_string()); match &self.describe_response { Ok(()) => Ok(()), Err(e) => Err(e.clone()), } } } #[cfg(test)] mod tests { use super::*; use crate::error::Error; /// Test that mock can implement JjExecutor trait #[test] fn mock_implements_trait() { let mock = MockJjExecutor::new(); fn _accepts_executor(_e: impl JjExecutor) {} _accepts_executor(mock); } /// Test mock is_repository() returns configured true response #[tokio::test] async fn mock_is_repository_returns_true() { let mock = MockJjExecutor::new().with_is_repo_response(Ok(true)); let result = mock.is_repository().await; assert!(result.is_ok()); assert!(result.unwrap()); assert!(mock.was_is_repo_called()); } /// Test mock is_repository() returns configured false response #[tokio::test] async fn mock_is_repository_returns_false() { let mock = MockJjExecutor::new().with_is_repo_response(Ok(false)); let result = mock.is_repository().await; assert!(result.is_ok()); assert!(!result.unwrap()); } /// Test mock is_repository() returns configured error #[tokio::test] async fn mock_is_repository_returns_error() { let mock = MockJjExecutor::new().with_is_repo_response(Err(Error::NotARepository)); let result = mock.is_repository().await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), Error::NotARepository)); } /// Test mock describe() records the message #[tokio::test] async fn mock_describe_records_message() { let mock = MockJjExecutor::new(); let result = mock.describe("test message").await; assert!(result.is_ok()); let messages = mock.describe_messages(); assert_eq!(messages.len(), 1); assert_eq!(messages[0], "test message"); } /// Test mock describe() records multiple messages #[tokio::test] async fn mock_describe_records_multiple_messages() { let mock = MockJjExecutor::new(); mock.describe("first message").await.unwrap(); mock.describe("second message").await.unwrap(); let messages = mock.describe_messages(); assert_eq!(messages.len(), 2); assert_eq!(messages[0], "first message"); assert_eq!(messages[1], "second message"); } /// Test mock describe() returns configured error #[tokio::test] async fn mock_describe_returns_error() { let mock = MockJjExecutor::new().with_describe_response(Err(Error::RepositoryLocked)); let result = mock.describe("test").await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), Error::RepositoryLocked)); } /// Test mock describe() returns JjOperation error with context #[tokio::test] async fn mock_describe_returns_jj_operation_error() { let mock = MockJjExecutor::new().with_describe_response(Err(Error::JjOperation { context: "transaction failed".to_string(), })); let result = mock.describe("test").await; assert!(result.is_err()); match result.unwrap_err() { Error::JjOperation { context } => { assert_eq!(context, "transaction failed"); } _ => panic!("Expected JjOperation error"), } } /// Test mock can be used through trait object #[tokio::test] async fn mock_works_as_trait_object() { let mock = MockJjExecutor::new(); let executor: Box = Box::new(mock); let result = executor.is_repository().await; assert!(result.is_ok()); } /// Test mock can be used through trait reference #[tokio::test] async fn mock_works_as_trait_reference() { let mock = MockJjExecutor::new(); let executor: &dyn JjExecutor = &mock; let result = executor.is_repository().await; assert!(result.is_ok()); } /// Test mock satisfies Send + Sync bounds (compile-time check) /// /// JjExecutor requires Send + Sync on implementors even though returned /// futures are !Send (due to jj-lib internals). #[test] fn mock_is_send_sync() { fn assert_send() {} fn assert_sync() {} assert_send::(); assert_sync::(); } /// Test that empty message can be passed to describe #[tokio::test] async fn mock_describe_accepts_empty_message() { let mock = MockJjExecutor::new(); let result = mock.describe("").await; assert!(result.is_ok()); assert_eq!(mock.describe_messages()[0], ""); } /// Test that long message can be passed to describe #[tokio::test] async fn mock_describe_accepts_long_message() { let mock = MockJjExecutor::new(); let long_message = "a".repeat(1000); let result = mock.describe(&long_message).await; assert!(result.is_ok()); assert_eq!(mock.describe_messages()[0].len(), 1000); } /// Test mock tracks is_repository calls #[tokio::test] async fn mock_tracks_is_repository_call() { let mock = MockJjExecutor::new(); assert!(!mock.was_is_repo_called()); mock.is_repository().await.unwrap(); assert!(mock.was_is_repo_called()); } }