376 lines
12 KiB
Rust
376 lines
12 KiB
Rust
//! Mock implementation of [`Prompter`] for testing
|
|
//!
|
|
//! This module is gated via `#[cfg(any(test, feature = "test-utils"))]` on its
|
|
//! declaration in `mod.rs`, so it is never compiled into production binaries.
|
|
//!
|
|
//! [`Prompter`]: super::prompter::Prompter
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use crate::{
|
|
commit::types::{BreakingChange, CommitType, Description, Scope},
|
|
error::Error,
|
|
prompts::prompter::Prompter,
|
|
};
|
|
|
|
/// Enum representing different types of mock responses
|
|
#[derive(Debug)]
|
|
enum MockResponse {
|
|
CommitType(CommitType),
|
|
Scope(Scope),
|
|
Description(Description),
|
|
BreakingChange(BreakingChange),
|
|
Confirm(bool),
|
|
Error(Error),
|
|
}
|
|
|
|
/// Mock implementation of [`Prompter`] for testing
|
|
///
|
|
/// This struct allows configuring responses for each prompt type and tracks
|
|
/// which prompts were called during test execution.
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct MockPrompts {
|
|
/// Queue of responses to return for each prompt call
|
|
responses: Arc<Mutex<Vec<MockResponse>>>,
|
|
/// Track which prompts were called (for verification)
|
|
prompts_called: Arc<Mutex<Vec<String>>>,
|
|
/// Messages emitted via emit_message() for test assertion
|
|
messages: Arc<Mutex<Vec<String>>>,
|
|
}
|
|
|
|
impl MockPrompts {
|
|
/// Create a new MockPrompts with empty response queue
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Configure the mock to return a specific commit type
|
|
pub fn with_commit_type(self, commit_type: CommitType) -> Self {
|
|
self.responses
|
|
.lock()
|
|
.unwrap()
|
|
.push(MockResponse::CommitType(commit_type));
|
|
self
|
|
}
|
|
|
|
/// Configure the mock to return a specific scope
|
|
pub fn with_scope(self, scope: Scope) -> Self {
|
|
self.responses
|
|
.lock()
|
|
.unwrap()
|
|
.push(MockResponse::Scope(scope));
|
|
self
|
|
}
|
|
|
|
/// Configure the mock to return a specific description
|
|
pub fn with_description(self, description: Description) -> Self {
|
|
self.responses
|
|
.lock()
|
|
.unwrap()
|
|
.push(MockResponse::Description(description));
|
|
self
|
|
}
|
|
|
|
/// Configure the mock to return a specific breaking change response
|
|
pub fn with_breaking_change(self, breaking_change: BreakingChange) -> Self {
|
|
self.responses
|
|
.lock()
|
|
.unwrap()
|
|
.push(MockResponse::BreakingChange(breaking_change));
|
|
self
|
|
}
|
|
|
|
/// Configure the mock to return a specific confirmation response
|
|
pub fn with_confirm(self, confirm: bool) -> Self {
|
|
self.responses
|
|
.lock()
|
|
.unwrap()
|
|
.push(MockResponse::Confirm(confirm));
|
|
self
|
|
}
|
|
|
|
/// Configure the mock to return an error
|
|
pub fn with_error(self, error: Error) -> Self {
|
|
self.responses
|
|
.lock()
|
|
.unwrap()
|
|
.push(MockResponse::Error(error));
|
|
self
|
|
}
|
|
|
|
/// Check if select_commit_type was called
|
|
pub fn was_commit_type_called(&self) -> bool {
|
|
self.prompts_called
|
|
.lock()
|
|
.unwrap()
|
|
.contains(&"select_commit_type".to_string())
|
|
}
|
|
|
|
/// Check if input_scope was called
|
|
pub fn was_scope_called(&self) -> bool {
|
|
self.prompts_called
|
|
.lock()
|
|
.unwrap()
|
|
.contains(&"input_scope".to_string())
|
|
}
|
|
|
|
/// Check if input_description was called
|
|
pub fn was_description_called(&self) -> bool {
|
|
self.prompts_called
|
|
.lock()
|
|
.unwrap()
|
|
.contains(&"input_description".to_string())
|
|
}
|
|
|
|
/// Check if input_breaking_change was called
|
|
pub fn was_breaking_change_called(&self) -> bool {
|
|
self.prompts_called
|
|
.lock()
|
|
.unwrap()
|
|
.contains(&"input_breaking_change".to_string())
|
|
}
|
|
|
|
/// Check if confirm_apply was called
|
|
pub fn was_confirm_called(&self) -> bool {
|
|
self.prompts_called
|
|
.lock()
|
|
.unwrap()
|
|
.contains(&"confirm_apply".to_string())
|
|
}
|
|
|
|
/// Get all messages emitted via emit_message()
|
|
pub fn emitted_messages(&self) -> Vec<String> {
|
|
self.messages.lock().unwrap().clone()
|
|
}
|
|
}
|
|
|
|
impl Prompter for MockPrompts {
|
|
fn select_commit_type(&self) -> Result<CommitType, Error> {
|
|
self.prompts_called
|
|
.lock()
|
|
.unwrap()
|
|
.push("select_commit_type".to_string());
|
|
|
|
match self.responses.lock().unwrap().remove(0) {
|
|
MockResponse::CommitType(ct) => Ok(ct),
|
|
MockResponse::Error(e) => Err(e),
|
|
_ => panic!("MockPrompts: Expected CommitType response, got different type"),
|
|
}
|
|
}
|
|
|
|
fn input_scope(&self) -> Result<Scope, Error> {
|
|
self.prompts_called
|
|
.lock()
|
|
.unwrap()
|
|
.push("input_scope".to_string());
|
|
|
|
match self.responses.lock().unwrap().remove(0) {
|
|
MockResponse::Scope(scope) => Ok(scope),
|
|
MockResponse::Error(e) => Err(e),
|
|
_ => panic!("MockPrompts: Expected Scope response, got different type"),
|
|
}
|
|
}
|
|
|
|
fn input_description(&self) -> Result<Description, Error> {
|
|
self.prompts_called
|
|
.lock()
|
|
.unwrap()
|
|
.push("input_description".to_string());
|
|
|
|
match self.responses.lock().unwrap().remove(0) {
|
|
MockResponse::Description(desc) => Ok(desc),
|
|
MockResponse::Error(e) => Err(e),
|
|
_ => panic!("MockPrompts: Expected Description response, got different type"),
|
|
}
|
|
}
|
|
|
|
fn input_breaking_change(&self) -> Result<BreakingChange, Error> {
|
|
self.prompts_called
|
|
.lock()
|
|
.unwrap()
|
|
.push("input_breaking_change".to_string());
|
|
|
|
match self.responses.lock().unwrap().remove(0) {
|
|
MockResponse::BreakingChange(bc) => Ok(bc),
|
|
MockResponse::Error(e) => Err(e),
|
|
_ => panic!("MockPrompts: Expected BreakingChange response, got different type"),
|
|
}
|
|
}
|
|
|
|
fn confirm_apply(&self, _message: &str) -> Result<bool, Error> {
|
|
self.prompts_called
|
|
.lock()
|
|
.unwrap()
|
|
.push("confirm_apply".to_string());
|
|
|
|
match self.responses.lock().unwrap().remove(0) {
|
|
MockResponse::Confirm(confirm) => Ok(confirm),
|
|
MockResponse::Error(e) => Err(e),
|
|
_ => panic!("MockPrompts: Expected Confirm response, got different type"),
|
|
}
|
|
}
|
|
|
|
fn emit_message(&self, msg: &str) {
|
|
self.messages.lock().unwrap().push(msg.to_string());
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::commit::types::{CommitType, Description, Scope};
|
|
|
|
#[test]
|
|
fn mock_prompts_creation() {
|
|
let mock = MockPrompts::new();
|
|
assert!(matches!(mock, MockPrompts { .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn mock_prompts_implements_trait() {
|
|
let mock = MockPrompts::new();
|
|
fn _accepts_prompter(_p: impl Prompter) {}
|
|
_accepts_prompter(mock);
|
|
}
|
|
|
|
#[test]
|
|
fn mock_select_commit_type() {
|
|
let mock = MockPrompts::new().with_commit_type(CommitType::Feat);
|
|
let result = mock.select_commit_type();
|
|
assert!(result.is_ok());
|
|
assert_eq!(result.unwrap(), CommitType::Feat);
|
|
assert!(mock.was_commit_type_called());
|
|
}
|
|
|
|
#[test]
|
|
fn mock_input_scope() {
|
|
let scope = Scope::parse("test-scope").unwrap();
|
|
let mock = MockPrompts::new().with_scope(scope.clone());
|
|
let result = mock.input_scope();
|
|
assert!(result.is_ok());
|
|
assert_eq!(result.unwrap(), scope);
|
|
assert!(mock.was_scope_called());
|
|
}
|
|
|
|
#[test]
|
|
fn mock_input_description() {
|
|
let desc = Description::parse("test description").unwrap();
|
|
let mock = MockPrompts::new().with_description(desc.clone());
|
|
let result = mock.input_description();
|
|
assert!(result.is_ok());
|
|
assert_eq!(result.unwrap(), desc);
|
|
assert!(mock.was_description_called());
|
|
}
|
|
|
|
#[test]
|
|
fn mock_confirm_apply() {
|
|
let mock = MockPrompts::new().with_confirm(true);
|
|
let result = mock.confirm_apply("test message");
|
|
assert!(result.is_ok());
|
|
assert!(result.unwrap());
|
|
assert!(mock.was_confirm_called());
|
|
}
|
|
|
|
#[test]
|
|
fn mock_error_response() {
|
|
let mock = MockPrompts::new().with_error(Error::Cancelled);
|
|
let result = mock.select_commit_type();
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), Error::Cancelled));
|
|
}
|
|
|
|
#[test]
|
|
fn mock_tracks_prompt_calls() {
|
|
let mock = MockPrompts::new()
|
|
.with_commit_type(CommitType::Fix)
|
|
.with_scope(Scope::empty())
|
|
.with_description(Description::parse("test").unwrap())
|
|
.with_confirm(true);
|
|
|
|
mock.select_commit_type().unwrap();
|
|
mock.input_scope().unwrap();
|
|
mock.input_description().unwrap();
|
|
mock.confirm_apply("test").unwrap();
|
|
|
|
assert!(mock.was_commit_type_called());
|
|
assert!(mock.was_scope_called());
|
|
assert!(mock.was_description_called());
|
|
assert!(mock.was_confirm_called());
|
|
}
|
|
|
|
#[test]
|
|
fn mock_emit_message_records_messages() {
|
|
let mock = MockPrompts::new();
|
|
mock.emit_message("hello");
|
|
mock.emit_message("world");
|
|
let msgs = mock.emitted_messages();
|
|
assert_eq!(msgs, vec!["hello", "world"]);
|
|
}
|
|
|
|
#[test]
|
|
fn mock_emit_message_starts_empty() {
|
|
let mock = MockPrompts::new();
|
|
assert!(mock.emitted_messages().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn mock_input_breaking_change_no() {
|
|
let mock = MockPrompts::new().with_breaking_change(BreakingChange::No);
|
|
let result = mock.input_breaking_change();
|
|
assert!(result.is_ok());
|
|
assert_eq!(result.unwrap(), BreakingChange::No);
|
|
assert!(mock.was_breaking_change_called());
|
|
}
|
|
|
|
#[test]
|
|
fn mock_input_breaking_change_yes_no_note() {
|
|
let mock = MockPrompts::new().with_breaking_change(BreakingChange::Yes);
|
|
let result = mock.input_breaking_change();
|
|
assert!(result.is_ok());
|
|
assert_eq!(result.unwrap(), BreakingChange::Yes);
|
|
assert!(mock.was_breaking_change_called());
|
|
}
|
|
|
|
#[test]
|
|
fn mock_input_breaking_change_yes_with_note() {
|
|
let mock = MockPrompts::new().with_breaking_change("removes old API".into());
|
|
let result = mock.input_breaking_change();
|
|
assert!(result.is_ok());
|
|
assert_eq!(
|
|
result.unwrap(),
|
|
BreakingChange::WithNote("removes old API".into())
|
|
);
|
|
assert!(mock.was_breaking_change_called());
|
|
}
|
|
|
|
#[test]
|
|
fn mock_input_breaking_change_error() {
|
|
let mock = MockPrompts::new().with_error(Error::Cancelled);
|
|
let result = mock.input_breaking_change();
|
|
assert!(result.is_err());
|
|
assert!(matches!(result.unwrap_err(), Error::Cancelled));
|
|
}
|
|
|
|
#[test]
|
|
fn mock_tracks_breaking_change_call() {
|
|
let mock = MockPrompts::new()
|
|
.with_commit_type(CommitType::Fix)
|
|
.with_scope(Scope::empty())
|
|
.with_description(Description::parse("test").unwrap())
|
|
.with_breaking_change(BreakingChange::No)
|
|
.with_confirm(true);
|
|
|
|
mock.select_commit_type().unwrap();
|
|
mock.input_scope().unwrap();
|
|
mock.input_description().unwrap();
|
|
mock.input_breaking_change().unwrap();
|
|
mock.confirm_apply("test").unwrap();
|
|
|
|
assert!(mock.was_commit_type_called());
|
|
assert!(mock.was_scope_called());
|
|
assert!(mock.was_description_called());
|
|
assert!(mock.was_breaking_change_called());
|
|
assert!(mock.was_confirm_called());
|
|
}
|
|
}
|