brlapi 0.4.1

Safe Rust bindings for the BrlAPI library
// SPDX-License-Identifier: LGPL-2.1

//! Error handling specific tests

use brlapi::{BrlApiError, Connection};

/// Test that connections handle failure cases gracefully
#[test]
fn test_connection_error_handling() {
    use brlapi::ConnectionSettings;

    // Test connection to invalid host (should fail)
    let bad_settings = ConnectionSettings::for_host("invalid-host-that-does-not-exist");

    match Connection::open_with_settings(Some(&bad_settings)) {
        Ok(_) => {
            // This is unexpected - the host should not exist
            println!("Connection to invalid host succeeded (unexpected)");
        }
        Err(e) => {
            println!("Connection to invalid host failed as expected: {e}");

            // Verify the error provides helpful information
            let suggestions = e.suggestions();

            println!("  Error message: {e}");
            println!("  Suggestions: {:?}", suggestions);

            // Should be categorized as a connection error
            assert!(e.is_connection_error() || e.is_operation_error());
        }
    }
}

/// Test error conversion and classification
#[test]
fn test_error_types() {
    // Test all error types have proper classification
    let errors_and_categories = vec![
        (BrlApiError::ConnectionRefused, "connection"),
        (BrlApiError::AuthenticationFailed, "connection"),
        (BrlApiError::TTYBusy, "resource"),
        (BrlApiError::DeviceBusy, "resource"),
        (BrlApiError::NoMem, "resource"),
        (BrlApiError::OperationNotSupported, "operation"),
        (BrlApiError::UnknownInstruction, "operation"),
        (BrlApiError::InvalidParameter, "operation"),
    ];

    for (error, category) in errors_and_categories {
        match category {
            "connection" => {
                assert!(error.is_connection_error());
                assert!(!error.is_resource_busy());
                assert!(!error.is_operation_error());
            }
            "resource" => {
                assert!(!error.is_connection_error());
                assert!(error.is_resource_busy());
                assert!(!error.is_operation_error());
            }
            "operation" => {
                assert!(!error.is_connection_error());
                assert!(!error.is_resource_busy());
                assert!(error.is_operation_error());
            }
            _ => panic!("Unknown category: {category}"),
        }

        // All errors should have non-empty string representation
        assert!(!error.to_string().is_empty());

        // All errors should have non-empty display messages (via thiserror)
        assert!(!error.to_string().is_empty());

        println!("Error {error:?} correctly classified as {category}");
    }
}

/// Test that error messages are helpful
#[test]
fn test_error_helpfulness() {
    let test_cases = vec![
        (
            BrlApiError::ConnectionRefused,
            vec!["BRLTTY", "systemctl", "daemon"],
        ),
        (
            BrlApiError::TTYBusy,
            vec!["TTY", "connection", "applications"],
        ),
        (BrlApiError::UnknownTTY, vec!["TTY", "console", "Ctrl+Alt"]),
        (
            BrlApiError::DriverError,
            vec!["display", "connection", "brltty"],
        ),
    ];

    for (error, keywords) in test_cases {
        let error_msg = error.to_string();
        let suggestions = error.suggestions();

        // Check that error message contains relevant keywords
        let msg_lower = error_msg.to_lowercase();
        for keyword in &keywords {
            if !msg_lower.contains(&keyword.to_lowercase()) {
                println!("Error message for {error:?} doesn't contain '{keyword}'");
                println!("   Message: {error_msg}");
            }
        }

        // Suggestions should be non-empty for common errors
        if suggestions.is_empty() {
            println!("No suggestions provided for {:?}", error);
        } else {
            println!("Error {error:?} has {} suggestions", suggestions.len());

            // Each suggestion should be actionable (contain a command or instruction)
            for suggestion in suggestions {
                assert!(!suggestion.is_empty());
                // Good suggestions typically contain colons (command:) or specific actions
                if !(suggestion.contains(':')
                    || suggestion.contains("sudo")
                    || suggestion.contains("systemctl")
                    || suggestion.contains("Check"))
                {
                    println!("Suggestion may not be actionable: {suggestion}");
                }
            }
        }
    }
}

/// Test error creation from C error codes
#[test]
fn test_error_from_c_codes() {
    // Test a few known error codes
    let test_cases = vec![
        (
            brlapi_sys::brlapi_error::BRLAPI_ERROR_SUCCESS,
            BrlApiError::Success,
        ),
        (
            brlapi_sys::brlapi_error::BRLAPI_ERROR_NOMEM,
            BrlApiError::NoMem,
        ),
        (
            brlapi_sys::brlapi_error::BRLAPI_ERROR_TTYBUSY,
            BrlApiError::TTYBusy,
        ),
        (
            brlapi_sys::brlapi_error::BRLAPI_ERROR_UNKNOWNTTY,
            BrlApiError::UnknownTTY,
        ),
    ];

    for (c_code, expected_error) in test_cases {
        let converted = BrlApiError::from_c_error(c_code as i32);

        // Check that we get the expected error type
        assert_eq!(
            std::mem::discriminant(&converted),
            std::mem::discriminant(&expected_error)
        );
        println!("C error code {c_code} correctly converts to {converted:?}");
    }

    // Test unknown error code falls back to OperationNotSupported
    let unknown_error = BrlApiError::from_c_error(99999);
    assert!(matches!(unknown_error, BrlApiError::OperationNotSupported));
    println!("Unknown error codes fall back to OperationNotSupported");
}