comperr 1.0.0

A minimal, lightweight crate for emitting span-accurate compile-time errors from procedural macros.
Documentation
#[cfg(test)]
mod tests {
    use comperr::{Error, error};
    use proc_macro2::Span;

    // --- error() free function ---

    #[test]
    fn error_creates_valid_tokenstream() {
        let result = error(Span::call_site(), "test error");
        assert!(!result.is_empty());
    }

    #[test]
    fn error_message_is_preserved() {
        let result = error(Span::call_site(), "test error message");
        let result_str = result.to_string();
        assert!(result_str.contains("test error message"));
    }

    #[test]
    fn error_contains_compile_error() {
        let result = error(Span::call_site(), "test error");
        let result_str = result.to_string();
        assert!(result_str.contains("compile_error"));
    }

    // --- Error::new and to_compile_error ---

    #[test]
    fn error_struct_works() {
        let err = Error::new(Span::call_site(), "test error");
        let result = err.to_compile_error();
        assert!(!result.is_empty());
    }

    #[test]
    fn error_with_different_messages() {
        let err1 = Error::new(Span::call_site(), "message one");
        let err2 = Error::new(Span::call_site(), "message two");

        let result1 = err1.to_compile_error();
        let result2 = err2.to_compile_error();

        assert!(result1.to_string().contains("message one"));
        assert!(result2.to_string().contains("message two"));
    }

    #[test]
    fn error_string_message() {
        let msg = String::from("owned message");
        let result = error(Span::call_site(), msg);
        assert!(!result.is_empty());
    }

    #[test]
    fn error_empty_message() {
        let result = error(Span::call_site(), "");
        let result_str = result.to_string();
        assert!(result_str.contains("compile_error"));
    }

    #[test]
    fn error_multiline_message() {
        let result = error(Span::call_site(), "line 1\nline 2\nline 3");
        let result_str = result.to_string();
        assert!(result_str.contains("line 1"));
    }

    // --- Error::combine ---

    #[test]
    fn error_combine_multiple_errors() {
        let mut err = Error::new(Span::call_site(), "first");
        err.combine(Error::new(Span::call_site(), "second"));
        let result = err.to_compile_error();
        let result_str = result.to_string();
        assert!(result_str.contains("compile_error"));
        assert!(result_str.contains("first"));
        assert!(result_str.contains("second"));
    }

    #[test]
    fn error_combine_three_errors() {
        let mut err = Error::new(Span::call_site(), "one");
        err.combine(Error::new(Span::call_site(), "two"));
        err.combine(Error::new(Span::call_site(), "three"));
        let result = err.to_compile_error();
        let result_str = result.to_string();
        assert!(result_str.contains("one"));
        assert!(result_str.contains("two"));
        assert!(result_str.contains("three"));
    }

    #[test]
    fn error_combine_preserves_span_per_message() {
        let span1 = Span::call_site();
        let span2 = Span::call_site();
        let mut err = Error::new(span1, "error at span1");
        err.combine(Error::new(span2, "error at span2"));
        let result = err.to_compile_error();
        let result_str = result.to_string();
        assert!(result_str.contains("error at span1"));
        assert!(result_str.contains("error at span2"));
    }

    // --- Message content edge cases ---

    #[test]
    fn error_special_characters_in_message() {
        let result = error(Span::call_site(), "expected <T> but found &[i32]");
        let result_str = result.to_string();
        assert!(result_str.contains("expected <T> but found &[i32]"));
    }

    #[test]
    fn error_quotes_in_message() {
        let result = error(Span::call_site(), "expected \"foo\"");
        let result_str = result.to_string();
        assert!(result_str.contains("expected \\\"foo\\\""));
    }

    #[test]
    fn error_backticks_in_message() {
        let result = error(Span::call_site(), "use `Foo` instead");
        let result_str = result.to_string();
        assert!(result_str.contains("use `Foo` instead"));
    }

    #[test]
    fn error_unicode_message() {
        let result = error(Span::call_site(), "héllo wörld");
        let result_str = result.to_string();
        assert!(result_str.contains("héllo wörld"));
    }

    #[test]
    fn error_emoji_message() {
        let result = error(Span::call_site(), "error 💔");
        let result_str = result.to_string();
        assert!(result_str.contains("💔"));
    }

    #[test]
    fn error_very_long_message() {
        let long_msg = "x".repeat(10000);
        let result = error(Span::call_site(), long_msg);
        let result_str = result.to_string();
        assert!(result_str.contains("xxxxx"));
    }

    #[test]
    fn error_single_char_message() {
        let result = error(Span::call_site(), "a");
        let result_str = result.to_string();
        assert!(result_str.contains("a"));
        assert!(result_str.contains("compile_error"));
    }

    #[test]
    fn error_new_with_string() {
        let err = Error::new(Span::call_site(), String::from("owned"));
        let result = err.to_compile_error();
        assert!(!result.is_empty());
    }

    #[test]
    fn error_with_colon_in_message() {
        let result = error(Span::call_site(), "help: did you mean?");
        let result_str = result.to_string();
        assert!(result_str.contains("help: did you mean?"));
    }

    #[test]
    fn error_with_arrow_in_message() {
        let result = error(Span::call_site(), "expected i32 -> Result");
        let result_str = result.to_string();
        assert!(result_str.contains("->"));
    }

    // --- Error::empty and is_empty ---

    #[test]
    fn empty_error_is_empty() {
        assert!(Error::empty().is_empty());
    }

