jj-cz 1.1.0

Conventional commits for Jujutsu
Documentation
//! Comprehensive tests for error handling
//!
//! These tests ensure all error variants are properly handled
//! and that error conversions work correctly.

use jj_cz::{CommitMessageError, DescriptionError, Error, ScopeError};

/// Test that all error variants can be created and displayed
#[test]
fn test_all_error_variants() {
    // Domain errors
    let invalid_scope = Error::InvalidScope("test".to_string());
    let _invalid_desc = Error::InvalidDescription("test".to_string());
    let _invalid_msg = Error::InvalidCommitMessage("test".to_string());

    // Infrastructure errors
    let not_repo = Error::NotARepository;
    let _jj_op = Error::JjOperation {
        context: "test".to_string(),
    };
    let _repo_locked = Error::RepositoryLocked;
    let _failed_dir = Error::FailedGettingCurrentDir;
    let _failed_config = Error::FailedReadingConfig {
        context: "test".to_string(),
    };

    // Application errors
    let cancelled = Error::Cancelled;
    let _non_interactive = Error::NonInteractive;

    // Verify all variants can be displayed
    assert_eq!(format!("{}", invalid_scope), "Invalid scope: test");
    assert_eq!(format!("{}", not_repo), "Not a Jujutsu repository");
    assert_eq!(format!("{}", cancelled), "Operation cancelled by user");
}

/// Test error conversions from domain types
#[test]
fn test_error_conversions() {
    // ScopeError -> Error::InvalidScope
    let scope_err = ScopeError::TooLong {
        actual: 31,
        max: 30,
    };
    let error: Error = scope_err.into();
    assert!(matches!(error, Error::InvalidScope(_)));

    // DescriptionError -> Error::InvalidDescription
    let desc_err = DescriptionError::Empty;
    let error: Error = desc_err.into();
    assert!(matches!(error, Error::InvalidDescription(_)));

    // CommitMessageError -> Error::InvalidCommitMessage
    let msg_err = CommitMessageError::FirstLineTooLong {
        actual: 73,
        max: 72,
    };
    let error: Error = msg_err.into();
    assert!(matches!(error, Error::InvalidCommitMessage(_)));
}

/// Test error equality and partial equality
#[test]
fn test_error_equality() {
    let err1 = Error::NotARepository;
    let err2 = Error::NotARepository;
    assert_eq!(err1, err2);

    let err3 = Error::Cancelled;
    assert_ne!(err1, err3);
}

/// Test error debugging
#[test]
fn test_error_debug() {
    let error = Error::JjOperation {
        context: "test operation".to_string(),
    };
    let debug_str = format!("{:?}", error);
    assert!(debug_str.contains("JjOperation"));
    assert!(debug_str.contains("test operation"));
}

/// Test error cloning
#[test]
fn test_error_clone() {
    let original = Error::JjOperation {
        context: "original".to_string(),
    };
    let cloned = original.clone();
    assert_eq!(original, cloned);
}

/// Test error send and sync traits
#[test]
fn test_error_send_sync() {
    fn assert_send<T: Send>() {}
    fn assert_sync<T: Sync>() {}

    let _error = Error::NotARepository;
    assert_send::<Error>();
    assert_sync::<Error>();

    // Test with owned data
    let _owned_error = Error::JjOperation {
        context: "test".to_string(),
    };
    assert_send::<Error>();
    assert_sync::<Error>();
}

/// Test error matching patterns
#[test]
fn test_error_matching() {
    let error = Error::Cancelled;

    match error {
        Error::Cancelled => {}
        Error::NotARepository => panic!("Should not match"),
        Error::JjOperation { context } => panic!("Should not match: {}", context),
        _ => panic!("Should not match other variants"),
    }
}

/// Test error context extraction
#[test]
fn test_jj_operation_context() {
    let error = Error::JjOperation {
        context: "repository locked".to_string(),
    };

    if let Error::JjOperation { context } = error {
        assert_eq!(context, "repository locked");
    } else {
        panic!("Expected JjOperation variant");
    }
}

/// Test conversion from std::io::Error
#[test]
fn test_from_io_error() {
    let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
    let error: Error = io_err.into();
    assert!(matches!(error, Error::FailedGettingCurrentDir));
}

