Files
jj-cz/src/jj/mock.rs

235 lines
7.6 KiB
Rust
Raw Normal View History

//! 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 calls to is_repository()
is_repo_called: AtomicBool,
/// Track calls to describe() with the message passed
describe_calls: Mutex<Vec<String>>,
}
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<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()
}
}
#[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, 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<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());
}
}