    #[test]
    fn new_error_is_not_empty() {
        assert!(!Error::new(Span::call_site(), "hello").is_empty());
    }

    #[test]
    fn empty_error_produces_empty_tokenstream() {
        assert!(Error::empty().to_compile_error().is_empty());
    }

    #[test]
    fn combine_into_empty_makes_non_empty() {
        let mut acc = Error::empty();
        assert!(acc.is_empty());
        acc.combine(Error::new(Span::call_site(), "oops"));
        assert!(!acc.is_empty());
    }

    // --- Display ---

    #[test]
    fn display_single_error() {
        let e = Error::new(Span::call_site(), "something broke");
        assert_eq!(e.to_string(), "something broke");
    }

    #[test]
    fn display_multiple_errors_joined_by_newline() {
        let mut e = Error::new(Span::call_site(), "first");
        e.combine(Error::new(Span::call_site(), "second"));
        e.combine(Error::new(Span::call_site(), "third"));
        assert_eq!(e.to_string(), "first\nsecond\nthird");
    }

    #[test]
    fn display_empty_error_is_empty_string() {
        assert_eq!(Error::empty().to_string(), "");
    }

    // --- Debug and Clone ---

    #[test]
    fn error_implements_debug() {
        let e = Error::new(Span::call_site(), "debug me");
        let s = format!("{:?}", e);
        assert!(s.contains("debug me"));
    }

    #[test]
    fn error_implements_clone() {
        let e = Error::new(Span::call_site(), "original");
        let cloned = e.clone();
        assert_eq!(cloned.to_string(), "original");
    }

    #[test]
    fn clone_is_independent() {
        let mut e = Error::new(Span::call_site(), "original");
        let cloned = e.clone();
        e.combine(Error::new(Span::call_site(), "added to original"));
        assert_eq!(cloned.to_string(), "original");
        assert_eq!(e.to_string(), "original\nadded to original");
    }

    // --- std::error::Error ---

    #[test]
    fn implements_std_error() {
        let e = Error::new(Span::call_site(), "std error");
        let _: &dyn std::error::Error = &e;
    }

    // --- Extend ---

    #[test]
    fn extend_adds_errors() {
        let mut base = Error::new(Span::call_site(), "base");
        base.extend([
            Error::new(Span::call_site(), "extra one"),
            Error::new(Span::call_site(), "extra two"),
        ]);
        assert_eq!(base.to_string(), "base\nextra one\nextra two");
    }

    #[test]
    fn extend_with_empty_iter_is_noop() {
        let mut base = Error::new(Span::call_site(), "unchanged");
        base.extend(std::iter::empty::<Error>());
        assert_eq!(base.to_string(), "unchanged");
    }

    // --- FromIterator ---

    #[test]
    fn collect_errors_into_single_error() {
        let errors = vec![
            Error::new(Span::call_site(), "alpha"),
            Error::new(Span::call_site(), "beta"),
            Error::new(Span::call_site(), "gamma"),
        ];
        let combined: Error = errors.into_iter().collect();
        assert_eq!(combined.to_string(), "alpha\nbeta\ngamma");
    }

    #[test]
    fn collect_empty_iterator_gives_empty_error() {
        let combined: Error = std::iter::empty::<Error>().collect();
        assert!(combined.is_empty());
    }

    // --- from_token_stream ---

    #[test]
    fn from_token_stream_round_trips_single_error() {
        let original = Error::new(Span::call_site(), "round trip me");
        let ts = original.to_compile_error();
        let recovered = Error::from_token_stream(ts);
        assert_eq!(recovered.to_string(), "round trip me");
    }

    #[test]
    fn from_token_stream_round_trips_combined_errors() {
        let mut original = Error::new(Span::call_site(), "first");
        original.combine(Error::new(Span::call_site(), "second"));
        original.combine(Error::new(Span::call_site(), "third"));
        let ts = original.to_compile_error();
        let recovered = Error::from_token_stream(ts);
        let s = recovered.to_string();
        assert!(s.contains("first"));
        assert!(s.contains("second"));
        assert!(s.contains("third"));
    }

    #[test]
    fn from_token_stream_preserves_count() {
        let mut original = Error::new(Span::call_site(), "a");
        original.combine(Error::new(Span::call_site(), "b"));
        let ts = original.to_compile_error();
        let recovered = Error::from_token_stream(ts);
        // Both messages should appear in the token stream output.
        let output = recovered.to_compile_error().to_string();
        assert!(output.contains("\"a\""));
        assert!(output.contains("\"b\""));
    }

    #[test]
    fn from_token_stream_empty_stream_returns_fallback() {
        use proc_macro2::TokenStream;
        let empty: TokenStream = TokenStream::new();
        let recovered = Error::from_token_stream(empty);
        assert!(!recovered.is_empty());
        assert!(recovered.to_string().contains("unexpected error"));
    }

    #[test]
    fn from_token_stream_non_compile_error_returns_fallback() {
        use proc_macro2::TokenStream;
        let ts: TokenStream = "let x = 1;".parse().unwrap();
        let recovered = Error::from_token_stream(ts);
        assert!(!recovered.is_empty());
    }

    #[test]
    fn from_token_stream_unicode_message() {
        let original = Error::new(Span::call_site(), "héllo wörld");
        let ts = original.to_compile_error();
        let recovered = Error::from_token_stream(ts);
        assert!(recovered.to_string().contains("héllo wörld"));
    }
}