tree_sitter_cli/
test.rs

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