luhtwin 0.1.4

A beta horrible Rust error handling library with AnyError and context macros
Documentation
use super::*;

use std::collections::HashMap;
use std::io;
use std::error::Error as StdError;

fn root_cause<'a>(err: &'a (dyn StdError + 'static)) -> &'a (dyn StdError + 'static) {
    let mut current = err;
    while let Some(source) = current.source() {
        current = source;
    }
    current
}

#[test]
fn context_adds_message_and_preserves_source() {
    let err: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::Other, "file missing"));
    let result = err.context("reading config");

    assert!(result.is_err());
    let e = result.unwrap_err();

    assert_eq!(e.to_string(), "reading config");

    assert_eq!(e.source().unwrap().to_string(), "file missing");
}

#[test]
fn chained_context_produces_nested_sources() {
    let err: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::Other, "root"));
    let result = err
        .context("loading data")
        .context("initializing system");

    assert!(result.is_err());
    let e = result.unwrap_err();

    assert_eq!(e.to_string(), "initializing system");

    let src1 = e.source().unwrap();
    assert_eq!(src1.to_string(), "loading data");

    let src2 = src1.source().unwrap();
    assert_eq!(src2.to_string(), "root");
}

#[test]
fn context_chain_root_cause_is_original_error() {
    let err: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::Other, "disk full"));
    let result = err
        .context("saving file")
        .context("processing upload")
        .context("user request");

    let e = result.unwrap_err();
    let cause = root_cause(&e);
    assert_eq!(cause.to_string(), "disk full");
}

#[test]
fn map_err_produces_combined_message() {
    let err: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::Other, "low memory"));
    let result = err.wrap(|| "while running benchmark");

    assert!(result.is_err());
    let e = result.unwrap_err();
    let msg = e.to_string();
    
    assert!(msg.contains("while running benchmark"));
    assert!(msg.contains("low memory"));
}

#[test]
fn bail_macro_returns_early_with_message() {
    fn fail() -> LuhTwin<()> {
        bail!("fatal error occurred");
    }

    let result = fail();
    assert!(result.is_err());
    assert_eq!(result.unwrap_err().to_string(), "fatal error occurred");
}

#[test]
fn bail_macro_with_format_args() {
    fn fail_with_data(x: i32) -> LuhTwin<()> {
        bail!("invalid number: {}", x);
    }

    let result = fail_with_data(99);
    assert!(result.is_err());
    assert_eq!(result.unwrap_err().to_string(), "invalid number: 99");
}

#[test]
fn ensure_macro_allows_valid_condition() {
    fn check_positive(x: i32) -> LuhTwin<()> {
        ensure!(x > 0, "expected positive, got {}", x);
        Ok(())
    }

    let ok = check_positive(10);
    assert!(ok.is_ok());
}

#[test]
fn ensure_macro_bails_on_failure() {
    fn check_positive(x: i32) -> LuhTwin<()> {
        ensure!(x > 0, "expected positive, got {}", x);
        Ok(())
    }

    let err = check_positive(-5);
    
    assert!(err.is_err());
    assert_eq!(err.unwrap_err().to_string(), "expected positive, got -5");
}

#[test]
fn context_and_macros_mix_well() {
    fn process_file() -> LuhTwin<()> {
        let file: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::Other, "IO fail"));
        file.context("opening file")?;
        Ok(())
    }

    let result = process_file();
    assert!(result.is_err());
    let e = result.unwrap_err();
    assert_eq!(e.to_string(), "opening file");
    assert_eq!(e.source().unwrap().to_string(), "IO fail");
}

#[test]
fn ensure_and_bail_can_coexist() {
    fn maybe_fail(x: i32) -> LuhTwin<()> {
        ensure!(x != 0, "zero not allowed");
        if x == 42 {
            bail!("meaning of life error");
        }
        Ok(())
    }

    let zero = maybe_fail(0);
    assert!(zero.is_err());
    assert_eq!(zero.unwrap_err().to_string(), "zero not allowed");

    let meaning = maybe_fail(42);
    assert!(meaning.is_err());
    assert_eq!(meaning.unwrap_err().to_string(), "meaning of life error");

    let ok = maybe_fail(7);
    assert!(ok.is_ok());
}

use std::thread;
use std::sync::Arc;

#[test]
fn anyerror_is_send_and_sync() {
    fn assert_send_sync<T: Send + Sync>() {}
    assert_send_sync::<AnyError>();
}

#[test]
fn anyerror_can_be_sent_across_threads() {
    let err = AnyError::new(std::io::Error::new(std::io::ErrorKind::Other, "thread error"))
        .with_context(at!("from worker thread"));

    let shared = Arc::new(err);
    let thread_err = shared.clone();

    let handle = thread::spawn(move || {
        assert_eq!(thread_err.to_string(), "from worker thread");
        assert!(thread_err.source().unwrap().to_string().contains("thread error"));
    });

    handle.join().expect("thread should finish");
}

#[test]
fn anyerror_without_message_defaults_to_unknown() {
    let e = AnyError { contexts: vec!(), source: None, backtrace: Backtrace::capture() };
    assert_eq!(e.to_string(), "unknown error");
    assert!(e.source().is_none());
}

#[test]
fn anyerror_with_source_but_no_message_displays_source() {
    let inner = std::io::Error::new(std::io::ErrorKind::Other, "inner cause");
    let e = AnyError::new(inner);
    assert_eq!(e.to_string(), "inner cause");
}

#[derive(Debug)]
struct CustomErr;
impl fmt::Display for CustomErr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "custom error occurred")
    }
}
impl Error for CustomErr {}

