grammar-kit 0.8.0

Runtime support library for parsers generated by syn-grammar.
Documentation
//! Utilities for testing parsers generated by `syn-grammar`.
//!
//! This module provides a fluent API for testing parsing results,
//! asserting success/failure, and checking error messages.

#[cfg(feature = "syn")]
use std::any::Any;
use std::fmt::{Debug, Display};

// A wrapper around Result to write fluent tests.
pub struct TestResult<T, E> {
    inner: Result<T, E>,
    context: Option<String>,
    source: Option<String>,
}

impl<T: Debug, E: Display + Debug + 'static> TestResult<T, E> {
    pub fn new(result: Result<T, E>) -> Self {
        Self {
            inner: result,
            context: None,
            source: None,
        }
    }

    pub fn with_context(mut self, context: &str) -> Self {
        self.context = Some(context.to_string());
        self
    }

    pub fn with_source(mut self, source: &str) -> Self {
        self.source = Some(source.to_string());
        self
    }

    fn format_context(&self) -> String {
        self.context
            .as_ref()
            .map(|c| format!("\nContext:  {}", c))
            .unwrap_or_default()
    }

    fn format_err(&self, err: &E) -> String {
        format_error_impl(err, self.source.as_deref())
    }

    /// Prints the result to stdout for debugging purposes.
    /// Useful when running tests with `-- --nocapture`.
    pub fn inspect(self) -> Self {
        let ctx = self.format_context();
        match &self.inner {
            Ok(val) => {
                println!("\n🔎 INSPECT SUCCESS: {}\nValue: {:?}\n", ctx, val);
            }
            Err(e) => {
                let msg = self.format_err(e);
                println!(
                    "\n🔎 INSPECT FAILURE: {}\nMessage: {}\nDebug:   {:?}\n",
                    ctx, msg, e
                );
            }
        }
        self
    }

    // 1. Asserts success and returns the value.
    pub fn assert_success(self) -> T {
        let ctx = self.format_context();
        match self.inner {
            Ok(val) => {
                println!("â„šī¸  Asserting success.\n   Actual:   {:?}", val);
                val
            }
            Err(ref e) => {
                let msg = self.format_err(e);
                panic!(
                    "\n🔴 TEST FAILED (Expected Success, but got Error):{}\nMessage:  {}\nError Debug: {:?}\n", 
                    ctx, msg, e
                );
            }
        }
    }

    // 2. Asserts success AND checks the value directly.
    // Returns a nice diff output if values do not match.
    pub fn assert_success_is<Exp>(self, expected: Exp) -> T
    where
        T: PartialEq<Exp>,
        Exp: Debug,
    {
        let ctx = self.format_context();
        // This will print "Asserting success. Actual: ..."
        let val = self.assert_success();

        println!("â„šī¸  Checking equality.\n   Expected: {:?}", expected);

        if val != expected {
            panic!(
                "\n🔴 TEST FAILED (Value Mismatch):{}\nExpected: {:?}\nGot:      {:?}\n",
                ctx, expected, val
            );
        }
        val
    }

    // 3. Asserts success AND checks the value using a closure.
    // Useful for complex assertions or when PartialEq is not implemented.
    pub fn assert_success_with<F>(self, f: F) -> T
    where
        F: FnOnce(&T),
    {
        // This will print "Asserting success. Actual: ..."
        let val = self.assert_success();
        println!("â„šī¸  Asserting success with closure.");
        f(&val);
        val
    }

    // 4. Asserts success AND checks the Debug representation matches.
    // Useful for syn types where PartialEq is often missing or complicated by Spans.
    pub fn assert_success_debug(self, expected_debug: &str) -> T {
        let ctx = self.format_context();
        // This will print "Asserting success. Actual: ..."
        let val = self.assert_success();
        let actual_debug = format!("{:?}", val);

        println!(
            "â„šī¸  Checking debug representation.\n   Expected: {:?}",
            expected_debug
        );

        if actual_debug != expected_debug {
            panic!(
                "\n🔴 TEST FAILED (Debug Mismatch):{}\nExpected: {:?}\nGot:      {:?}\n",
                ctx, expected_debug, actual_debug
            );
        }
        val
    }

    // 5. Asserts failure and returns the error.
    pub fn assert_failure(self) -> E {
        let ctx = self.format_context();
        match self.inner {
            Ok(val) => {
                panic!(
                    "\n🔴 TEST FAILED (Expected Failure, but got Success):{}\nParsed Value: {:?}\n",
                    ctx, val
                );
            }
            Err(e) => {
                println!("â„šī¸  Asserting failure.\n   Error:    {:?}", e);
                e
            }
        }
    }

    // 6. Asserts failure AND checks if the message contains a specific text.
    pub fn assert_failure_contains(self, expected_msg_part: &str) {
        let ctx = self.format_context();
        let source = self.source.clone();

        // This will print "Asserting failure. Error: ..."
        let err = self.assert_failure();
        let actual_msg = err.to_string();

        println!(
            "â„šī¸  Checking error message contains {:?}.\n   Actual message: {:?}",
            expected_msg_part, actual_msg
        );

        if !actual_msg.contains(expected_msg_part) {
            let formatted = format_error_impl(&err, source.as_deref());
            panic!(
                "\n🔴 TEST FAILED (Error Message Mismatch):{}\nExpected part: {:?}\nActual msg:    {:?}\nError Debug:   {:?}\nFormatted:   \n{}\n", 
                ctx, expected_msg_part, actual_msg, err, formatted
            );
        }
    }

    // 7. Asserts success AND checks if the string representation contains a specific substring.
    pub fn assert_success_contains(self, expected_part: &str) -> T
    where
        T: Display,
    {
        let ctx = self.format_context();
        // This will print "Asserting success. Actual: ..."
        let val = self.assert_success();
        let val_str = val.to_string();

        println!(
            "â„šī¸  Checking success string contains {:?}.\n   Actual string: {:?}",
            expected_part, val_str
        );

        if !val_str.contains(expected_part) {
            panic!(
                "\n🔴 TEST FAILED (Content Mismatch):{}\nExpected to contain: {:?}\nGot:                 {:?}\n",
                ctx, expected_part, val_str
            );
        }
        val
    }

    // 8. Asserts failure AND checks if the message DOES NOT contain a specific text.
    pub fn assert_failure_not_contains(self, unexpected_part: &str) {
        let ctx = self.format_context();
        let source = self.source.clone();

        // This will print "Asserting failure. Error: ..."
        let err = self.assert_failure();
        let actual_msg = err.to_string();

        println!(
            "â„šī¸  Checking error message NOT contains {:?}.\n   Actual message: {:?}",
            unexpected_part, actual_msg
        );

        if actual_msg.contains(unexpected_part) {
            let formatted = format_error_impl(&err, source.as_deref());
            panic!(
                "\n🔴 TEST FAILED (Unexpected Error Message Content):{}\nUnexpected part: {:?}\nActual msg:      {:?}\nError Debug:     {:?}\nFormatted:\n{}\n", 
                ctx, unexpected_part, actual_msg, err, formatted
            );
        }
    }
}

