Skip to main content

tree_sitter_cli/
test.rs

1use std::{
2    collections::BTreeMap,
3    ffi::OsStr,
4    fmt::{Display as _, Write as _},
5    fs,
6    io::{self, Write},
7    path::{Path, PathBuf},
8    time::Duration,
9};
10
11use anstyle::AnsiColor;
12use anyhow::{Context, Result, anyhow};
13use clap::ValueEnum;
14use indoc::indoc;
15use log::warn;
16use regex::Regex;
17use schemars::{JsonSchema, Schema, SchemaGenerator};
18use serde::Serialize;
19use similar::{ChangeTag, TextDiff};
20use tree_sitter::{Language, LogType, Parser, Query, Tree, format_sexp};
21use walkdir::WalkDir;
22
23use super::util;
24use crate::{
25    paint::{color_enabled, paint},
26    parse::{
27        ParseDebugType, ParseFileOptions, ParseOutput, ParseStats, ParseTheme, Stats, render_cst,
28    },
29};
30
31/// Check if a line consists of 3+ repetitions of `ch` followed by an optional suffix.
32///
33/// Returns `Some((delim_len, suffix))` if so, where `suffix` is the part after the
34/// repeated characters (empty string if no suffix). Returns `None` otherwise.
35fn parse_delimiter_line(line: &str, c: char) -> Option<(usize, &str)> {
36    let delim_len = line.len() - line.trim_start_matches(c).len();
37    if delim_len < 3 {
38        return None;
39    }
40    let suffix = line[delim_len..].trim_end_matches(['\r', '\n']);
41    Some((delim_len, suffix))
42}
43
44/// Normalize expected sexp output: remove comment lines (lines starting with `;`),
45/// collapse whitespace, and remove spaces before closing parens.
46fn normalize_sexp_output(raw: &str) -> (String, bool) {
47    let mut result = String::with_capacity(raw.len());
48    let mut prev_was_space = false;
49
50    for line in raw.lines() {
51        // Skip comment lines: lines whose first non-whitespace character is `;`
52        if line.trim_start().starts_with(';') {
53            continue;
54        }
55        for ch in line.chars() {
56            if ch.is_whitespace() {
57                if !prev_was_space && !result.is_empty() {
58                    result.push(' ');
59                    prev_was_space = true;
60                }
61            } else {
62                if ch == ')' && prev_was_space {
63                    result.pop(); // remove trailing space before `)`
64                }
65                result.push(ch);
66                prev_was_space = false;
67            }
68        }
69        // Line boundary counts as whitespace
70        if !result.is_empty() && !prev_was_space {
71            result.push(' ');
72            prev_was_space = true;
73        }
74    }
75
76    // No leading whitespace
77    let result = result.trim_end().to_string();
78    let has_fields = result.contains(": (");
79
80    (result, has_fields)
81}
82
83#[derive(Debug, PartialEq, Eq)]
84pub enum TestEntry {
85    Group {
86        name: String,
87        children: Vec<Self>,
88        file_path: Option<PathBuf>,
89    },
90    Example {
91        name: String,
92        input: Vec<u8>,
93        output: String,
94        header_delim_len: usize,
95        divider_delim_len: usize,
96        has_fields: bool,
97        attributes_str: String,
98        attributes: TestAttributes,
99        file_name: Option<String>,
100    },
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct TestAttributes {
105    pub platform: bool,
106    pub fail_fast: bool,
107    pub expectation: TestExpectation,
108    pub cst: bool,
109    pub languages: Vec<Box<str>>,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum TestExpectation {
114    Pass,
115    Error,
116    Skip,
117}
118
119impl TestAttributes {
120    #[must_use]
121    fn skip(&self) -> bool {
122        self.expectation == TestExpectation::Skip
123    }
124
125    #[must_use]
126    fn error(&self) -> bool {
127        self.expectation == TestExpectation::Error
128    }
129}
130
131impl Default for TestEntry {
132    fn default() -> Self {
133        Self::Group {
134            name: String::new(),
135            children: Vec::new(),
136            file_path: None,
137        }
138    }
139}
140
141impl Default for TestAttributes {
142    fn default() -> Self {
143        Self {
144            platform: true,
145            fail_fast: false,
146            expectation: TestExpectation::Pass,
147            cst: false,
148            languages: vec!["".into()],
149        }
150    }
151}
152
153#[derive(ValueEnum, Default, Debug, Copy, Clone, PartialEq, Eq, Serialize)]
154pub enum TestStats {
155    All,
156    #[default]
157    OutliersAndTotal,
158    TotalOnly,
159}
160
161pub struct TestOptions<'a> {
162    pub path: PathBuf,
163    pub debug: bool,
164    pub debug_graph: bool,
165    pub include: Option<Regex>,
166    pub exclude: Option<Regex>,
167    pub file_name: Option<String>,
168    pub update: bool,
169    pub open_log: bool,
170    pub languages: BTreeMap<&'a str, &'a Language>,
171    pub show_fields: bool,
172    pub overview_only: bool,
173}
174
175/// A stateful object used to collect results from running a grammar's test suite
176#[derive(Debug, Default, Serialize, JsonSchema)]
177pub struct TestSummary {
178    // Parse test results and associated data
179    #[schemars(schema_with = "schema_as_array")]
180    #[serde(serialize_with = "serialize_as_array")]
181    pub parse_results: TestResultHierarchy,
182    pub parse_failures: Vec<TestFailure>,
183    pub parse_stats: Stats,
184    #[schemars(skip)]
185    #[serde(skip)]
186    pub has_parse_errors: bool,
187    #[schemars(skip)]
188    #[serde(skip)]
189    pub parse_stat_display: TestStats,
190
191    // Other test results
192    #[schemars(schema_with = "schema_as_array")]
193    #[serde(serialize_with = "serialize_as_array")]
194    pub highlight_results: TestResultHierarchy,
195    #[schemars(schema_with = "schema_as_array")]
196    #[serde(serialize_with = "serialize_as_array")]
197    pub tag_results: TestResultHierarchy,
198    #[schemars(schema_with = "schema_as_array")]
199    #[serde(serialize_with = "serialize_as_array")]
200    pub query_results: TestResultHierarchy,
201
202    // Data used during construction
203    #[schemars(skip)]
204    #[serde(skip)]
205    pub test_num: usize,
206    // Options passed in from the CLI which control how the summary is displayed
207    #[schemars(skip)]
208    #[serde(skip)]
209    pub use_markers: bool,
210    #[schemars(skip)]
211    #[serde(skip)]
212    pub overview_only: bool,
213    #[schemars(skip)]
214    #[serde(skip)]
215    pub update: bool,
216    #[schemars(skip)]
217    #[serde(skip)]
218    pub json: bool,
219}
220
221impl TestSummary {
222    #[must_use]
223    pub fn new(
224        stat_display: TestStats,
225        parse_update: bool,
226        overview_only: bool,
227        json_summary: bool,
228    ) -> Self {
229        Self {
230            parse_stat_display: stat_display,
231            update: parse_update,
232            overview_only,
233            json: json_summary,
234            test_num: 1,
235            ..Default::default()
236        }
237    }
238}
239
240#[derive(Debug, Default, JsonSchema)]
241pub struct TestResultHierarchy {
242    root_group: Vec<TestResult>,
243    traversal_idxs: Vec<usize>,
244}
245
246fn serialize_as_array<S>(results: &TestResultHierarchy, serializer: S) -> Result<S::Ok, S::Error>
247where
248    S: serde::Serializer,
249{
250    results.root_group.serialize(serializer)
251}
252
253fn schema_as_array(schema_gen: &mut SchemaGenerator) -> Schema {
254    schema_gen.subschema_for::<Vec<TestResult>>()
255}
256
257/// Stores arbitrarily nested parent test groups and child cases. Supports creation
258/// in DFS traversal order
259impl TestResultHierarchy {
260    /// Signifies the start of a new group's traversal during construction.
261    fn push_traversal(&mut self, idx: usize) {
262        self.traversal_idxs.push(idx);
263    }
264
265    /// Signifies the end of the current group's traversal during construction.
266    /// Must be paired with a prior call to [`TestResultHierarchy::add_group`].
267    pub fn pop_traversal(&mut self) {
268        self.traversal_idxs.pop();
269    }
270
271    /// Adds a new group as a child of the current group. Caller is responsible
272    /// for calling [`TestResultHierarchy::pop_traversal`] once the group is done
273    /// being traversed.
274    pub fn add_group(&mut self, group_name: &str) {
275        let new_group_idx = self.curr_group_len();
276        self.push(TestResult {
277            name: group_name.to_string(),
278            info: TestInfo::Group {
279                children: Vec::new(),
280            },
281        });
282        self.push_traversal(new_group_idx);
283    }
284
285    /// Adds a new test example as a child of the current group.
286    /// Asserts that `test_case.info` is not [`TestInfo::Group`].
287    pub fn add_case(&mut self, test_case: TestResult) {
288        assert!(!matches!(test_case.info, TestInfo::Group { .. }));
289        self.push(test_case);
290    }
291
292    /// Adds a new `TestResult` to the current group.
293    fn push(&mut self, result: TestResult) {
294        // If there are no traversal steps, we're adding to the root
295        if self.traversal_idxs.is_empty() {
296            self.root_group.push(result);
297            return;
298        }
299
300        #[expect(
301            clippy::manual_let_else,
302            reason = "mutable borrow in match arm prevents let-else"
303        )]
304        let mut curr_group = match self.root_group[self.traversal_idxs[0]].info {
305            TestInfo::Group { ref mut children } => children,
306            _ => unreachable!(),
307        };
308        for idx in self.traversal_idxs.iter().skip(1) {
309            curr_group = match curr_group[*idx].info {
310                TestInfo::Group { ref mut children } => children,
311                _ => unreachable!(),
312            };
313        }
314
315        curr_group.push(result);
316    }
317
318    fn curr_group_len(&self) -> usize {
319        if self.traversal_idxs.is_empty() {
320            return self.root_group.len();
321        }
322
323        #[expect(
324            clippy::manual_let_else,
325            reason = "destructuring borrow in match arm prevents let-else"
326        )]
327        let mut curr_group = match self.root_group[self.traversal_idxs[0]].info {
328            TestInfo::Group { ref children } => children,
329            _ => unreachable!(),
330        };
331        for idx in self.traversal_idxs.iter().skip(1) {
332            curr_group = match curr_group[*idx].info {
333                TestInfo::Group { ref children } => children,
334                _ => unreachable!(),
335            };
336        }
337        curr_group.len()
338    }
339
340    #[expect(
341        clippy::iter_without_into_iter,
342        reason = "IntoIterator not needed for this internal type"
343    )]
344    #[must_use]
345    pub fn iter(&self) -> TestResultIterWithDepth<'_> {
346        let mut stack = Vec::with_capacity(self.root_group.len());
347        for child in self.root_group.iter().rev() {
348            stack.push((0, child));
349        }
350        TestResultIterWithDepth { stack }
351    }
352}
353
354pub struct TestResultIterWithDepth<'a> {
355    stack: Vec<(usize, &'a TestResult)>,
356}
357
358impl<'a> Iterator for TestResultIterWithDepth<'a> {
359    type Item = (usize, &'a TestResult);
360
361    fn next(&mut self) -> Option<Self::Item> {
362        self.stack.pop().inspect(|(depth, result)| {
363            if let TestInfo::Group { children } = &result.info {
364                for child in children.iter().rev() {
365                    self.stack.push((depth + 1, child));
366                }
367            }
368        })
369    }
370}
371
372#[derive(Debug, Serialize, JsonSchema)]
373pub struct TestResult {
374    pub name: String,
375    #[schemars(flatten)]
376    #[serde(flatten)]
377    pub info: TestInfo,
378}
379
380#[derive(Debug, Serialize, JsonSchema)]
381#[schemars(untagged)]
382#[serde(untagged)]
383pub enum TestInfo {
384    Group {
385        children: Vec<TestResult>,
386    },
387    ParseTest {
388        outcome: TestOutcome,
389        // True parse rate, adjusted parse rate
390        #[schemars(schema_with = "parse_rate_schema")]
391        #[serde(serialize_with = "serialize_parse_rates")]
392        parse_rate: Option<(f64, f64)>,
393        test_num: usize,
394    },
395    AssertionTest {
396        outcome: TestOutcome,
397        test_num: usize,
398    },
399}
400
401#[expect(
402    clippy::ref_option,
403    reason = "signature required by serde serialize_with"
404)]
405fn serialize_parse_rates<S>(
406    parse_rate: &Option<(f64, f64)>,
407    serializer: S,
408) -> Result<S::Ok, S::Error>
409where
410    S: serde::Serializer,
411{
412    match parse_rate {
413        None => serializer.serialize_none(),
414        Some((first, _)) => serializer.serialize_some(first),
415    }
416}
417
418fn parse_rate_schema(schema_gen: &mut SchemaGenerator) -> Schema {
419    schema_gen.subschema_for::<Option<f64>>()
420}
421
422#[derive(Debug, Clone, Eq, PartialEq, Serialize, JsonSchema)]
423pub enum TestOutcome {
424    // Parse outcomes
425    Passed,
426    Failed,
427    Updated,
428    Skipped,
429    Platform,
430
431    // Highlight/Tag/Query outcomes
432    AssertionPassed { assertion_count: usize },
433    AssertionFailed { error: String },
434}
435
436impl TestSummary {
437    fn fmt_parse_results(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438        let (count, total_adj_parse_time) = self
439            .parse_results
440            .iter()
441            .filter_map(|(_, result)| match result.info {
442                TestInfo::Group { .. } => None,
443                TestInfo::ParseTest { parse_rate, .. } => parse_rate,
444                TestInfo::AssertionTest { .. } => unreachable!(),
445            })
446            .fold((0usize, 0.0f64), |(count, rate_accum), (_, adj_rate)| {
447                (count + 1, rate_accum + adj_rate)
448            });
449
450        let avg = total_adj_parse_time / count as f64;
451        let std_dev = {
452            let variance = self
453                .parse_results
454                .iter()
455                .filter_map(|(_, result)| match result.info {
456                    TestInfo::Group { .. } => None,
457                    TestInfo::ParseTest { parse_rate, .. } => parse_rate,
458                    TestInfo::AssertionTest { .. } => unreachable!(),
459                })
460                .map(|(_, rate_i)| (rate_i - avg).powi(2))
461                .sum::<f64>()
462                / count as f64;
463            variance.sqrt()
464        };
465
466        for (depth, entry) in self.parse_results.iter() {
467            write!(f, "{}", "  ".repeat(depth + 1))?;
468            match &entry.info {
469                TestInfo::Group { .. } => writeln!(f, "{}:", entry.name)?,
470                TestInfo::ParseTest {
471                    outcome,
472                    parse_rate,
473                    test_num,
474                } => {
475                    let (color, result_char) = match outcome {
476                        TestOutcome::Passed => (AnsiColor::Green, "✓"),
477                        TestOutcome::Failed => (AnsiColor::Red, "✗"),
478                        TestOutcome::Updated => (AnsiColor::Blue, "✓"),
479                        TestOutcome::Skipped => (AnsiColor::Yellow, "⌀"),
480                        TestOutcome::Platform => (AnsiColor::Magenta, "⌀"),
481                        _ => unreachable!(),
482                    };
483                    let stat_display = match (self.parse_stat_display, parse_rate) {
484                        (TestStats::TotalOnly, _) | (_, None) => String::new(),
485                        (display, Some((true_rate, adj_rate))) => {
486                            let mut stats = if display == TestStats::All {
487                                format!(" ({true_rate:.3} bytes/ms)")
488                            } else {
489                                String::new()
490                            };
491                            // 3 standard deviations below the mean, aka the "Empirical Rule"
492                            if *adj_rate < 3.0f64.mul_add(-std_dev, avg) {
493                                let _ = write!(
494                                    stats,
495                                    "{}",
496                                    paint(
497                                        Some(AnsiColor::Yellow),
498                                        format_args!(
499                                            " -- Warning: Slow parse rate ({true_rate:.3} bytes/ms)"
500                                        ),
501                                    )
502                                );
503                            }
504                            stats
505                        }
506                    };
507                    writeln!(
508                        f,
509                        "{test_num:>3}. {result_char} {}{stat_display}",
510                        paint(Some(color), &entry.name),
511                    )?;
512                }
513                TestInfo::AssertionTest { .. } => unreachable!(),
514            }
515        }
516
517        // Parse failure info
518        if !self.parse_failures.is_empty() && self.update && !self.has_parse_errors {
519            writeln!(
520                f,
521                "\n{} update{}:\n",
522                self.parse_failures.len(),
523                if self.parse_failures.len() == 1 {
524                    ""
525                } else {
526                    "s"
527                }
528            )?;
529
530            for (i, TestFailure { name, .. }) in self.parse_failures.iter().enumerate() {
531                writeln!(f, "  {}. {name}", i + 1)?;
532            }
533        } else if !self.parse_failures.is_empty() && !self.overview_only {
534            if !self.has_parse_errors {
535                writeln!(
536                    f,
537                    "\n{} failure{}:",
538                    self.parse_failures.len(),
539                    if self.parse_failures.len() == 1 {
540                        ""
541                    } else {
542                        "s"
543                    }
544                )?;
545            }
546
547            if color_enabled() {
548                DiffKey.fmt(f)?;
549            }
550            for (
551                i,
552                TestFailure {
553                    name,
554                    actual,
555                    expected,
556                    is_cst,
557                },
558            ) in self.parse_failures.iter().enumerate()
559            {
560                if expected == "NO ERROR" {
561                    writeln!(f, "\n  {}. {name}:\n", i + 1)?;
562                    writeln!(f, "  Expected an ERROR node, but got:")?;
563                    let actual = if *is_cst {
564                        actual
565                    } else {
566                        &format_sexp(actual, 2)
567                    };
568                    writeln!(f, "  {}", paint(Some(AnsiColor::Red), actual))?;
569                } else {
570                    writeln!(f, "\n  {}. {name}:", i + 1)?;
571                    if *is_cst {
572                        writeln!(
573                            f,
574                            "{}",
575                            TestDiff::new(actual, expected).with_markers(self.use_markers)
576                        )?;
577                    } else {
578                        writeln!(
579                            f,
580                            "{}",
581                            TestDiff::new(&format_sexp(actual, 2), &format_sexp(expected, 2))
582                                .with_markers(self.use_markers)
583                        )?;
584                    }
585                }
586            }
587        } else {
588            writeln!(f)?;
589        }
590
591        Ok(())
592    }
593}
594
595impl std::fmt::Display for TestSummary {
596    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
597        self.fmt_parse_results(f)?;
598
599        let mut render_assertion_results =
600            |name: &str, results: &TestResultHierarchy| -> std::fmt::Result {
601                writeln!(f, "{name}:")?;
602                for (depth, entry) in results.iter() {
603                    write!(f, "{}", "  ".repeat(depth + 2))?;
604                    match &entry.info {
605                        TestInfo::Group { .. } => writeln!(f, "{}", entry.name)?,
606                        TestInfo::AssertionTest { outcome, test_num } => match outcome {
607                            TestOutcome::AssertionPassed { assertion_count } => writeln!(
608                                f,
609                                "{:>3}. ✓ {} ({assertion_count} assertions)",
610                                test_num,
611                                paint(Some(AnsiColor::Green), &entry.name)
612                            )?,
613                            TestOutcome::AssertionFailed { error } => {
614                                writeln!(
615                                    f,
616                                    "{:>3}. ✗ {}",
617                                    test_num,
618                                    paint(Some(AnsiColor::Red), &entry.name)
619                                )?;
620                                writeln!(f, "{}  {error}", "  ".repeat(depth + 1))?;
621                            }
622                            _ => unreachable!(),
623                        },
624                        TestInfo::ParseTest { .. } => unreachable!(),
625                    }
626                }
627                Ok(())
628            };
629
630        if !self.highlight_results.root_group.is_empty() {
631            render_assertion_results("syntax highlighting", &self.highlight_results)?;
632        }
633
634        if !self.tag_results.root_group.is_empty() {
635            render_assertion_results("tags", &self.tag_results)?;
636        }
637
638        if !self.query_results.root_group.is_empty() {
639            render_assertion_results("queries", &self.query_results)?;
640        }
641
642        write!(f, "{}", self.parse_stats)?;
643
644        Ok(())
645    }
646}
647
648pub fn run_tests_at_path(
649    parser: &mut Parser,
650    opts: &TestOptions,
651    test_summary: &mut TestSummary,
652) -> Result<()> {
653    let test_entry = parse_tests(&opts.path)?;
654
655    let _log_session = if opts.debug_graph {
656        Some(util::log_graphs(parser, "log.html", opts.open_log)?)
657    } else {
658        None
659    };
660    if opts.debug {
661        parser.set_logger(Some(Box::new(|log_type, message| {
662            if log_type == LogType::Lex {
663                io::stderr().write_all(b"  ").unwrap();
664            }
665            writeln!(&mut io::stderr(), "{message}").unwrap();
666        })));
667    }
668
669    let mut corrected_entries = Vec::new();
670    run_tests(
671        parser,
672        test_entry,
673        opts,
674        test_summary,
675        &mut corrected_entries,
676        true,
677    )?;
678
679    parser.stop_printing_dot_graphs();
680
681    if test_summary.parse_failures.is_empty() || (opts.update && !test_summary.has_parse_errors) {
682        Ok(())
683    } else if opts.update && test_summary.has_parse_errors {
684        Err(anyhow!(indoc! {"
685                Some tests failed to parse with unexpected `ERROR` or `MISSING` nodes, as shown above, and cannot be updated automatically.
686                Either fix the grammar or manually update the tests if this is expected."}))
687    } else {
688        Err(anyhow!(""))
689    }
690}
691
692pub fn check_queries_at_path(language: &Language, path: &Path) -> Result<()> {
693    for entry in WalkDir::new(path)
694        .into_iter()
695        .filter_map(std::result::Result::ok)
696        .filter(|e| {
697            e.file_type().is_file()
698                && e.path().extension().and_then(OsStr::to_str) == Some("scm")
699                && !e.path().starts_with(".")
700        })
701    {
702        let filepath = entry.file_name().to_str().unwrap_or("");
703        let content = fs::read_to_string(entry.path())
704            .with_context(|| format!("Error reading query file {filepath:?}"))?;
705        Query::new(language, &content)
706            .with_context(|| format!("Error in query file {filepath:?}"))?;
707    }
708    Ok(())
709}
710
711pub struct DiffKey;
712
713impl std::fmt::Display for DiffKey {
714    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
715        write!(
716            f,
717            "\ncorrect / {} / {}",
718            paint(Some(AnsiColor::Green), "expected"),
719            paint(Some(AnsiColor::Red), "unexpected")
720        )?;
721        Ok(())
722    }
723}
724
725impl DiffKey {
726    /// Writes [`DiffKey`] to stdout
727    pub fn print() {
728        println!("{Self}");
729    }
730}
731
732pub struct TestDiff<'a> {
733    pub actual: &'a str,
734    pub expected: &'a str,
735    /// Force `+`/`-` markers even when color is enabled. Markers are always
736    /// shown when color is disabled, regardless of this flag.
737    pub use_markers: bool,
738}
739
740impl<'a> TestDiff<'a> {
741    #[must_use]
742    pub const fn new(actual: &'a str, expected: &'a str) -> Self {
743        Self {
744            actual,
745            expected,
746            use_markers: false,
747        }
748    }
749
750    #[must_use]
751    pub const fn with_markers(mut self, use_markers: bool) -> Self {
752        self.use_markers = use_markers;
753        self
754    }
755}
756
757impl std::fmt::Display for TestDiff<'_> {
758    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
759        let use_markers = !color_enabled() || self.use_markers;
760        let text_diff = TextDiff::from_lines(self.actual, self.expected);
761        for diff in text_diff.iter_all_changes() {
762            let tag = diff.tag();
763            let (symbol, color) = match tag {
764                ChangeTag::Equal => (' ', None),
765                ChangeTag::Insert => ('+', Some(AnsiColor::Green)),
766                ChangeTag::Delete => ('-', Some(AnsiColor::Red)),
767            };
768            match (color, use_markers) {
769                (Some(color), true) => {
770                    write!(f, "{}", paint(Some(color), format!("{symbol}{diff}")))?;
771                }
772                (Some(color), false) => {
773                    write!(f, "{}", paint(Some(color), diff))?;
774                }
775                (None, true) => write!(f, "{symbol}{diff}")?,
776                (None, false) => write!(f, "{diff}")?,
777            }
778        }
779
780        Ok(())
781    }
782}
783
784#[derive(Debug, Serialize, JsonSchema)]
785pub struct TestFailure {
786    name: String,
787    actual: String,
788    expected: String,
789    is_cst: bool,
790}
791
792impl TestFailure {
793    fn new<T, U, V>(name: T, actual: U, expected: V, is_cst: bool) -> Self
794    where
795        T: Into<String>,
796        U: Into<String>,
797        V: Into<String>,
798    {
799        Self {
800            name: name.into(),
801            actual: actual.into(),
802            expected: expected.into(),
803            is_cst,
804        }
805    }
806}
807
808struct TestCorrection {
809    name: String,
810    input: String,
811    output: String,
812    attributes_str: String,
813    header_delim_len: usize,
814    divider_delim_len: usize,
815}
816
817impl TestCorrection {
818    fn new<T, U, V, W>(
819        name: T,
820        input: U,
821        output: V,
822        attributes_str: W,
823        header_delim_len: usize,
824        divider_delim_len: usize,
825    ) -> Self
826    where
827        T: Into<String>,
828        U: Into<String>,
829        V: Into<String>,
830        W: Into<String>,
831    {
832        Self {
833            name: name.into(),
834            input: input.into(),
835            output: output.into(),
836            attributes_str: attributes_str.into(),
837            header_delim_len,
838            divider_delim_len,
839        }
840    }
841}
842
843/// This will return false if we want to "fail fast". It will bail and not parse any more tests.
844fn run_tests(
845    parser: &mut Parser,
846    test_entry: TestEntry,
847    opts: &TestOptions,
848    test_summary: &mut TestSummary,
849    corrected_entries: &mut Vec<TestCorrection>,
850    is_root: bool,
851) -> Result<bool> {
852    match test_entry {
853        TestEntry::Example {
854            name,
855            input,
856            output,
857            header_delim_len,
858            divider_delim_len,
859            has_fields,
860            attributes_str,
861            attributes,
862            ..
863        } => {
864            if attributes.skip() {
865                test_summary.parse_results.add_case(TestResult {
866                    name,
867                    info: TestInfo::ParseTest {
868                        outcome: TestOutcome::Skipped,
869                        parse_rate: None,
870                        test_num: test_summary.test_num,
871                    },
872                });
873                test_summary.test_num += 1;
874                return Ok(true);
875            }
876
877            if !attributes.platform {
878                test_summary.parse_results.add_case(TestResult {
879                    name,
880                    info: TestInfo::ParseTest {
881                        outcome: TestOutcome::Platform,
882                        parse_rate: None,
883                        test_num: test_summary.test_num,
884                    },
885                });
886                test_summary.test_num += 1;
887                return Ok(true);
888            }
889
890            for (i, language_name) in attributes.languages.iter().enumerate() {
891                if !language_name.is_empty() {
892                    let language = opts
893                        .languages
894                        .get(language_name.as_ref())
895                        .ok_or_else(|| anyhow!("Language not found: {language_name}"))?;
896                    parser.set_language(language)?;
897                }
898                let start = std::time::Instant::now();
899                let tree = parser.parse(&input, None).unwrap();
900                let parse_rate = {
901                    let parse_time = start.elapsed();
902                    let byte_len = tree.root_node().byte_range().len();
903                    let true_parse_rate =
904                        byte_len as f64 / (parse_time.as_nanos() as f64 / 1_000_000.0);
905                    let adj_parse_rate = adjusted_parse_rate(&tree, parse_time);
906
907                    test_summary.parse_stats.total_parses += 1;
908                    test_summary.parse_stats.total_duration += parse_time;
909                    test_summary.parse_stats.total_bytes += byte_len;
910
911                    Some((true_parse_rate, adj_parse_rate))
912                };
913
914                if attributes.error() {
915                    if tree.root_node().has_error() {
916                        test_summary.parse_results.add_case(TestResult {
917                            name: name.clone(),
918                            info: TestInfo::ParseTest {
919                                outcome: TestOutcome::Passed,
920                                parse_rate,
921                                test_num: test_summary.test_num,
922                            },
923                        });
924                        test_summary.parse_stats.successful_parses += 1;
925                        if opts.update {
926                            let input = String::from_utf8(input.clone()).unwrap();
927                            let output = if attributes.cst {
928                                output.clone()
929                            } else {
930                                format_sexp(&output, 0)
931                            };
932                            corrected_entries.push(TestCorrection::new(
933                                &name,
934                                input,
935                                output,
936                                &attributes_str,
937                                header_delim_len,
938                                divider_delim_len,
939                            ));
940                        }
941                    } else {
942                        if opts.update {
943                            let input = String::from_utf8(input.clone()).unwrap();
944                            // Keep the original `expected` output if the actual output has no error
945                            let output = if attributes.cst {
946                                output.clone()
947                            } else {
948                                format_sexp(&output, 0)
949                            };
950                            corrected_entries.push(TestCorrection::new(
951                                &name,
952                                input,
953                                output,
954                                &attributes_str,
955                                header_delim_len,
956                                divider_delim_len,
957                            ));
958                        }
959                        test_summary.parse_results.add_case(TestResult {
960                            name: name.clone(),
961                            info: TestInfo::ParseTest {
962                                outcome: TestOutcome::Failed,
963                                parse_rate,
964                                test_num: test_summary.test_num,
965                            },
966                        });
967                        let actual = render_test_output(&input, &tree, attributes.cst, true)?;
968                        test_summary.parse_failures.push(TestFailure::new(
969                            &name,
970                            actual,
971                            "NO ERROR",
972                            attributes.cst,
973                        ));
974                    }
975
976                    if attributes.fail_fast {
977                        return Ok(false);
978                    }
979                } else {
980                    let actual = render_test_output(
981                        &input,
982                        &tree,
983                        attributes.cst,
984                        opts.show_fields || has_fields,
985                    )?;
986
987                    if actual == output {
988                        test_summary.parse_results.add_case(TestResult {
989                            name: name.clone(),
990                            info: TestInfo::ParseTest {
991                                outcome: TestOutcome::Passed,
992                                parse_rate,
993                                test_num: test_summary.test_num,
994                            },
995                        });
996                        test_summary.parse_stats.successful_parses += 1;
997                        if opts.update {
998                            let input = String::from_utf8(input.clone()).unwrap();
999                            let output = if attributes.cst {
1000                                actual
1001                            } else {
1002                                format_sexp(&output, 0)
1003                            };
1004                            corrected_entries.push(TestCorrection::new(
1005                                &name,
1006                                input,
1007                                output,
1008                                &attributes_str,
1009                                header_delim_len,
1010                                divider_delim_len,
1011                            ));
1012                        }
1013                    } else {
1014                        if opts.update {
1015                            let input = String::from_utf8(input.clone()).unwrap();
1016                            let (expected_output, actual_output) = if attributes.cst {
1017                                (output.clone(), actual.clone())
1018                            } else {
1019                                (format_sexp(&output, 0), format_sexp(&actual, 0))
1020                            };
1021
1022                            // Only bail early before updating if `actual` does not match `output`.
1023                            // Sometimes users want to test cases that are intended to have
1024                            // errors, hence why this check isn't shown above.
1025                            if actual.contains("ERROR") || actual.contains("MISSING") {
1026                                test_summary.has_parse_errors = true;
1027
1028                                // keep the original `expected` output if the actual output has an
1029                                // error
1030                                corrected_entries.push(TestCorrection::new(
1031                                    &name,
1032                                    input,
1033                                    expected_output,
1034                                    &attributes_str,
1035                                    header_delim_len,
1036                                    divider_delim_len,
1037                                ));
1038                            } else {
1039                                corrected_entries.push(TestCorrection::new(
1040                                    &name,
1041                                    input,
1042                                    actual_output,
1043                                    &attributes_str,
1044                                    header_delim_len,
1045                                    divider_delim_len,
1046                                ));
1047                                test_summary.parse_results.add_case(TestResult {
1048                                    name: name.clone(),
1049                                    info: TestInfo::ParseTest {
1050                                        outcome: TestOutcome::Updated,
1051                                        parse_rate,
1052                                        test_num: test_summary.test_num,
1053                                    },
1054                                });
1055                            }
1056                        } else {
1057                            test_summary.parse_results.add_case(TestResult {
1058                                name: name.clone(),
1059                                info: TestInfo::ParseTest {
1060                                    outcome: TestOutcome::Failed,
1061                                    parse_rate,
1062                                    test_num: test_summary.test_num,
1063                                },
1064                            });
1065                        }
1066                        test_summary.parse_failures.push(TestFailure::new(
1067                            &name,
1068                            actual,
1069                            &output,
1070                            attributes.cst,
1071                        ));
1072
1073                        if attributes.fail_fast {
1074                            return Ok(false);
1075                        }
1076                    }
1077                }
1078
1079                if i == attributes.languages.len() - 1 {
1080                    // reset to the first language
1081                    parser.set_language(opts.languages.values().next().unwrap())?;
1082                }
1083            }
1084            test_summary.test_num += 1;
1085        }
1086        TestEntry::Group {
1087            name,
1088            children,
1089            file_path,
1090        } => {
1091            if children.is_empty() {
1092                return Ok(true);
1093            }
1094
1095            let mut ran_test_in_group = false;
1096
1097            let matches_filter = |name: &str, file_name: &Option<String>, opts: &TestOptions| {
1098                if let (Some(test_file_path), Some(filter_file_name)) = (file_name, &opts.file_name)
1099                    && !filter_file_name.eq(test_file_path)
1100                {
1101                    return false;
1102                }
1103                if let Some(include) = &opts.include {
1104                    include.is_match(name)
1105                } else if let Some(exclude) = &opts.exclude {
1106                    !exclude.is_match(name)
1107                } else {
1108                    true
1109                }
1110            };
1111
1112            for child in children {
1113                if let TestEntry::Example {
1114                    ref name,
1115                    ref file_name,
1116                    ref input,
1117                    ref output,
1118                    ref attributes_str,
1119                    header_delim_len,
1120                    divider_delim_len,
1121                    ..
1122                } = child
1123                    && !matches_filter(name, file_name, opts)
1124                {
1125                    if opts.update {
1126                        let input = String::from_utf8(input.clone()).unwrap();
1127                        let output = format_sexp(output, 0);
1128                        corrected_entries.push(TestCorrection::new(
1129                            name,
1130                            input,
1131                            output,
1132                            attributes_str,
1133                            header_delim_len,
1134                            divider_delim_len,
1135                        ));
1136                    }
1137
1138                    test_summary.test_num += 1;
1139                    continue;
1140                }
1141
1142                if !ran_test_in_group && !is_root {
1143                    test_summary.parse_results.add_group(&name);
1144                    ran_test_in_group = true;
1145                }
1146                if !run_tests(parser, child, opts, test_summary, corrected_entries, false)? {
1147                    // fail fast
1148                    return Ok(false);
1149                }
1150            }
1151            // Now that we're done traversing the children of the current group, pop
1152            // the index
1153            test_summary.parse_results.pop_traversal();
1154
1155            if let Some(file_path) = file_path {
1156                if opts.update {
1157                    write_tests(&file_path, corrected_entries)?;
1158                }
1159                corrected_entries.clear();
1160            }
1161        }
1162    }
1163    Ok(true)
1164}
1165
1166/// Convenience wrapper to render a CST for a test entry.
1167fn render_test_cst(input: &[u8], tree: &Tree) -> io::Result<String> {
1168    let mut rendered_cst: Vec<u8> = Vec::new();
1169    let mut cursor = tree.walk();
1170    let opts = ParseFileOptions {
1171        edits: &[],
1172        output: ParseOutput::Cst,
1173        stats: &mut ParseStats::default(),
1174        print_time: false,
1175        timeout: 0,
1176        debug: ParseDebugType::Quiet,
1177        debug_graph: false,
1178        cancellation_flag: None,
1179        encoding: None,
1180        open_log: false,
1181        no_ranges: false,
1182        parse_theme: &ParseTheme::empty(),
1183    };
1184    render_cst(input, tree, &mut cursor, &opts, &mut rendered_cst)?;
1185    Ok(String::from_utf8_lossy(&rendered_cst).trim().to_string())
1186}
1187
1188/// Render a parsed tree in the output format expected by a corpus test.
1189pub(crate) fn render_test_output(
1190    input: &[u8],
1191    tree: &Tree,
1192    cst: bool,
1193    include_fields: bool,
1194) -> io::Result<String> {
1195    if cst {
1196        render_test_cst(input, tree)
1197    } else {
1198        let out = tree.root_node().to_sexp();
1199        Ok(if include_fields {
1200            out
1201        } else {
1202            strip_sexp_fields(&out)
1203        })
1204    }
1205}
1206
1207// Parse time is interpreted in ns before converting to ms to avoid truncation issues
1208// Parse rates often have several outliers, leading to a large standard deviation. Taking
1209// the log of these rates serves to "flatten" out the distribution, yielding a more
1210// usable standard deviation for finding statistically significant slow parse rates
1211// NOTE: This is just a heuristic
1212#[must_use]
1213pub fn adjusted_parse_rate(tree: &Tree, parse_time: Duration) -> f64 {
1214    f64::ln(
1215        tree.root_node().byte_range().len() as f64 / (parse_time.as_nanos() as f64 / 1_000_000.0),
1216    )
1217}
1218
1219fn write_tests(file_path: &Path, corrected_entries: &[TestCorrection]) -> Result<()> {
1220    let mut buffer = fs::File::create(file_path)?;
1221    write_tests_to_buffer(&mut buffer, corrected_entries)
1222}
1223
1224fn write_tests_to_buffer(
1225    buffer: &mut impl Write,
1226    corrected_entries: &[TestCorrection],
1227) -> Result<()> {
1228    for (
1229        i,
1230        TestCorrection {
1231            name,
1232            input,
1233            output,
1234            attributes_str,
1235            header_delim_len,
1236            divider_delim_len,
1237        },
1238    ) in corrected_entries.iter().enumerate()
1239    {
1240        if i > 0 {
1241            writeln!(buffer)?;
1242        }
1243        writeln!(
1244            buffer,
1245            "{}\n{name}\n{}{}\n{input}\n{}\n\n{}",
1246            "=".repeat(*header_delim_len),
1247            if attributes_str.is_empty() {
1248                attributes_str.clone()
1249            } else {
1250                format!("{attributes_str}\n")
1251            },
1252            "=".repeat(*header_delim_len),
1253            "-".repeat(*divider_delim_len),
1254            output.trim()
1255        )?;
1256    }
1257    Ok(())
1258}
1259
1260pub fn parse_tests(path: &Path) -> io::Result<TestEntry> {
1261    let name = path
1262        .file_stem()
1263        .and_then(|s| s.to_str())
1264        .unwrap_or("")
1265        .to_string();
1266    if path.is_dir() {
1267        let mut children = Vec::new();
1268        for entry in fs::read_dir(path)? {
1269            let entry = entry?;
1270            let hidden = entry.file_name().to_str().unwrap_or("").starts_with('.');
1271            if !hidden {
1272                children.push(entry.path());
1273            }
1274        }
1275        children.sort_by(|a, b| {
1276            a.file_name()
1277                .unwrap_or_default()
1278                .cmp(b.file_name().unwrap_or_default())
1279        });
1280        let children = children
1281            .iter()
1282            .map(|path| parse_tests(path))
1283            .collect::<io::Result<Vec<TestEntry>>>()?;
1284        Ok(TestEntry::Group {
1285            name,
1286            children,
1287            file_path: None,
1288        })
1289    } else {
1290        let content = fs::read_to_string(path)?;
1291        Ok(parse_test_content(name, &content, Some(path.to_path_buf())))
1292    }
1293}
1294
1295/// Replace ` word: (` with ` (` throughout the string.
1296/// Intended to operate on `to_sexp()` output where elements are separated by single spaces.
1297#[must_use]
1298pub fn strip_sexp_fields(sexp: &str) -> String {
1299    let mut result = String::with_capacity(sexp.len());
1300    let mut remaining = sexp;
1301    while let Some(pos) = remaining.find(": (") {
1302        // Walk backwards from the `:` to find the field name and preceding space.
1303        if let Some(space_pos) = remaining[..pos].rfind(' ') {
1304            let word = &remaining[space_pos + 1..pos];
1305            if !word.is_empty() && word.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
1306                // Emit everything up to and including the space, then `(`
1307                result.push_str(&remaining[..=space_pos]);
1308                result.push('(');
1309                remaining = &remaining[pos + 3..];
1310                continue;
1311            }
1312        }
1313        // Not a field pattern — emit through `: (` and keep going
1314        result.push_str(&remaining[..pos + 3]);
1315        remaining = &remaining[pos + 3..];
1316    }
1317    result.push_str(remaining);
1318    result
1319}
1320
1321/// Remove `[row, col]` point annotations from sexp strings, including surrounding whitespace.
1322/// Matches the pattern: `\s* [ \s* digits \s* , \s* digits \s* ] \s*`
1323#[must_use]
1324pub fn strip_points(sexp: &str) -> String {
1325    let mut result = String::with_capacity(sexp.len());
1326    let mut skip_until = 0;
1327    for (i, c) in sexp.char_indices() {
1328        if i < skip_until {
1329            continue;
1330        }
1331        if let Some(point_len) = try_match_point(&sexp[i..]) {
1332            skip_until = i + point_len;
1333        } else {
1334            result.push(c);
1335        }
1336    }
1337    result
1338}
1339
1340/// Try to match `\s*[\s*\d+\s*,\s*\d+\s*]\s*` from the start of `text`.
1341/// Returns the length of the match, or `None` if no match.
1342fn try_match_point(text: &str) -> Option<usize> {
1343    let mut j = count_whitespace(text);
1344    j += expect_char(&text[j..], '[')?;
1345    j += count_whitespace(&text[j..]);
1346    j += expect_digits(&text[j..])?;
1347    j += count_whitespace(&text[j..]);
1348    j += expect_char(&text[j..], ',')?;
1349    j += count_whitespace(&text[j..]);
1350    j += expect_digits(&text[j..])?;
1351    j += count_whitespace(&text[j..]);
1352    j += expect_char(&text[j..], ']')?;
1353    Some(j + count_whitespace(&text[j..]))
1354}
1355
1356fn count_whitespace(text: &str) -> usize {
1357    text.char_indices()
1358        .take_while(|(_, c)| c.is_whitespace())
1359        .last()
1360        .map_or(0, |(i, c)| i + c.len_utf8())
1361}
1362
1363fn expect_char(text: &str, expected: char) -> Option<usize> {
1364    text.starts_with(expected).then_some(expected.len_utf8())
1365}
1366
1367fn expect_digits(text: &str) -> Option<usize> {
1368    let end = text
1369        .char_indices()
1370        .take_while(|(_, c)| c.is_ascii_digit())
1371        .last()
1372        .map(|(i, c)| i + c.len_utf8())?;
1373    Some(end)
1374}
1375
1376/// Check if a delimiter line's suffix matches the file's first suffix.
1377fn suffix_matches(first_suffix: Option<&str>, suffix: &str) -> bool {
1378    match (first_suffix, suffix.is_empty()) {
1379        (None, true) => true,
1380        (Some(fs), false) => fs == suffix,
1381        _ => false,
1382    }
1383}
1384
1385/// Parsed header info stored between iterations while we wait to discover the body boundaries.
1386struct PendingTest {
1387    name: String,
1388    attributes_str: String,
1389    header_delim_len: usize,
1390    attributes: TestAttributes,
1391    body_start_line: usize,
1392}
1393
1394/// If `token` matches the shape of one of the known test attributes,
1395/// then return the prefix
1396fn known_attribute(token: &str) -> Option<&str> {
1397    let head = token.split('(').next().unwrap_or(token);
1398    matches!(
1399        head,
1400        ":skip" | ":error" | ":fail-fast" | ":cst" | ":platform" | ":language"
1401    )
1402    .then_some(head)
1403}
1404
1405/// Try to parse a header block (opening `===`, name/markers, closing `===`) starting at
1406/// `lines[start_line]`. Returns the parsed header and the line index after the closing `===`,
1407/// or `None` if `lines[start_line]` isn't a matching `===` delimiter.
1408fn parse_header(
1409    lines: &[&str],
1410    first_suffix: Option<&str>,
1411    start_line: usize,
1412) -> Option<(PendingTest, usize)> {
1413    let (header_delim_len, suffix) = parse_delimiter_line(lines[start_line], '=')?;
1414    if !suffix_matches(first_suffix, suffix) {
1415        return None;
1416    }
1417
1418    // Collect name and attribute lines until the closing `===` line.
1419    let mut test_name = String::new();
1420    let mut seen_marker = false;
1421    let mut seen_skip = false;
1422    let mut seen_error = false;
1423    let (mut platform, mut fail_fast, mut cst, mut languages) = (None, false, false, vec![]);
1424
1425    let mut line_num = start_line + 1; // start past opening === line
1426    while line_num < lines.len() {
1427        if let Some((_, closing_suffix)) = parse_delimiter_line(lines[line_num], '=')
1428            && suffix_matches(first_suffix, closing_suffix)
1429        {
1430            break;
1431        }
1432        let trimmed = lines[line_num].trim();
1433        // Reject a blank line in the name region so a literal `===` inside a
1434        // test body can't be mistaken for an opening delimiter. Blank lines
1435        // between markers are allowed as visual separators.
1436        if trimmed.is_empty() && !seen_marker {
1437            return None;
1438        }
1439        match trimmed.split('(').next().unwrap() {
1440            ":skip" => (seen_marker, seen_skip) = (true, true),
1441            ":platform" => {
1442                if let Some(platforms) = trimmed.strip_prefix(':').and_then(|s| {
1443                    s.strip_prefix("platform(")
1444                        .and_then(|s| s.strip_suffix(')'))
1445                }) {
1446                    seen_marker = true;
1447                    platform =
1448                        Some(platform.unwrap_or(false) || platforms.trim() == std::env::consts::OS);
1449                }
1450            }
1451            ":fail-fast" => (seen_marker, fail_fast) = (true, true),
1452            ":error" => (seen_marker, seen_error) = (true, true),
1453            ":language" => {
1454                if let Some(lang) = trimmed.strip_prefix(':').and_then(|s| {
1455                    s.strip_prefix("language(")
1456                        .and_then(|s| s.strip_suffix(')'))
1457                }) {
1458                    seen_marker = true;
1459                    languages.push(lang.into());
1460                }
1461            }
1462            ":cst" => (seen_marker, cst) = (true, true),
1463            _ if !seen_marker => {
1464                // This line is part of the test name. If it contains a token that
1465                // looks like an attribute marker, warn the user.
1466                let mut warned = false;
1467                for token in trimmed.split_whitespace() {
1468                    if let Some(attr) = known_attribute(token) {
1469                        warn!(
1470                            "Test header line `{trimmed}` contains `{attr}`, \
1471                             which looks like a test attribute but won't be \
1472                             recognized as one. Attributes must appear on \
1473                             their own line(s) below the test name."
1474                        );
1475                        warned = true;
1476                    }
1477                }
1478                // A line that is itself a single `:` prefixed token and didn't
1479                // match any known marker is most likely a typo'd attribute.
1480                if !warned && trimmed.starts_with(':') && !trimmed.contains(char::is_whitespace) {
1481                    warn!("Test header line `{trimmed}` looks like a test attribute but isn't.");
1482                }
1483                test_name.push_str(lines[line_num]);
1484            }
1485            _ => {
1486                // In the marker region, lines that start with `:` but don't
1487                // match any known marker are most likely a typo.
1488                if trimmed.starts_with(':') {
1489                    warn!("Test header line `{trimmed}` looks like a test attribute but isn't.");
1490                }
1491            }
1492        }
1493        line_num += 1;
1494    }
1495
1496    if line_num >= lines.len() {
1497        warn!("No closing `===` line found for {}", test_name.trim_end());
1498        return None; // No closing `===` line found.
1499    }
1500
1501    let expectation = match (seen_skip, seen_error) {
1502        (true, true) => {
1503            warn!(
1504                "Test '{}' specifies both `:skip` and `:error`. The `:error` attribute will be dropped.",
1505                test_name.trim_end()
1506            );
1507            TestExpectation::Skip
1508        }
1509        (false, false) => TestExpectation::Pass,
1510        (true, false) => TestExpectation::Skip,
1511        (false, true) => TestExpectation::Error,
1512    };
1513
1514    // Build attributes string from the content between test name and closing delimiter.
1515    let name_and_markers: String = lines[start_line + 1..line_num].iter().copied().collect();
1516    let attributes_str = name_and_markers
1517        .strip_prefix(&test_name)
1518        .unwrap_or("")
1519        .trim_end()
1520        .to_string();
1521
1522    if languages.is_empty() {
1523        languages.push("".into());
1524    }
1525
1526    let pending = PendingTest {
1527        name: test_name.trim_end().to_string(),
1528        attributes_str,
1529        header_delim_len,
1530        attributes: TestAttributes {
1531            platform: platform.unwrap_or(true),
1532            fail_fast,
1533            expectation,
1534            cst,
1535            languages,
1536        },
1537        body_start_line: line_num + 1,
1538    };
1539
1540    Some((pending, line_num + 1)) // +1 to consume the closing `===` line
1541}
1542
1543fn parse_test_content(name: String, content: &str, file_path: Option<PathBuf>) -> TestEntry {
1544    let mut children = Vec::new();
1545    let lines = content.split_inclusive('\n').collect::<Vec<_>>();
1546
1547    // Determine the suffix from the first `===` line in the file.
1548    let first_suffix = lines
1549        .iter()
1550        .find_map(|line| match parse_delimiter_line(line, '=')? {
1551            (_, suffix) if !suffix.is_empty() => Some(suffix.to_string()),
1552            _ => None,
1553        });
1554
1555    // Scan for header blocks and build test entries from the bodies between them.
1556    let mut line_num = 0;
1557    let mut prev_test: Option<PendingTest> = None;
1558
1559    while line_num < lines.len() {
1560        let Some((pending, body_start_line)) =
1561            parse_header(&lines, first_suffix.as_deref(), line_num)
1562        else {
1563            line_num += 1;
1564            continue;
1565        };
1566
1567        let opening_line = line_num;
1568        line_num = body_start_line;
1569
1570        // Process the PREVIOUS test's body now that we know where it ends.
1571        if let Some(prev) = prev_test
1572            && let Some(entry) = build_test_entry(
1573                &lines[prev.body_start_line..opening_line],
1574                first_suffix.as_deref(),
1575                prev,
1576                file_path.as_deref(),
1577            )
1578        {
1579            children.push(entry);
1580        }
1581
1582        prev_test = Some(pending);
1583    }
1584
1585    // Process the last test's body (terminated by end of content).
1586    if let Some(prev) = prev_test
1587        && let Some(entry) = build_test_entry(
1588            &lines[prev.body_start_line..],
1589            first_suffix.as_deref(),
1590            prev,
1591            file_path.as_deref(),
1592        )
1593    {
1594        children.push(entry);
1595    }
1596
1597    TestEntry::Group {
1598        name,
1599        children,
1600        file_path,
1601    }
1602}
1603
1604/// Build a single test entry from the body lines between a header and the next header.
1605/// Finds the longest matching `---` divider to separate input from expected output.
1606fn build_test_entry(
1607    body_lines: &[&str],
1608    first_suffix: Option<&str>,
1609    pending: PendingTest,
1610    file_path: Option<&Path>,
1611) -> Option<TestEntry> {
1612    // Find the longest `---` divider line in the body whose suffix matches.
1613    let mut best_divider: Option<(usize, usize)> = None; // (delim_len, line_index)
1614    let mut best_total_len = 0;
1615    for (j, line) in body_lines.iter().enumerate() {
1616        if let Some((delim_len, suffix)) = parse_delimiter_line(line, '-')
1617            && suffix_matches(first_suffix, suffix)
1618        {
1619            let total_len = delim_len + suffix.len();
1620            // For ties prefer the later candidate, as an earlier same-length
1621            // `---` is a literal in the input.
1622            if total_len >= best_total_len {
1623                best_divider = Some((delim_len, j));
1624                best_total_len = total_len;
1625            }
1626        }
1627    }
1628
1629    let (divider_delim_len, divider_line) = best_divider?;
1630
1631    // Input: lines before the divider (as bytes), with trailing newline stripped.
1632    let mut input = body_lines[..divider_line]
1633        .iter()
1634        .flat_map(|l| l.as_bytes())
1635        .copied()
1636        .collect::<Vec<_>>();
1637    // Remove trailing newline.
1638    if input.last() == Some(&b'\n') {
1639        input.pop();
1640    }
1641    if input.last() == Some(&b'\r') {
1642        input.pop();
1643    }
1644
1645    // Output: lines after the divider.
1646    let output_str = body_lines[divider_line + 1..]
1647        .iter()
1648        .copied()
1649        .collect::<String>();
1650
1651    let (output, has_fields) = if pending.attributes.cst {
1652        (output_str.trim().to_string(), false)
1653    } else {
1654        normalize_sexp_output(&output_str)
1655    };
1656
1657    let file_name = file_path
1658        .and_then(|p| p.file_name())
1659        .map(|n| n.to_string_lossy().to_string());
1660
1661    Some(TestEntry::Example {
1662        name: pending.name,
1663        input,
1664        output,
1665        header_delim_len: pending.header_delim_len,
1666        divider_delim_len,
1667        has_fields,
1668        attributes_str: pending.attributes_str,
1669        attributes: pending.attributes,
1670        file_name,
1671    })
1672}
1673
1674#[cfg(test)]
1675mod tests {
1676    use serde_json::json;
1677
1678    use crate::tests::get_language;
1679
1680    use super::*;
1681
1682    #[test]
1683    fn test_parse_test_content_simple() {
1684        let entry = parse_test_content(
1685            "the-filename".to_string(),
1686            r"
1687===============
1688The first test
1689===============
1690
1691a b c
1692
1693---
1694
1695(a
1696    (b c))
1697
1698================
1699The second test
1700================
1701d
1702---
1703(d)
1704        "
1705            .trim(),
1706            None,
1707        );
1708
1709        assert_eq!(
1710            entry,
1711            TestEntry::Group {
1712                name: "the-filename".to_string(),
1713                children: vec![
1714                    TestEntry::Example {
1715                        name: "The first test".to_string(),
1716                        input: b"\na b c\n".to_vec(),
1717                        output: "(a (b c))".to_string(),
1718                        header_delim_len: 15,
1719                        divider_delim_len: 3,
1720                        has_fields: false,
1721                        attributes_str: String::new(),
1722                        attributes: TestAttributes::default(),
1723                        file_name: None,
1724                    },
1725                    TestEntry::Example {
1726                        name: "The second test".to_string(),
1727                        input: b"d".to_vec(),
1728                        output: "(d)".to_string(),
1729                        header_delim_len: 16,
1730                        divider_delim_len: 3,
1731                        has_fields: false,
1732                        attributes_str: String::new(),
1733                        attributes: TestAttributes::default(),
1734                        file_name: None,
1735                    },
1736                ],
1737                file_path: None,
1738            }
1739        );
1740    }
1741
1742    #[test]
1743    fn test_parse_test_content_with_dashes_in_source_code() {
1744        let entry = parse_test_content(
1745            "the-filename".to_string(),
1746            r"
1747==================
1748Code with dashes
1749==================
1750abc
1751---
1752defg
1753----
1754hijkl
1755-------
1756
1757(a (b))
1758
1759=========================
1760Code ending with dashes
1761=========================
1762abc
1763-----------
1764-------------------
1765
1766(c (d))
1767        "
1768            .trim(),
1769            None,
1770        );
1771
1772        assert_eq!(
1773            entry,
1774            TestEntry::Group {
1775                name: "the-filename".to_string(),
1776                children: vec![
1777                    TestEntry::Example {
1778                        name: "Code with dashes".to_string(),
1779                        input: b"abc\n---\ndefg\n----\nhijkl".to_vec(),
1780                        output: "(a (b))".to_string(),
1781                        header_delim_len: 18,
1782                        divider_delim_len: 7,
1783                        has_fields: false,
1784                        attributes_str: String::new(),
1785                        attributes: TestAttributes::default(),
1786                        file_name: None,
1787                    },
1788                    TestEntry::Example {
1789                        name: "Code ending with dashes".to_string(),
1790                        input: b"abc\n-----------".to_vec(),
1791                        output: "(c (d))".to_string(),
1792                        header_delim_len: 25,
1793                        divider_delim_len: 19,
1794                        has_fields: false,
1795                        attributes_str: String::new(),
1796                        attributes: TestAttributes::default(),
1797                        file_name: None,
1798                    },
1799                ],
1800                file_path: None,
1801            }
1802        );
1803    }
1804
1805    #[test]
1806    fn test_parse_test_content_with_equals_in_source_code() {
1807        // A literal `===` inside a test body must not be mistaken for an
1808        // opening header
1809        let entry = parse_test_content(
1810            "the-filename".to_string(),
1811            r"
1812==========
1813First
1814==========
1815a
1816===
1817b
1818---
1819(a)
1820
1821==========
1822Second
1823==========
1824c
1825---
1826(c)
1827        "
1828            .trim(),
1829            None,
1830        );
1831
1832        assert_eq!(
1833            entry,
1834            TestEntry::Group {
1835                name: "the-filename".to_string(),
1836                children: vec![
1837                    TestEntry::Example {
1838                        name: "First".to_string(),
1839                        input: b"a\n===\nb".to_vec(),
1840                        output: "(a)".to_string(),
1841                        header_delim_len: 10,
1842                        divider_delim_len: 3,
1843                        has_fields: false,
1844                        attributes_str: String::new(),
1845                        attributes: TestAttributes::default(),
1846                        file_name: None,
1847                    },
1848                    TestEntry::Example {
1849                        name: "Second".to_string(),
1850                        input: b"c".to_vec(),
1851                        output: "(c)".to_string(),
1852                        header_delim_len: 10,
1853                        divider_delim_len: 3,
1854                        has_fields: false,
1855                        attributes_str: String::new(),
1856                        attributes: TestAttributes::default(),
1857                        file_name: None,
1858                    },
1859                ],
1860                file_path: None,
1861            }
1862        );
1863    }
1864
1865    #[test]
1866    fn test_parse_test_content_with_tied_divider_length() {
1867        // When two `---` lines in a body have the same length, the real
1868        // divider is the last one.
1869        let entry = parse_test_content(
1870            "the-filename".to_string(),
1871            r"
1872==========
1873Tied dashes
1874==========
1875a
1876---
1877b
1878---
1879(c)
1880        "
1881            .trim(),
1882            None,
1883        );
1884
1885        assert_eq!(
1886            entry,
1887            TestEntry::Group {
1888                name: "the-filename".to_string(),
1889                children: vec![TestEntry::Example {
1890                    name: "Tied dashes".to_string(),
1891                    input: b"a\n---\nb".to_vec(),
1892                    output: "(c)".to_string(),
1893                    header_delim_len: 10,
1894                    divider_delim_len: 3,
1895                    has_fields: false,
1896                    attributes_str: String::new(),
1897                    attributes: TestAttributes::default(),
1898                    file_name: None,
1899                }],
1900                file_path: None,
1901            }
1902        );
1903    }
1904
1905    #[test]
1906    fn test_format_sexp() {
1907        assert_eq!(format_sexp("", 0), "");
1908        assert_eq!(
1909            format_sexp("(a b: (c) (d) e: (f (g (h (MISSING i)))))", 0),
1910            r"
1911(a
1912  b: (c)
1913  (d)
1914  e: (f
1915    (g
1916      (h
1917        (MISSING i)))))
1918"
1919            .trim()
1920        );
1921        assert_eq!(
1922            format_sexp("(program (ERROR (UNEXPECTED ' ')) (identifier))", 0),
1923            r"
1924(program
1925  (ERROR
1926    (UNEXPECTED ' '))
1927  (identifier))
1928"
1929            .trim()
1930        );
1931        assert_eq!(
1932            format_sexp(r#"(source_file (MISSING ")"))"#, 0),
1933            r#"
1934(source_file
1935  (MISSING ")"))
1936        "#
1937            .trim()
1938        );
1939        assert_eq!(
1940            format_sexp(
1941                r"(source_file (ERROR (UNEXPECTED 'f') (UNEXPECTED '+')))",
1942                0
1943            ),
1944            r"
1945(source_file
1946  (ERROR
1947    (UNEXPECTED 'f')
1948    (UNEXPECTED '+')))
1949"
1950            .trim()
1951        );
1952    }
1953
1954    #[test]
1955    fn test_write_tests_to_buffer() {
1956        let mut buffer = Vec::new();
1957        let corrected_entries = vec![
1958            TestCorrection::new(
1959                "title 1".to_string(),
1960                "input 1".to_string(),
1961                "output 1".to_string(),
1962                String::new(),
1963                80,
1964                80,
1965            ),
1966            TestCorrection::new(
1967                "title 2".to_string(),
1968                "input 2".to_string(),
1969                "output 2".to_string(),
1970                String::new(),
1971                80,
1972                80,
1973            ),
1974        ];
1975        write_tests_to_buffer(&mut buffer, &corrected_entries).unwrap();
1976        assert_eq!(
1977            String::from_utf8(buffer).unwrap(),
1978            r"
1979================================================================================
1980title 1
1981================================================================================
1982input 1
1983--------------------------------------------------------------------------------
1984
1985output 1
1986
1987================================================================================
1988title 2
1989================================================================================
1990input 2
1991--------------------------------------------------------------------------------
1992
1993output 2
1994"
1995            .trim_start()
1996            .to_string()
1997        );
1998    }
1999
2000    #[test]
2001    fn test_parse_test_content_with_comments_in_sexp() {
2002        let entry = parse_test_content(
2003            "the-filename".to_string(),
2004            r#"
2005==================
2006sexp with comment
2007==================
2008code
2009---
2010
2011; Line start comment
2012(a (b))
2013
2014==================
2015sexp with comment between
2016==================
2017code
2018---
2019
2020; Line start comment
2021(a
2022; ignore this
2023    (b)
2024    ; also ignore this
2025)
2026
2027=========================
2028sexp with ';'
2029=========================
2030code
2031---
2032
2033(MISSING ";")
2034        "#
2035            .trim(),
2036            None,
2037        );
2038
2039        assert_eq!(
2040            entry,
2041            TestEntry::Group {
2042                name: "the-filename".to_string(),
2043                children: vec![
2044                    TestEntry::Example {
2045                        name: "sexp with comment".to_string(),
2046                        input: b"code".to_vec(),
2047                        output: "(a (b))".to_string(),
2048                        header_delim_len: 18,
2049                        divider_delim_len: 3,
2050                        has_fields: false,
2051                        attributes_str: String::new(),
2052                        attributes: TestAttributes::default(),
2053                        file_name: None,
2054                    },
2055                    TestEntry::Example {
2056                        name: "sexp with comment between".to_string(),
2057                        input: b"code".to_vec(),
2058                        output: "(a (b))".to_string(),
2059                        header_delim_len: 18,
2060                        divider_delim_len: 3,
2061                        has_fields: false,
2062                        attributes_str: String::new(),
2063                        attributes: TestAttributes::default(),
2064                        file_name: None,
2065                    },
2066                    TestEntry::Example {
2067                        name: "sexp with ';'".to_string(),
2068                        input: b"code".to_vec(),
2069                        output: "(MISSING \";\")".to_string(),
2070                        header_delim_len: 25,
2071                        divider_delim_len: 3,
2072                        has_fields: false,
2073                        attributes_str: String::new(),
2074                        attributes: TestAttributes::default(),
2075                        file_name: None,
2076                    }
2077                ],
2078                file_path: None,
2079            }
2080        );
2081    }
2082
2083    #[test]
2084    fn test_parse_test_content_with_suffixes() {
2085        let entry = parse_test_content(
2086            "the-filename".to_string(),
2087            r"
2088==================asdf\()[]|{}*+?^$.-
2089First test
2090==================asdf\()[]|{}*+?^$.-
2091
2092=========================
2093NOT A TEST HEADER
2094=========================
2095-------------------------
2096
2097---asdf\()[]|{}*+?^$.-
2098
2099(a)
2100
2101==================asdf\()[]|{}*+?^$.-
2102Second test
2103==================asdf\()[]|{}*+?^$.-
2104
2105=========================
2106NOT A TEST HEADER
2107=========================
2108-------------------------
2109
2110---asdf\()[]|{}*+?^$.-
2111
2112(a)
2113
2114=========================asdf\()[]|{}*+?^$.-
2115Test name with = symbol
2116=========================asdf\()[]|{}*+?^$.-
2117
2118=========================
2119NOT A TEST HEADER
2120=========================
2121-------------------------
2122
2123---asdf\()[]|{}*+?^$.-
2124
2125(a)
2126
2127==============================asdf\()[]|{}*+?^$.-
2128Test containing equals
2129==============================asdf\()[]|{}*+?^$.-
2130
2131===
2132
2133------------------------------asdf\()[]|{}*+?^$.-
2134
2135(a)
2136
2137==============================asdf\()[]|{}*+?^$.-
2138Subsequent test containing equals
2139==============================asdf\()[]|{}*+?^$.-
2140
2141===
2142
2143------------------------------asdf\()[]|{}*+?^$.-
2144
2145(a)
2146"
2147            .trim(),
2148            None,
2149        );
2150
2151        let expected_input = b"\n=========================\n\
2152            NOT A TEST HEADER\n\
2153            =========================\n\
2154            -------------------------\n"
2155            .to_vec();
2156        pretty_assertions::assert_eq!(
2157            entry,
2158            TestEntry::Group {
2159                name: "the-filename".to_string(),
2160                children: vec![
2161                    TestEntry::Example {
2162                        name: "First test".to_string(),
2163                        input: expected_input.clone(),
2164                        output: "(a)".to_string(),
2165                        header_delim_len: 18,
2166                        divider_delim_len: 3,
2167                        has_fields: false,
2168                        attributes_str: String::new(),
2169                        attributes: TestAttributes::default(),
2170                        file_name: None,
2171                    },
2172                    TestEntry::Example {
2173                        name: "Second test".to_string(),
2174                        input: expected_input.clone(),
2175                        output: "(a)".to_string(),
2176                        header_delim_len: 18,
2177                        divider_delim_len: 3,
2178                        has_fields: false,
2179                        attributes_str: String::new(),
2180                        attributes: TestAttributes::default(),
2181                        file_name: None,
2182                    },
2183                    TestEntry::Example {
2184                        name: "Test name with = symbol".to_string(),
2185                        input: expected_input,
2186                        output: "(a)".to_string(),
2187                        header_delim_len: 25,
2188                        divider_delim_len: 3,
2189                        has_fields: false,
2190                        attributes_str: String::new(),
2191                        attributes: TestAttributes::default(),
2192                        file_name: None,
2193                    },
2194                    TestEntry::Example {
2195                        name: "Test containing equals".to_string(),
2196                        input: "\n===\n".into(),
2197                        output: "(a)".into(),
2198                        header_delim_len: 30,
2199                        divider_delim_len: 30,
2200                        has_fields: false,
2201                        attributes_str: String::new(),
2202                        attributes: TestAttributes::default(),
2203                        file_name: None,
2204                    },
2205                    TestEntry::Example {
2206                        name: "Subsequent test containing equals".to_string(),
2207                        input: "\n===\n".into(),
2208                        output: "(a)".into(),
2209                        header_delim_len: 30,
2210                        divider_delim_len: 30,
2211                        has_fields: false,
2212                        attributes_str: String::new(),
2213                        attributes: TestAttributes::default(),
2214                        file_name: None,
2215                    }
2216                ],
2217                file_path: None,
2218            }
2219        );
2220    }
2221
2222    #[test]
2223    fn test_parse_test_content_with_newlines_in_test_names() {
2224        let entry = parse_test_content(
2225            "the-filename".to_string(),
2226            r"
2227===============
2228name
2229with
2230newlines
2231===============
2232a
2233---
2234(b)
2235
2236====================
2237name with === signs
2238====================
2239code with ----
2240---
2241(d)
2242",
2243            None,
2244        );
2245
2246        assert_eq!(
2247            entry,
2248            TestEntry::Group {
2249                name: "the-filename".to_string(),
2250                file_path: None,
2251                children: vec![
2252                    TestEntry::Example {
2253                        name: "name\nwith\nnewlines".to_string(),
2254                        input: b"a".to_vec(),
2255                        output: "(b)".to_string(),
2256                        header_delim_len: 15,
2257                        divider_delim_len: 3,
2258                        has_fields: false,
2259                        attributes_str: String::new(),
2260                        attributes: TestAttributes::default(),
2261                        file_name: None,
2262                    },
2263                    TestEntry::Example {
2264                        name: "name with === signs".to_string(),
2265                        input: b"code with ----".to_vec(),
2266                        output: "(d)".to_string(),
2267                        header_delim_len: 20,
2268                        divider_delim_len: 3,
2269                        has_fields: false,
2270                        attributes_str: String::new(),
2271                        attributes: TestAttributes::default(),
2272                        file_name: None,
2273                    }
2274                ]
2275            }
2276        );
2277    }
2278
2279    #[test]
2280    fn test_parse_test_with_markers() {
2281        // do one with :skip, we should not see it in the entry output
2282
2283        let entry = parse_test_content(
2284            "the-filename".to_string(),
2285            r"
2286=====================
2287Test with skip marker
2288:skip
2289=====================
2290a
2291---
2292(b)
2293",
2294            None,
2295        );
2296
2297        assert_eq!(
2298            entry,
2299            TestEntry::Group {
2300                name: "the-filename".to_string(),
2301                file_path: None,
2302                children: vec![TestEntry::Example {
2303                    name: "Test with skip marker".to_string(),
2304                    input: b"a".to_vec(),
2305                    output: "(b)".to_string(),
2306                    header_delim_len: 21,
2307                    divider_delim_len: 3,
2308                    has_fields: false,
2309                    attributes_str: ":skip".to_string(),
2310                    attributes: TestAttributes {
2311                        platform: true,
2312                        fail_fast: false,
2313                        expectation: TestExpectation::Skip,
2314                        cst: false,
2315                        languages: vec!["".into()]
2316                    },
2317                    file_name: None,
2318                }]
2319            }
2320        );
2321
2322        let entry = parse_test_content(
2323            "the-filename".to_string(),
2324            &format!(
2325                r"
2326=========================
2327Test with platform marker
2328:platform({})
2329:fail-fast
2330=========================
2331a
2332---
2333(b)
2334
2335=============================
2336Test with bad platform marker
2337:platform({})
2338
2339:language(foo)
2340=============================
2341a
2342---
2343(b)
2344
2345====================
2346Test with cst marker
2347:cst
2348====================
23491
2350---
23510:0 - 1:0   source_file
23520:0 - 0:1   expression
23530:0 - 0:1     number_literal `1`
2354",
2355                std::env::consts::OS,
2356                if std::env::consts::OS == "linux" {
2357                    "macos"
2358                } else {
2359                    "linux"
2360                }
2361            ),
2362            None,
2363        );
2364
2365        assert_eq!(
2366            entry,
2367            TestEntry::Group {
2368                name: "the-filename".to_string(),
2369                file_path: None,
2370                children: vec![
2371                    TestEntry::Example {
2372                        name: "Test with platform marker".to_string(),
2373                        input: b"a".to_vec(),
2374                        output: "(b)".to_string(),
2375                        header_delim_len: 25,
2376                        divider_delim_len: 3,
2377                        has_fields: false,
2378                        attributes_str: format!(":platform({})\n:fail-fast", std::env::consts::OS),
2379                        attributes: TestAttributes {
2380                            platform: true,
2381                            fail_fast: true,
2382                            expectation: TestExpectation::Pass,
2383                            cst: false,
2384                            languages: vec!["".into()]
2385                        },
2386                        file_name: None,
2387                    },
2388                    TestEntry::Example {
2389                        name: "Test with bad platform marker".to_string(),
2390                        input: b"a".to_vec(),
2391                        output: "(b)".to_string(),
2392                        header_delim_len: 29,
2393                        divider_delim_len: 3,
2394                        has_fields: false,
2395                        attributes_str: if std::env::consts::OS == "linux" {
2396                            ":platform(macos)\n\n:language(foo)".to_string()
2397                        } else {
2398                            ":platform(linux)\n\n:language(foo)".to_string()
2399                        },
2400                        attributes: TestAttributes {
2401                            platform: false,
2402                            fail_fast: false,
2403                            expectation: TestExpectation::Pass,
2404                            cst: false,
2405                            languages: vec!["foo".into()]
2406                        },
2407                        file_name: None,
2408                    },
2409                    TestEntry::Example {
2410                        name: "Test with cst marker".to_string(),
2411                        input: b"1".to_vec(),
2412                        output: "0:0 - 1:0   source_file
24130:0 - 0:1   expression
24140:0 - 0:1     number_literal `1`"
2415                            .to_string(),
2416                        header_delim_len: 20,
2417                        divider_delim_len: 3,
2418                        has_fields: false,
2419                        attributes_str: ":cst".to_string(),
2420                        attributes: TestAttributes {
2421                            platform: true,
2422                            fail_fast: false,
2423                            expectation: TestExpectation::Pass,
2424                            cst: true,
2425                            languages: vec!["".into()]
2426                        },
2427                        file_name: None,
2428                    }
2429                ]
2430            }
2431        );
2432    }
2433
2434    fn clear_parse_rate(result: &mut TestResult) {
2435        let test_case_info = &mut result.info;
2436        match test_case_info {
2437            TestInfo::ParseTest { parse_rate, .. } => {
2438                assert!(parse_rate.is_some());
2439                *parse_rate = None;
2440            }
2441            TestInfo::Group { .. } | TestInfo::AssertionTest { .. } => {
2442                panic!("Unexpected test result")
2443            }
2444        }
2445    }
2446
2447    fn c_parser_and_language() -> (Parser, Language) {
2448        let mut parser = Parser::new();
2449        let language = get_language("c");
2450        parser
2451            .set_language(&language)
2452            .expect("Failed to set language");
2453        (parser, language)
2454    }
2455
2456    fn c_test_options(language: &Language) -> TestOptions<'_> {
2457        let mut languages = BTreeMap::new();
2458        languages.insert("c", language);
2459        TestOptions {
2460            path: PathBuf::from("foo"),
2461            debug: true,
2462            debug_graph: false,
2463            include: None,
2464            exclude: None,
2465            file_name: None,
2466            update: false,
2467            open_log: false,
2468            languages,
2469            show_fields: false,
2470            overview_only: false,
2471        }
2472    }
2473
2474    #[test]
2475    fn run_tests_single_passing() {
2476        let (mut parser, language) = c_parser_and_language();
2477        let opts = c_test_options(&language);
2478
2479        let test_entry = TestEntry::Group {
2480            name: "foo".to_string(),
2481            file_path: None,
2482            children: vec![TestEntry::Example {
2483                name: "C Test 1".to_string(),
2484                input: b"1;\n".to_vec(),
2485                output: "(translation_unit (expression_statement (number_literal)))".to_string(),
2486                header_delim_len: 25,
2487                divider_delim_len: 3,
2488                has_fields: false,
2489                attributes_str: String::new(),
2490                attributes: TestAttributes::default(),
2491                file_name: None,
2492            }],
2493        };
2494
2495        let mut test_summary = TestSummary::new(TestStats::All, false, false, false);
2496        let mut corrected_entries = Vec::new();
2497        run_tests(
2498            &mut parser,
2499            test_entry,
2500            &opts,
2501            &mut test_summary,
2502            &mut corrected_entries,
2503            true,
2504        )
2505        .expect("Failed to run tests");
2506
2507        // parse rates will always be different, so we need to clear out these
2508        // fields to reliably assert equality below
2509        clear_parse_rate(&mut test_summary.parse_results.root_group[0]);
2510        test_summary.parse_stats.total_duration = Duration::from_secs(0);
2511
2512        let json_results = serde_json::to_string(&test_summary).unwrap();
2513
2514        assert_eq!(
2515            json_results,
2516            json!({
2517              "parse_results": [
2518                {
2519                  "name": "C Test 1",
2520                  "outcome": "Passed",
2521                  "parse_rate": null,
2522                  "test_num": 1
2523                }
2524              ],
2525              "parse_failures": [],
2526              "parse_stats": {
2527                "successful_parses": 1,
2528                "total_parses": 1,
2529                "total_bytes": 3,
2530                "total_duration": {
2531                  "secs": 0,
2532                  "nanos": 0,
2533                }
2534              },
2535              "highlight_results": [],
2536              "tag_results": [],
2537              "query_results": []
2538            })
2539            .to_string()
2540        );
2541    }
2542
2543    #[test]
2544    fn run_tests_fail_fast() {
2545        let (mut parser, language) = c_parser_and_language();
2546        let opts = c_test_options(&language);
2547
2548        let test_entry = TestEntry::Group {
2549            name: "corpus".to_string(),
2550            file_path: None,
2551            children: vec![
2552                TestEntry::Group {
2553                    name: "group1".to_string(),
2554                    // This test passes
2555                    children: vec![TestEntry::Example {
2556                        name: "C Test 1".to_string(),
2557                        input: b"1;\n".to_vec(),
2558                        output: "(translation_unit (expression_statement (number_literal)))"
2559                            .to_string(),
2560                        header_delim_len: 25,
2561                        divider_delim_len: 3,
2562                        has_fields: false,
2563                        attributes_str: String::new(),
2564                        attributes: TestAttributes::default(),
2565                        file_name: None,
2566                    }],
2567                    file_path: None,
2568                },
2569                TestEntry::Group {
2570                    name: "group2".to_string(),
2571                    children: vec![
2572                        // This test passes
2573                        TestEntry::Example {
2574                            name: "C Test 2".to_string(),
2575                            input: b"1;\n".to_vec(),
2576                            output: "(translation_unit (expression_statement (number_literal)))"
2577                                .to_string(),
2578                            header_delim_len: 25,
2579                            divider_delim_len: 3,
2580                            has_fields: false,
2581                            attributes_str: String::new(),
2582                            attributes: TestAttributes::default(),
2583                            file_name: None,
2584                        },
2585                        // This test fails, and is marked with fail-fast
2586                        TestEntry::Example {
2587                            name: "C Test 3".to_string(),
2588                            input: b"1;\n".to_vec(),
2589                            output: "(translation_unit (expression_statement (string_literal)))"
2590                                .to_string(),
2591                            header_delim_len: 25,
2592                            divider_delim_len: 3,
2593                            has_fields: false,
2594                            attributes_str: String::new(),
2595                            attributes: TestAttributes {
2596                                fail_fast: true,
2597                                ..Default::default()
2598                            },
2599                            file_name: None,
2600                        },
2601                    ],
2602                    file_path: None,
2603                },
2604                // This group never runs because of the previous failure
2605                TestEntry::Group {
2606                    name: "group3".to_string(),
2607                    // This test fails, and is marked with fail-fast
2608                    children: vec![TestEntry::Example {
2609                        name: "C Test 4".to_string(),
2610                        input: b"1;\n".to_vec(),
2611                        output: "(translation_unit (expression_statement (number_literal)))"
2612                            .to_string(),
2613                        header_delim_len: 25,
2614                        divider_delim_len: 3,
2615                        has_fields: false,
2616                        attributes_str: String::new(),
2617                        attributes: TestAttributes::default(),
2618                        file_name: None,
2619                    }],
2620                    file_path: None,
2621                },
2622            ],
2623        };
2624
2625        let mut test_summary = TestSummary::new(TestStats::All, false, false, false);
2626        let mut corrected_entries = Vec::new();
2627        run_tests(
2628            &mut parser,
2629            test_entry,
2630            &opts,
2631            &mut test_summary,
2632            &mut corrected_entries,
2633            true,
2634        )
2635        .expect("Failed to run tests");
2636
2637        // parse rates will always be different, so we need to clear out these
2638        // fields to reliably assert equality below
2639        {
2640            let test_group_1_info = &mut test_summary.parse_results.root_group[0].info;
2641            match test_group_1_info {
2642                TestInfo::Group { children, .. } => clear_parse_rate(&mut children[0]),
2643                TestInfo::ParseTest { .. } | TestInfo::AssertionTest { .. } => {
2644                    panic!("Unexpected test result");
2645                }
2646            }
2647            let test_group_2_info = &mut test_summary.parse_results.root_group[1].info;
2648            match test_group_2_info {
2649                TestInfo::Group { children, .. } => {
2650                    clear_parse_rate(&mut children[0]);
2651                    clear_parse_rate(&mut children[1]);
2652                }
2653                TestInfo::ParseTest { .. } | TestInfo::AssertionTest { .. } => {
2654                    panic!("Unexpected test result");
2655                }
2656            }
2657            test_summary.parse_stats.total_duration = Duration::from_secs(0);
2658        }
2659
2660        let json_results = serde_json::to_string(&test_summary).unwrap();
2661
2662        assert_eq!(
2663            json_results,
2664            json!({
2665              "parse_results": [
2666                {
2667                  "name": "group1",
2668                  "children": [
2669                    {
2670                      "name": "C Test 1",
2671                      "outcome": "Passed",
2672                      "parse_rate": null,
2673                      "test_num": 1
2674                    }
2675                  ]
2676                },
2677                {
2678                  "name": "group2",
2679                  "children": [
2680                    {
2681                      "name": "C Test 2",
2682                      "outcome": "Passed",
2683                      "parse_rate": null,
2684                      "test_num": 2
2685                    },
2686                    {
2687                      "name": "C Test 3",
2688                      "outcome": "Failed",
2689                      "parse_rate": null,
2690                      "test_num": 3
2691                    }
2692                  ]
2693                }
2694              ],
2695              "parse_failures": [
2696                {
2697                  "name": "C Test 3",
2698                  "actual": "(translation_unit (expression_statement (number_literal)))",
2699                  "expected": "(translation_unit (expression_statement (string_literal)))",
2700                  "is_cst": false,
2701                }
2702              ],
2703              "parse_stats": {
2704                "successful_parses": 2,
2705                "total_parses": 3,
2706                "total_bytes": 9,
2707                "total_duration": {
2708                  "secs": 0,
2709                  "nanos": 0,
2710                }
2711              },
2712              "highlight_results": [],
2713              "tag_results": [],
2714              "query_results": []
2715            })
2716            .to_string()
2717        );
2718    }
2719}