/// Test conversion from std::sync::PoisonError
#[test]
fn test_from_poison_error() {
    let mutex = std::sync::Mutex::new(());
    // Poison the mutex by panicking while holding the lock
    let poison_err = std::panic::catch_unwind(|| {
        let _guard = mutex.lock().unwrap();
        panic!("deliberate panic");
    });
    assert!(poison_err.is_err());

    // Now lock should fail with PoisonError
    let result = mutex.lock();
    assert!(result.is_err());

    let error: Error = result.unwrap_err().into();
    assert!(matches!(error, Error::JjOperation { .. }));
    assert_eq!(
        format!("{}", error),
        "Repository operation failed: internal lock poisoned"
    );
}

/// Test from_revset_evaluation_error constructs RevsetResolutionError
#[test]
fn test_from_revset_evaluation_error() {
    let underlying = std::io::Error::other("store failure");
    let eval_err = jj_lib::revset::RevsetEvaluationError::Other(Box::new(underlying));
    let error = Error::from_revset_evaluation_error("@", eval_err);
    assert!(matches!(error, Error::RevsetResolutionError { .. }));
    let description = format!("{}", error);
    assert!(description.contains("@"));
    assert!(description.contains("store failure"));
}

/// Test from_revset_resolution_error constructs RevsetResolutionError
#[test]
fn test_from_revset_resolution_error() {
    let resolution_err = jj_lib::revset::RevsetResolutionError::NoSuchRevision {
        name: "nonexistent".to_string(),
        candidates: Vec::new(),
    };
    let error = Error::from_revset_resolution_error("@", resolution_err);
    assert!(matches!(error, Error::RevsetResolutionError { .. }));
    let description = format!("{}", error);
    assert!(description.contains("@"));
    assert!(description.contains("nonexistent"));
}

/// Test NewFlagWithMultipleRevisions error display
#[test]
fn test_new_flag_with_multiple_revisions() {
    let error = Error::NewFlagWithMultipleRevisions;
    assert_eq!(
        format!("{}", error),
        "--new cannot be used with multiple revisions"
    );
}

/// Test NonInteractive error display
#[test]
fn test_non_interactive() {
    let error = Error::NonInteractive;
    assert_eq!(format!("{}", error), "Non-interactive terminal detected");
}

/// Test FailedReadingConfig error display
#[test]
fn test_failed_reading_config() {
    let error = Error::FailedReadingConfig {
        context: "config parse error".to_string(),
    };
    let description = format!("{}", error);
    assert!(description.contains("config parse error"));
}

/// Test MultipleRevisions error display
#[test]
fn test_multiple_revisions() {
    let error = Error::MultipleRevisions {
        revset: "abc | def".to_string(),
    };
    let description = format!("{}", error);
    assert!(description.contains("abc | def"));
    assert!(description.contains("multiple commits"));
}

/// Test RepositoryLocked error display
#[test]
fn test_repository_locked() {
    let error = Error::RepositoryLocked;
    assert_eq!(
        format!("{}", error),
        "Repository is locked by another process"
    );
}

/// Test FailedGettingCurrentDir error display
#[test]
fn test_failed_getting_current_dir() {
    let error = Error::FailedGettingCurrentDir;
    assert_eq!(format!("{}", error), "Could not get current directory");
}

/// Test error matching on all variants
#[test]
fn test_error_matching_all_variants() {
    let variants: Vec<Error> = vec![
        Error::InvalidScope("s".into()),
        Error::InvalidDescription("d".into()),
        Error::InvalidCommitMessage("m".into()),
        Error::NotARepository,
        Error::JjOperation {
            context: "c".into(),
        },
        Error::RepositoryLocked,
        Error::FailedGettingCurrentDir,
        Error::FailedReadingConfig {
            context: "c".into(),
        },
        Error::Cancelled,
        Error::NonInteractive,
        Error::RevsetResolutionError {
            revset: "@".into(),
            context: "c".into(),
        },
        Error::MultipleRevisions { revset: "@".into() },
        Error::NewFlagWithMultipleRevisions,
    ];

    // All variants should be displayable without panicking
    for variant in &variants {
        let _ = format!("{}", variant);
        let _ = format!("{:?}", variant);
    }

    // Verify all variants can be cloned
    let cloned: Vec<Error> = variants.to_vec();
    assert_eq!(variants.len(), cloned.len());
    for (original, clone) in variants.iter().zip(cloned.iter()) {
        assert_eq!(original, clone);
    }
}