pub trait Testable<T, E> {
    fn test(self) -> TestResult<T, E>;
}

#[cfg(feature = "syn")]
impl<T: Debug> Testable<T, syn::Error> for syn::Result<T> {
    fn test(self) -> TestResult<T, syn::Error> {
        TestResult::new(self)
    }
}

fn format_error_impl<E: Display + Debug + 'static>(err: &E, source: Option<&str>) -> String {
    #[cfg(feature = "syn")]
    if let Some(src) = source {
        if let Some(syn_err) = (err as &dyn Any).downcast_ref::<syn::Error>() {
            return pretty_print_syn_error(syn_err, src);
        }
    }
    format!("{}", err)
}

#[cfg(feature = "syn")]
fn pretty_print_syn_error(err: &syn::Error, source: &str) -> String {
    let start = err.span().start();
    let end = err.span().end();

    if start.line == 0 {
        return err.to_string();
    }

    let line_idx = start.line - 1;
    let lines: Vec<&str> = source.lines().collect();

    if line_idx >= lines.len() {
        return err.to_string();
    }

    let line = lines[line_idx];
    let col = start.column;

    // Calculate width of the underline
    // If start and end are on the same line, width is end.column - start.column
    // Else just highlight until end of line or 1 char.
    let width = if start.line == end.line {
        end.column.saturating_sub(col).max(1)
    } else {
        1
    };

    format!(
        "{}\n  --> line {}:{}\n   |\n {} | {}\n   | {}{}",
        err,
        start.line,
        col,
        start.line,
        line,
        " ".repeat(col),
        "^".repeat(width)
    )
}