Replace CLI executor with jj-lib integration, implement full interactive commit workflow via prompts, and add mock infrastructure for testing. Add CLI integration tests and error handling tests.
235 lines
7.6 KiB
Rust
235 lines
7.6 KiB
Rust
//! 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());
|
|
}
|
|
}
|