#[test]
fn context_works_with_custom_non_io_error() {
    let err: Result<(), CustomErr> = Err(CustomErr);
    let result = err.context("running plugin");
    let e = result.unwrap_err();
    assert_eq!(e.to_string(), "running plugin");
    assert_eq!(e.source().unwrap().to_string(), "custom error occurred");
}

#[test]
fn display_shows_top_message_only() {
    let err: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::Other, "root failure"));
    let e = err.context("loading config").context("starting system").unwrap_err();
    assert_eq!(e.to_string(), "starting system");
}

#[test]
fn root_cause_finds_deepest_source() {
    let err: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::Other, "deep root"));
    let result = err.context("layer 1").context("layer 2").context("layer 3");
    let e = result.unwrap_err();
    let cause = root_cause(&e);
    assert_eq!(cause.to_string(), "deep root");
}

#[test]
fn map_err_context_handles_empty_message() {
    let err: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::Other, "missing data"));
    let result = err.wrap(|| "");
    let e = result.unwrap_err();
    assert!(e.to_string().contains("missing data"));
}

#[test]
fn with_context_closure_is_lazy() {
    let mut called = false;
    let err: Result<(), io::Error> = Ok(());
    let _ = err.with_context(|| {
        called = true;
        "this should not be called"
    });
    assert!(!called, "closure should not be called on Ok");
}

#[test]
fn with_context_closure_is_called_on_err() {
    let mut called = false;
    let err: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::Other, "fail"));
    let _ = err.with_context(|| {
        called = true;
        "context added"
    });
    assert!(called, "closure should be called on Err");
}

#[test]
fn iter_sources_traverses_entire_chain() {
    let err: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::Other, "root"));
    let e = err.context("layer1").context("layer2").unwrap_err();
    
    let sources: Vec<String> = e.iter_sources().map(|s| s.to_string()).collect();
    assert_eq!(sources.len(), 3);
    assert_eq!(sources[0], "layer2");
    assert_eq!(sources[1], "layer1");
    assert_eq!(sources[2], "root");
}

#[test]
fn root_cause_on_anyerror_directly() {
    let err: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::Other, "original"));
    let e = err.context("wrapper").unwrap_err();
    
    let root = e.root_cause();
    assert_eq!(root.to_string(), "original");
}

#[test]
fn multiple_contexts_preserve_order() {
    let err = AnyError::new(io::Error::new(io::ErrorKind::Other, "root"))
        .with_context(at!("first"))
        .with_context(at!("second"))
        .with_context(at!("third"));
    
    assert_eq!(err.contexts.len(), 3);
    assert_eq!(err.contexts[0].message, "first");
    assert_eq!(err.contexts[1].message, "second");
    assert_eq!(err.contexts[2].message, "third");
}

#[test]
fn from_implementations_work() {
    let io_err = io::Error::new(io::ErrorKind::Other, "io");
    let any_err: AnyError = io_err.into();
    assert!(any_err.source().is_some());
    
    let fmt_err = std::fmt::Error;
    let any_err: AnyError = fmt_err.into();
    assert!(any_err.source().is_some());
}

#[test]
fn ensure_macro_with_complex_conditions() {
    fn validate(x: i32, y: i32) -> LuhTwin<()> {
        ensure!(x > 0 && y > 0, "both values must be positive: x={}, y={}", x, y);
        ensure!(x < y, "x must be less than y: {} >= {}", x, y);
        Ok(())
    }
    
    assert!(validate(5, 10).is_ok());
    assert!(validate(-1, 10).is_err());
    assert!(validate(10, 5).is_err());
}

#[test]
fn print_error_formats_demonstration() {
    println!("\n=== ERROR FORMAT DEMONSTRATION ===\n");
    
    let err: Result<(), io::Error> = Err(io::Error::new(io::ErrorKind::NotFound, "config.json not found"));
    let mut meta = HashMap::new();
    meta.insert("version", "1.0.0");
    meta.insert("environment", "production");
    let err_with_metadata = err
        .context("failed to load configuration")
        .unwrap_err()
        .with_context(
            at!("application startup failed")
                .attach("doc link", "https://docs.example.com/startup-errors")
                .attach("issues", vec!["#123", "#456"])
                .attach("metadata", meta)
        );
    
    println!("{:?}\n", err_with_metadata);
    
    println!("=== END DEMONSTRATION ===\n");
}

fn test_wrap_luhtwin_helper() -> LuhTwin<()> {
    let err: LuhTwin<()> = Err(io::Error::new(io::ErrorKind::NotFound, "config.json not found"))
        .wrap(|| "failed to get config.json");

    err
}

#[test]
fn test_wrap_luhtwin() {
    let err = test_wrap_luhtwin_helper()
        .encase(|| "luhtwin helper error");

    println!("{:?}", err)
}

#[test]
fn error_context_display_with_all_fields() {
    let mut meta = HashMap::new();
    meta.insert("version", "1.0.0");
    meta.insert("environment", "production");
    let ctx = ErrorContext::new("test error")
        .file("main.rs")
        .line(42)
        .attach("doc link", "https://example.com/docs")
        .attach("issues", vec!["issue-1", "issue-2"])
        .attach("metadata", meta);
    
    let display = format!("{}", ctx);
    println!("{}", display);
    assert!(display.contains("test error"));
    assert!(display.contains("main.rs"));
    assert!(display.contains("42"));
    assert!(display.contains("https://example.com/docs"));
    assert!(display.contains("issue-1"));
    assert!(display.contains("issue-2"));
}