Skip to main content

grammar_kit/
testing.rs

1//! Utilities for testing parsers generated by `syn-grammar`.
2//!
3//! This module provides a fluent API for testing parsing results,
4//! asserting success/failure, and checking error messages.
5
6#[cfg(feature = "syn")]
7use std::any::Any;
8use std::fmt::{Debug, Display};
9
10// A wrapper around Result to write fluent tests.
11pub struct TestResult<T, E> {
12    inner: Result<T, E>,
13    context: Option<String>,
14    source: Option<String>,
15}
16
17impl<T: Debug, E: Display + Debug + 'static> TestResult<T, E> {
18    pub fn new(result: Result<T, E>) -> Self {
19        Self {
20            inner: result,
21            context: None,
22            source: None,
23        }
24    }
25
26    pub fn with_context(mut self, context: &str) -> Self {
27        self.context = Some(context.to_string());
28        self
29    }
30
31    pub fn with_source(mut self, source: &str) -> Self {
32        self.source = Some(source.to_string());
33        self
34    }
35
36    fn format_context(&self) -> String {
37        self.context
38            .as_ref()
39            .map(|c| format!("\nContext:  {}", c))
40            .unwrap_or_default()
41    }
42
43    fn format_err(&self, err: &E) -> String {
44        format_error_impl(err, self.source.as_deref())
45    }
46
47    /// Prints the result to stdout for debugging purposes.
48    /// Useful when running tests with `-- --nocapture`.
49    pub fn inspect(self) -> Self {
50        let ctx = self.format_context();
51        match &self.inner {
52            Ok(val) => {
53                println!("\n🔎 INSPECT SUCCESS: {}\nValue: {:?}\n", ctx, val);
54            }
55            Err(e) => {
56                let msg = self.format_err(e);
57                println!(
58                    "\n🔎 INSPECT FAILURE: {}\nMessage: {}\nDebug:   {:?}\n",
59                    ctx, msg, e
60                );
61            }
62        }
63        self
64    }
65
66    // 1. Asserts success and returns the value.
67    pub fn assert_success(self) -> T {
68        let ctx = self.format_context();
69        match self.inner {
70            Ok(val) => {
71                println!("ℹ️  Asserting success.\n   Actual:   {:?}", val);
72                val
73            }
74            Err(ref e) => {
75                let msg = self.format_err(e);
76                panic!(
77                    "\n🔴 TEST FAILED (Expected Success, but got Error):{}\nMessage:  {}\nError Debug: {:?}\n", 
78                    ctx, msg, e
79                );
80            }
81        }
82    }
83
84    // 2. Asserts success AND checks the value directly.
85    // Returns a nice diff output if values do not match.
86    pub fn assert_success_is<Exp>(self, expected: Exp) -> T
87    where
88        T: PartialEq<Exp>,
89        Exp: Debug,
90    {
91        let ctx = self.format_context();
92        // This will print "Asserting success. Actual: ..."
93        let val = self.assert_success();
94
95        println!("ℹ️  Checking equality.\n   Expected: {:?}", expected);
96
97        if val != expected {
98            panic!(
99                "\n🔴 TEST FAILED (Value Mismatch):{}\nExpected: {:?}\nGot:      {:?}\n",
100                ctx, expected, val
101            );
102        }
103        val
104    }
105
106    // 3. Asserts success AND checks the value using a closure.
107    // Useful for complex assertions or when PartialEq is not implemented.
108    pub fn assert_success_with<F>(self, f: F) -> T
109    where
110        F: FnOnce(&T),
111    {
112        // This will print "Asserting success. Actual: ..."
113        let val = self.assert_success();
114        println!("ℹ️  Asserting success with closure.");
115        f(&val);
116        val
117    }
118
119    // 4. Asserts success AND checks the Debug representation matches.
120    // Useful for syn types where PartialEq is often missing or complicated by Spans.
121    pub fn assert_success_debug(self, expected_debug: &str) -> T {
122        let ctx = self.format_context();
123        // This will print "Asserting success. Actual: ..."
124        let val = self.assert_success();
125        let actual_debug = format!("{:?}", val);
126
127        println!(
128            "ℹ️  Checking debug representation.\n   Expected: {:?}",
129            expected_debug
130        );
131
132        if actual_debug != expected_debug {
133            panic!(
134                "\n🔴 TEST FAILED (Debug Mismatch):{}\nExpected: {:?}\nGot:      {:?}\n",
135                ctx, expected_debug, actual_debug
136            );
137        }
138        val
139    }
140
141    // 5. Asserts failure and returns the error.
142    pub fn assert_failure(self) -> E {
143        let ctx = self.format_context();
144        match self.inner {
145            Ok(val) => {
146                panic!(
147                    "\n🔴 TEST FAILED (Expected Failure, but got Success):{}\nParsed Value: {:?}\n",
148                    ctx, val
149                );
150            }
151            Err(e) => {
152                println!("ℹ️  Asserting failure.\n   Error:    {:?}", e);
153                e
154            }
155        }
156    }
157
158    // 6. Asserts failure AND checks if the message contains a specific text.
159    pub fn assert_failure_contains(self, expected_msg_part: &str) {
160        let ctx = self.format_context();
161        let source = self.source.clone();
162
163        // This will print "Asserting failure. Error: ..."
164        let err = self.assert_failure();
165        let actual_msg = err.to_string();
166
167        println!(
168            "ℹ️  Checking error message contains {:?}.\n   Actual message: {:?}",
169            expected_msg_part, actual_msg
170        );
171
172        if !actual_msg.contains(expected_msg_part) {
173            let formatted = format_error_impl(&err, source.as_deref());
174            panic!(
175                "\n🔴 TEST FAILED (Error Message Mismatch):{}\nExpected part: {:?}\nActual msg:    {:?}\nError Debug:   {:?}\nFormatted:   \n{}\n", 
176                ctx, expected_msg_part, actual_msg, err, formatted
177            );
178        }
179    }
180
181    // 7. Asserts success AND checks if the string representation contains a specific substring.
182    pub fn assert_success_contains(self, expected_part: &str) -> T
183    where
184        T: Display,
185    {
186        let ctx = self.format_context();
187        // This will print "Asserting success. Actual: ..."
188        let val = self.assert_success();
189        let val_str = val.to_string();
190
191        println!(
192            "ℹ️  Checking success string contains {:?}.\n   Actual string: {:?}",
193            expected_part, val_str
194        );
195
196        if !val_str.contains(expected_part) {
197            panic!(
198                "\n🔴 TEST FAILED (Content Mismatch):{}\nExpected to contain: {:?}\nGot:                 {:?}\n",
199                ctx, expected_part, val_str
200            );
201        }
202        val
203    }
204
205    // 8. Asserts failure AND checks if the message DOES NOT contain a specific text.
206    pub fn assert_failure_not_contains(self, unexpected_part: &str) {
207        let ctx = self.format_context();
208        let source = self.source.clone();
209
210        // This will print "Asserting failure. Error: ..."
211        let err = self.assert_failure();
212        let actual_msg = err.to_string();
213
214        println!(
215            "ℹ️  Checking error message NOT contains {:?}.\n   Actual message: {:?}",
216            unexpected_part, actual_msg
217        );
218
219        if actual_msg.contains(unexpected_part) {
220            let formatted = format_error_impl(&err, source.as_deref());
221            panic!(
222                "\n🔴 TEST FAILED (Unexpected Error Message Content):{}\nUnexpected part: {:?}\nActual msg:      {:?}\nError Debug:     {:?}\nFormatted:\n{}\n", 
223                ctx, unexpected_part, actual_msg, err, formatted
224            );
225        }
226    }
227}
228
229pub trait Testable<T, E> {
230    fn test(self) -> TestResult<T, E>;
231}
232
233#[cfg(feature = "syn")]
234impl<T: Debug> Testable<T, syn::Error> for syn::Result<T> {
235    fn test(self) -> TestResult<T, syn::Error> {
236        TestResult::new(self)
237    }
238}
239
240fn format_error_impl<E: Display + Debug + 'static>(err: &E, source: Option<&str>) -> String {
241    #[cfg(feature = "syn")]
242    if let Some(src) = source {
243        if let Some(syn_err) = (err as &dyn Any).downcast_ref::<syn::Error>() {
244            return pretty_print_syn_error(syn_err, src);
245        }
246    }
247    format!("{}", err)
248}
249
250#[cfg(feature = "syn")]
251fn pretty_print_syn_error(err: &syn::Error, source: &str) -> String {
252    let start = err.span().start();
253    let end = err.span().end();
254
255    if start.line == 0 {
256        return err.to_string();
257    }
258
259    let line_idx = start.line - 1;
260    let lines: Vec<&str> = source.lines().collect();
261
262    if line_idx >= lines.len() {
263        return err.to_string();
264    }
265
266    let line = lines[line_idx];
267    let col = start.column;
268
269    // Calculate width of the underline
270    // If start and end are on the same line, width is end.column - start.column
271    // Else just highlight until end of line or 1 char.
272    let width = if start.line == end.line {
273        end.column.saturating_sub(col).max(1)
274    } else {
275        1
276    };
277
278    format!(
279        "{}\n  --> line {}:{}\n   |\n {} | {}\n   | {}{}",
280        err,
281        start.line,
282        col,
283        start.line,
284        line,
285        " ".repeat(col),
286        "^".repeat(width)
287    )
288}