jj-cz 1.1.0

Conventional commits for Jujutsu
Documentation
//! 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<bool, Error>,
    /// Response to return from `describe()`
    describe_response: Result<(), Error>,
    /// Track described revsets
    described_revsets: Mutex<Vec<String>>,
    /// Track response to return from `get_description()`
    get_description_response: Result<String, Error>,
    /// Track calls to `is_repository()`
    is_repo_called: AtomicBool,
    /// Track calls to `describe()` with the message passed
    describe_calls: Mutex<Vec<String>>,
    /// Track response to return from `new_revision()`
    new_revision_response: Result<(), Error>,
    /// Track calls to `new_revision()`
    new_revision_calls: Mutex<Vec<String>>,
}

impl Default for MockJjExecutor {
    fn default() -> Self {
        Self {
            is_repo_response: Ok(true),
            describe_response: Ok(()),
            described_revsets: Mutex::new(Vec::new()),
            get_description_response: Ok(String::new()),
            is_repo_called: AtomicBool::new(false),
            describe_calls: Mutex::new(Vec::new()),
            new_revision_response: Ok(()),
            new_revision_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<bool, Error>) -> 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<String> {
        self.describe_calls.lock().unwrap().clone()
    }

    pub fn with_new_revision_response(mut self, response: Result<(), Error>) -> Self {
        self.new_revision_response = response;
        self
    }

    pub fn new_revision_calls(&self) -> Vec<String> {
        self.new_revision_calls.lock().unwrap().clone()
    }
}

#[async_trait(?Send)]
impl JjExecutor for MockJjExecutor {
    async fn is_repository(&self) -> Result<bool, Error> {
        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, revset: &str, message: &str) -> Result<(), Error> {
        self.described_revsets
            .lock()
            .unwrap()
            .push(revset.to_string());
        self.describe_calls
            .lock()
            .unwrap()
            .push(message.to_string());
        match &self.describe_response {
            Ok(()) => Ok(()),
            Err(e) => Err(e.clone()),
        }
    }

    async fn get_description(&self, _revset: &str) -> Result<String, Error> {
        self.get_description_response.clone()
    }

    async fn new_revision(&self, revset: &str) -> Result<(), Error> {
        self.new_revision_calls
            .lock()
            .unwrap()
            .push(revset.to_string());
        match &self.new_revision_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<dyn JjExecutor> = 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<T: Send>() {}
        fn assert_sync<T: Sync>() {}
        assert_send::<MockJjExecutor>();
        assert_sync::<MockJjExecutor>();
    }

    /// 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());
    }

    /// Test mock new_revision() records the revset
    #[tokio::test]
    async fn mock_new_revision_records_revset() {
        let mock = MockJjExecutor::new();
        let result = mock.new_revision("@").await;
        assert!(result.is_ok());
        let calls = mock.new_revision_calls();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0], "@");
    }

    /// Test mock new_revision() records multiple calls
    #[tokio::test]
    async fn mock_new_revision_records_multiple_calls() {
        let mock = MockJjExecutor::new();
        mock.new_revision("@").await.unwrap();
        mock.new_revision("abc").await.unwrap();
        mock.new_revision("xyz").await.unwrap();
        let calls = mock.new_revision_calls();
        assert_eq!(calls.len(), 3);
        assert_eq!(calls[0], "@");
        assert_eq!(calls[1], "abc");
        assert_eq!(calls[2], "xyz");
    }

    /// Test mock new_revision() returns configured error
    #[tokio::test]
    async fn mock_new_revision_returns_error() {
        let mock = MockJjExecutor::new().with_new_revision_response(Err(Error::RepositoryLocked));
        let result = mock.new_revision("@").await;
        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), Error::RepositoryLocked));
    }

    /// Test mock new_revision() records revset even on error
    #[tokio::test]
    async fn mock_new_revision_records_revset_on_error() {
        let mock = MockJjExecutor::new().with_new_revision_response(Err(Error::JjOperation {
            context: "failed".to_string(),
        }));
        let result = mock.new_revision("abc").await;
        assert!(result.is_err());
        let calls = mock.new_revision_calls();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0], "abc");
    }

    /// Test mock new_revision() can be inspected after success
    #[tokio::test]
    async fn mock_new_revision_returns_ok_and_tracks_revset() {
        let mock = MockJjExecutor::new();
        let result = mock.new_revision("my-feature").await;
        assert!(result.is_ok());
        let calls = mock.new_revision_calls();
        assert_eq!(calls, vec!["my-feature"]);
    }
}