Skip to main content

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        write!(f, "{}", self.parse_stats)?;
599
600        Ok(())
601    }
602}
603
604pub fn run_tests_at_path(
605    parser: &mut Parser,
606    opts: &TestOptions,
607    test_summary: &mut TestSummary,
608) -> Result<()> {
609    let test_entry = parse_tests(&opts.path)?;
610
611    let _log_session = if opts.debug_graph {
612        Some(util::log_graphs(parser, "log.html", opts.open_log)?)
613    } else {
614        None
615    };
616    if opts.debug {
617        parser.set_logger(Some(Box::new(|log_type, message| {
618            if log_type == LogType::Lex {
619                io::stderr().write_all(b"  ").unwrap();
620            }
621            writeln!(&mut io::stderr(), "{message}").unwrap();
622        })));
623    }
624
625    let mut corrected_entries = Vec::new();
626    run_tests(
627        parser,
628        test_entry,
629        opts,
630        test_summary,
631        &mut corrected_entries,
632        true,
633    )?;
634
635    parser.stop_printing_dot_graphs();
636
637    if test_summary.parse_failures.is_empty() || (opts.update && !test_summary.has_parse_errors) {
638        Ok(())
639    } else if opts.update && test_summary.has_parse_errors {
640        Err(anyhow!(indoc! {"
641                Some tests failed to parse with unexpected `ERROR` or `MISSING` nodes, as shown above, and cannot be updated automatically.
642                Either fix the grammar or manually update the tests if this is expected."}))
643    } else {
644        Err(anyhow!(""))
645    }
646}
647
648pub fn check_queries_at_path(language: &Language, path: &Path) -> Result<()> {
649    for entry in WalkDir::new(path)
650        .into_iter()
651        .filter_map(std::result::Result::ok)
652        .filter(|e| {
653            e.file_type().is_file()
654                && e.path().extension().and_then(OsStr::to_str) == Some("scm")
655                && !e.path().starts_with(".")
656        })
657    {
658        let filepath = entry.file_name().to_str().unwrap_or("");
659        let content = fs::read_to_string(entry.path())
660            .with_context(|| format!("Error reading query file {filepath:?}"))?;
661        Query::new(language, &content)
662            .with_context(|| format!("Error in query file {filepath:?}"))?;
663    }
664    Ok(())
665}
666
667pub struct DiffKey;
668
669impl std::fmt::Display for DiffKey {
670    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
671        write!(
672            f,
673            "\ncorrect / {} / {}",
674            paint(Some(AnsiColor::Green), "expected"),
675            paint(Some(AnsiColor::Red), "unexpected")
676        )?;
677        Ok(())
678    }
679}
680
681impl DiffKey {
682    /// Writes [`DiffKey`] to stdout
683    pub fn print() {
684        println!("{Self}");
685    }
686}
687
688pub struct TestDiff<'a> {
689    pub actual: &'a str,
690    pub expected: &'a str,
691    pub color: bool,
692}
693
694impl<'a> TestDiff<'a> {
695    #[must_use]
696    pub const fn new(actual: &'a str, expected: &'a str) -> Self {
697        Self {
698            actual,
699            expected,
700            color: true,
701        }
702    }
703
704    #[must_use]
705    pub const fn with_color(mut self, color: bool) -> Self {
706        self.color = color;
707        self
708    }
709}
710
711impl std::fmt::Display for TestDiff<'_> {
712    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
713        let diff = TextDiff::from_lines(self.actual, self.expected);
714        for diff in diff.iter_all_changes() {
715            match diff.tag() {
716                ChangeTag::Equal => {
717                    if self.color {
718                        write!(f, "{diff}")?;
719                    } else {
720                        write!(f, " {diff}")?;
721                    }
722                }
723                ChangeTag::Insert => {
724                    if self.color {
725                        write!(
726                            f,
727                            "{}",
728                            paint(Some(AnsiColor::Green), diff.as_str().unwrap())
729                        )?;
730                    } else {
731                        write!(f, "+{diff}")?;
732                    }
733                    if diff.missing_newline() {
734                        writeln!(f)?;
735                    }
736                }
737                ChangeTag::Delete => {
738                    if self.color {
739                        write!(f, "{}", paint(Some(AnsiColor::Red), diff.as_str().unwrap()))?;
740                    } else {
741                        write!(f, "-{diff}")?;
742                    }
743                    if diff.missing_newline() {
744                        writeln!(f)?;
745                    }
746                }
747            }
748        }
749
750        Ok(())
751    }
752}
753
754#[derive(Debug, Serialize, JsonSchema)]
755pub struct TestFailure {
756    name: String,
757    actual: String,
758    expected: String,
759    is_cst: bool,
760}
761
762impl TestFailure {
763    fn new<T, U, V>(name: T, actual: U, expected: V, is_cst: bool) -> Self
764    where
765        T: Into<String>,
766        U: Into<String>,
767        V: Into<String>,
768    {
769        Self {
770            name: name.into(),
771            actual: actual.into(),
772            expected: expected.into(),
773            is_cst,
774        }
775    }
776}
777
778struct TestCorrection {
779    name: String,
780    input: String,
781    output: String,
782    attributes_str: String,
783    header_delim_len: usize,
784    divider_delim_len: usize,
785}
786
787impl TestCorrection {
788    fn new<T, U, V, W>(
789        name: T,
790        input: U,
791        output: V,
792        attributes_str: W,
793        header_delim_len: usize,
794        divider_delim_len: usize,
795    ) -> Self
796    where
797        T: Into<String>,
798        U: Into<String>,
799        V: Into<String>,
800        W: Into<String>,
801    {
802        Self {
803            name: name.into(),
804            input: input.into(),
805            output: output.into(),
806            attributes_str: attributes_str.into(),
807            header_delim_len,
808            divider_delim_len,
809        }
810    }
811}
812
813/// This will return false if we want to "fail fast". It will bail and not parse any more tests.
814fn run_tests(
815    parser: &mut Parser,
816    test_entry: TestEntry,
817    opts: &TestOptions,
818    test_summary: &mut TestSummary,
819    corrected_entries: &mut Vec<TestCorrection>,
820    is_root: bool,
821) -> Result<bool> {
822    match test_entry {
823        TestEntry::Example {
824            name,
825            input,
826            output,
827            header_delim_len,
828            divider_delim_len,
829            has_fields,
830            attributes_str,
831            attributes,
832            ..
833        } => {
834            if attributes.skip {
835                test_summary.parse_results.add_case(TestResult {
836                    name: name.clone(),
837                    info: TestInfo::ParseTest {
838                        outcome: TestOutcome::Skipped,
839                        parse_rate: None,
840                        test_num: test_summary.test_num,
841                    },
842                });
843                test_summary.test_num += 1;
844                return Ok(true);
845            }
846
847            if !attributes.platform {
848                test_summary.parse_results.add_case(TestResult {
849                    name: name.clone(),
850                    info: TestInfo::ParseTest {
851                        outcome: TestOutcome::Platform,
852                        parse_rate: None,
853                        test_num: test_summary.test_num,
854                    },
855                });
856                test_summary.test_num += 1;
857                return Ok(true);
858            }
859
860            for (i, language_name) in attributes.languages.iter().enumerate() {
861                if !language_name.is_empty() {
862                    let language = opts
863                        .languages
864                        .get(language_name.as_ref())
865                        .ok_or_else(|| anyhow!("Language not found: {language_name}"))?;
866                    parser.set_language(language)?;
867                }
868                let start = std::time::Instant::now();
869                let tree = parser.parse(&input, None).unwrap();
870                let parse_rate = {
871                    let parse_time = start.elapsed();
872                    let byte_len = tree.root_node().byte_range().len();
873                    let true_parse_rate =
874                        byte_len as f64 / (parse_time.as_nanos() as f64 / 1_000_000.0);
875                    let adj_parse_rate = adjusted_parse_rate(&tree, parse_time);
876
877                    test_summary.parse_stats.total_parses += 1;
878                    test_summary.parse_stats.total_duration += parse_time;
879                    test_summary.parse_stats.total_bytes += byte_len;
880
881                    Some((true_parse_rate, adj_parse_rate))
882                };
883
884                if attributes.error {
885                    if tree.root_node().has_error() {
886                        test_summary.parse_results.add_case(TestResult {
887                            name: name.clone(),
888                            info: TestInfo::ParseTest {
889                                outcome: TestOutcome::Passed,
890                                parse_rate,
891                                test_num: test_summary.test_num,
892                            },
893                        });
894                        test_summary.parse_stats.successful_parses += 1;
895                        if opts.update {
896                            let input = String::from_utf8(input.clone()).unwrap();
897                            let output = if attributes.cst {
898                                output.clone()
899                            } else {
900                                format_sexp(&output, 0)
901                            };
902                            corrected_entries.push(TestCorrection::new(
903                                &name,
904                                input,
905                                output,
906                                &attributes_str,
907                                header_delim_len,
908                                divider_delim_len,
909                            ));
910                        }
911                    } else {
912                        if opts.update {
913                            let input = String::from_utf8(input.clone()).unwrap();
914                            // Keep the original `expected` output if the actual output has no error
915                            let output = if attributes.cst {
916                                output.clone()
917                            } else {
918                                format_sexp(&output, 0)
919                            };
920                            corrected_entries.push(TestCorrection::new(
921                                &name,
922                                input,
923                                output,
924                                &attributes_str,
925                                header_delim_len,
926                                divider_delim_len,
927                            ));
928                        }
929                        test_summary.parse_results.add_case(TestResult {
930                            name: name.clone(),
931                            info: TestInfo::ParseTest {
932                                outcome: TestOutcome::Failed,
933                                parse_rate,
934                                test_num: test_summary.test_num,
935                            },
936                        });
937                        let actual = if attributes.cst {
938                            render_test_cst(&input, &tree)?
939                        } else {
940                            tree.root_node().to_sexp()
941                        };
942                        test_summary.parse_failures.push(TestFailure::new(
943                            &name,
944                            actual,
945                            "NO ERROR",
946                            attributes.cst,
947                        ));
948                    }
949
950                    if attributes.fail_fast {
951                        return Ok(false);
952                    }
953                } else {
954                    let mut actual = if attributes.cst {
955                        render_test_cst(&input, &tree)?
956                    } else {
957                        tree.root_node().to_sexp()
958                    };
959                    if !(attributes.cst || opts.show_fields || has_fields) {
960                        actual = strip_sexp_fields(&actual);
961                    }
962
963                    if actual == output {
964                        test_summary.parse_results.add_case(TestResult {
965                            name: name.clone(),
966                            info: TestInfo::ParseTest {
967                                outcome: TestOutcome::Passed,
968                                parse_rate,
969                                test_num: test_summary.test_num,
970                            },
971                        });
972                        test_summary.parse_stats.successful_parses += 1;
973                        if opts.update {
974                            let input = String::from_utf8(input.clone()).unwrap();
975                            let output = if attributes.cst {
976                                actual
977                            } else {
978                                format_sexp(&output, 0)
979                            };
980                            corrected_entries.push(TestCorrection::new(
981                                &name,
982                                input,
983                                output,
984                                &attributes_str,
985                                header_delim_len,
986                                divider_delim_len,
987                            ));
988                        }
989                    } else {
990                        if opts.update {
991                            let input = String::from_utf8(input.clone()).unwrap();
992                            let (expected_output, actual_output) = if attributes.cst {
993                                (output.clone(), actual.clone())
994                            } else {
995                                (format_sexp(&output, 0), format_sexp(&actual, 0))
996                            };
997
998                            // Only bail early before updating if the actual is not the output,
999                            // sometimes users want to test cases that
1000                            // are intended to have errors, hence why this
1001                            // check isn't shown above
1002                            if actual.contains("ERROR") || actual.contains("MISSING") {
1003                                test_summary.has_parse_errors = true;
1004
1005                                // keep the original `expected` output if the actual output has an
1006                                // error
1007                                corrected_entries.push(TestCorrection::new(
1008                                    &name,
1009                                    input,
1010                                    expected_output,
1011                                    &attributes_str,
1012                                    header_delim_len,
1013                                    divider_delim_len,
1014                                ));
1015                            } else {
1016                                corrected_entries.push(TestCorrection::new(
1017                                    &name,
1018                                    input,
1019                                    actual_output,
1020                                    &attributes_str,
1021                                    header_delim_len,
1022                                    divider_delim_len,
1023                                ));
1024                                test_summary.parse_results.add_case(TestResult {
1025                                    name: name.clone(),
1026                                    info: TestInfo::ParseTest {
1027                                        outcome: TestOutcome::Updated,
1028                                        parse_rate,
1029                                        test_num: test_summary.test_num,
1030                                    },
1031                                });
1032                            }
1033                        } else {
1034                            test_summary.parse_results.add_case(TestResult {
1035                                name: name.clone(),
1036                                info: TestInfo::ParseTest {
1037                                    outcome: TestOutcome::Failed,
1038                                    parse_rate,
1039                                    test_num: test_summary.test_num,
1040                                },
1041                            });
1042                        }
1043                        test_summary.parse_failures.push(TestFailure::new(
1044                            &name,
1045                            actual,
1046                            &output,
1047                            attributes.cst,
1048                        ));
1049
1050                        if attributes.fail_fast {
1051                            return Ok(false);
1052                        }
1053                    }
1054                }
1055
1056                if i == attributes.languages.len() - 1 {
1057                    // reset to the first language
1058                    parser.set_language(opts.languages.values().next().unwrap())?;
1059                }
1060            }
1061            test_summary.test_num += 1;
1062        }
1063        TestEntry::Group {
1064            name,
1065            children,
1066            file_path,
1067        } => {
1068            if children.is_empty() {
1069                return Ok(true);
1070            }
1071
1072            let failure_count = test_summary.parse_failures.len();
1073            let mut ran_test_in_group = false;
1074
1075            let matches_filter = |name: &str, file_name: &Option<String>, opts: &TestOptions| {
1076                if let (Some(test_file_path), Some(filter_file_name)) = (file_name, &opts.file_name)
1077                {
1078                    if !filter_file_name.eq(test_file_path) {
1079                        return false;
1080                    }
1081                }
1082                if let Some(include) = &opts.include {
1083                    include.is_match(name)
1084                } else if let Some(exclude) = &opts.exclude {
1085                    !exclude.is_match(name)
1086                } else {
1087                    true
1088                }
1089            };
1090
1091            for child in children {
1092                if let TestEntry::Example {
1093                    ref name,
1094                    ref file_name,
1095                    ref input,
1096                    ref output,
1097                    ref attributes_str,
1098                    header_delim_len,
1099                    divider_delim_len,
1100                    ..
1101                } = child
1102                {
1103                    if !matches_filter(name, file_name, opts) {
1104                        if opts.update {
1105                            let input = String::from_utf8(input.clone()).unwrap();
1106                            let output = format_sexp(output, 0);
1107                            corrected_entries.push(TestCorrection::new(
1108                                name,
1109                                input,
1110                                output,
1111                                attributes_str,
1112                                header_delim_len,
1113                                divider_delim_len,
1114                            ));
1115                        }
1116
1117                        test_summary.test_num += 1;
1118                        continue;
1119                    }
1120                }
1121
1122                if !ran_test_in_group && !is_root {
1123                    test_summary.parse_results.add_group(&name);
1124                    ran_test_in_group = true;
1125                }
1126                if !run_tests(parser, child, opts, test_summary, corrected_entries, false)? {
1127                    // fail fast
1128                    return Ok(false);
1129                }
1130            }
1131            // Now that we're done traversing the children of the current group, pop
1132            // the index
1133            test_summary.parse_results.pop_traversal();
1134
1135            if let Some(file_path) = file_path {
1136                if opts.update && test_summary.parse_failures.len() - failure_count > 0 {
1137                    write_tests(&file_path, corrected_entries)?;
1138                }
1139                corrected_entries.clear();
1140            }
1141        }
1142    }
1143    Ok(true)
1144}
1145
1146/// Convenience wrapper to render a CST for a test entry.
1147fn render_test_cst(input: &[u8], tree: &Tree) -> Result<String> {
1148    let mut rendered_cst: Vec<u8> = Vec::new();
1149    let mut cursor = tree.walk();
1150    let opts = ParseFileOptions {
1151        edits: &[],
1152        output: ParseOutput::Cst,
1153        stats: &mut ParseStats::default(),
1154        print_time: false,
1155        timeout: 0,
1156        debug: ParseDebugType::Quiet,
1157        debug_graph: false,
1158        cancellation_flag: None,
1159        encoding: None,
1160        open_log: false,
1161        no_ranges: false,
1162        parse_theme: &ParseTheme::empty(),
1163    };
1164    render_cst(input, tree, &mut cursor, &opts, &mut rendered_cst)?;
1165    Ok(String::from_utf8_lossy(&rendered_cst).trim().to_string())
1166}
1167
1168// Parse time is interpreted in ns before converting to ms to avoid truncation issues
1169// Parse rates often have several outliers, leading to a large standard deviation. Taking
1170// the log of these rates serves to "flatten" out the distribution, yielding a more
1171// usable standard deviation for finding statistically significant slow parse rates
1172// NOTE: This is just a heuristic
1173#[must_use]
1174pub fn adjusted_parse_rate(tree: &Tree, parse_time: Duration) -> f64 {
1175    f64::ln(
1176        tree.root_node().byte_range().len() as f64 / (parse_time.as_nanos() as f64 / 1_000_000.0),
1177    )
1178}
1179
1180fn write_tests(file_path: &Path, corrected_entries: &[TestCorrection]) -> Result<()> {
1181    let mut buffer = fs::File::create(file_path)?;
1182    write_tests_to_buffer(&mut buffer, corrected_entries)
1183}
1184
1185fn write_tests_to_buffer(
1186    buffer: &mut impl Write,
1187    corrected_entries: &[TestCorrection],
1188) -> Result<()> {
1189    for (
1190        i,
1191        TestCorrection {
1192            name,
1193            input,
1194            output,
1195            attributes_str,
1196            header_delim_len,
1197            divider_delim_len,
1198        },
1199    ) in corrected_entries.iter().enumerate()
1200    {
1201        if i > 0 {
1202            writeln!(buffer)?;
1203        }
1204        writeln!(
1205            buffer,
1206            "{}\n{name}\n{}{}\n{input}\n{}\n\n{}",
1207            "=".repeat(*header_delim_len),
1208            if attributes_str.is_empty() {
1209                attributes_str.clone()
1210            } else {
1211                format!("{attributes_str}\n")
1212            },
1213            "=".repeat(*header_delim_len),
1214            "-".repeat(*divider_delim_len),
1215            output.trim()
1216        )?;
1217    }
1218    Ok(())
1219}
1220
1221pub fn parse_tests(path: &Path) -> io::Result<TestEntry> {
1222    let name = path
1223        .file_stem()
1224        .and_then(|s| s.to_str())
1225        .unwrap_or("")
1226        .to_string();
1227    if path.is_dir() {
1228        let mut children = Vec::new();
1229        for entry in fs::read_dir(path)? {
1230            let entry = entry?;
1231            let hidden = entry.file_name().to_str().unwrap_or("").starts_with('.');
1232            if !hidden {
1233                children.push(entry.path());
1234            }
1235        }
1236        children.sort_by(|a, b| {
1237            a.file_name()
1238                .unwrap_or_default()
1239                .cmp(b.file_name().unwrap_or_default())
1240        });
1241        let children = children
1242            .iter()
1243            .map(|path| parse_tests(path))
1244            .collect::<io::Result<Vec<TestEntry>>>()?;
1245        Ok(TestEntry::Group {
1246            name,
1247            children,
1248            file_path: None,
1249        })
1250    } else {
1251        let content = fs::read_to_string(path)?;
1252        Ok(parse_test_content(name, &content, Some(path.to_path_buf())))
1253    }
1254}
1255
1256#[must_use]
1257pub fn strip_sexp_fields(sexp: &str) -> String {
1258    SEXP_FIELD_REGEX.replace_all(sexp, " (").to_string()
1259}
1260
1261#[must_use]
1262pub fn strip_points(sexp: &str) -> String {
1263    POINT_REGEX.replace_all(sexp, "").to_string()
1264}
1265
1266fn parse_test_content(name: String, content: &str, file_path: Option<PathBuf>) -> TestEntry {
1267    let mut children = Vec::new();
1268    let bytes = content.as_bytes();
1269    let mut prev_name = String::new();
1270    let mut prev_attributes_str = String::new();
1271    let mut prev_header_end = 0;
1272
1273    // Find the first test header in the file, and determine if it has a
1274    // custom suffix. If so, then this suffix will be used to identify
1275    // all subsequent headers and divider lines in the file.
1276    let first_suffix = HEADER_REGEX
1277        .captures(bytes)
1278        .and_then(|c| c.name("suffix1"))
1279        .map(|m| String::from_utf8_lossy(m.as_bytes()));
1280
1281    // Find all of the `===` test headers, which contain the test names.
1282    // Ignore any matches whose suffix does not match the first header
1283    // suffix in the file.
1284    let header_matches = HEADER_REGEX.captures_iter(bytes).filter_map(|c| {
1285        let header_delim_len = c.name("equals").map_or(80, |m| m.as_bytes().len());
1286        let suffix1 = c
1287            .name("suffix1")
1288            .map(|m| String::from_utf8_lossy(m.as_bytes()));
1289        let suffix2 = c
1290            .name("suffix2")
1291            .map(|m| String::from_utf8_lossy(m.as_bytes()));
1292
1293        let (mut skip, mut platform, mut fail_fast, mut error, mut cst, mut languages) =
1294            (false, None, false, false, false, vec![]);
1295
1296        let test_name_and_markers = c
1297            .name("test_name_and_markers")
1298            .map_or("".as_bytes(), |m| m.as_bytes());
1299
1300        let mut test_name = String::new();
1301        let mut attributes_str = String::new();
1302
1303        let mut seen_marker = false;
1304
1305        let test_name_and_markers = str::from_utf8(test_name_and_markers).unwrap();
1306        for line in test_name_and_markers
1307            .split_inclusive('\n')
1308            .filter(|s| !s.is_empty())
1309        {
1310            let trimmed_line = line.trim();
1311            match trimmed_line.split('(').next().unwrap() {
1312                ":skip" => (seen_marker, skip) = (true, true),
1313                ":platform" => {
1314                    if let Some(platforms) = trimmed_line.strip_prefix(':').and_then(|s| {
1315                        s.strip_prefix("platform(")
1316                            .and_then(|s| s.strip_suffix(')'))
1317                    }) {
1318                        seen_marker = true;
1319                        platform = Some(
1320                            platform.unwrap_or(false) || platforms.trim() == std::env::consts::OS,
1321                        );
1322                    }
1323                }
1324                ":fail-fast" => (seen_marker, fail_fast) = (true, true),
1325                ":error" => (seen_marker, error) = (true, true),
1326                ":language" => {
1327                    if let Some(lang) = trimmed_line.strip_prefix(':').and_then(|s| {
1328                        s.strip_prefix("language(")
1329                            .and_then(|s| s.strip_suffix(')'))
1330                    }) {
1331                        seen_marker = true;
1332                        languages.push(lang.into());
1333                    }
1334                }
1335                ":cst" => (seen_marker, cst) = (true, true),
1336                _ if !seen_marker => {
1337                    test_name.push_str(line);
1338                }
1339                _ => {}
1340            }
1341        }
1342        attributes_str.push_str(test_name_and_markers.strip_prefix(&test_name).unwrap());
1343
1344        // prefer skip over error, both shouldn't be set
1345        if skip {
1346            error = false;
1347        }
1348
1349        // add a default language if none are specified, will defer to the first language
1350        if languages.is_empty() {
1351            languages.push("".into());
1352        }
1353
1354        if suffix1 == first_suffix && suffix2 == first_suffix {
1355            let header_range = c.get(0).unwrap().range();
1356            let test_name = if test_name.is_empty() {
1357                None
1358            } else {
1359                Some(test_name.trim_end().to_string())
1360            };
1361            let attributes_str = if attributes_str.is_empty() {
1362                None
1363            } else {
1364                Some(attributes_str.trim_end().to_string())
1365            };
1366            Some((
1367                header_delim_len,
1368                header_range,
1369                test_name,
1370                attributes_str,
1371                TestAttributes {
1372                    skip,
1373                    platform: platform.unwrap_or(true),
1374                    fail_fast,
1375                    error,
1376                    cst,
1377                    languages,
1378                },
1379            ))
1380        } else {
1381            None
1382        }
1383    });
1384
1385    let (mut prev_header_len, mut prev_attributes) = (80, TestAttributes::default());
1386    for (header_delim_len, header_range, test_name, attributes_str, attributes) in header_matches
1387        .chain(Some((
1388            80,
1389            bytes.len()..bytes.len(),
1390            None,
1391            None,
1392            TestAttributes::default(),
1393        )))
1394    {
1395        // Find the longest line of dashes following each test description. That line
1396        // separates the input from the expected output. Ignore any matches whose suffix
1397        // does not match the first suffix in the file.
1398        if prev_header_end > 0 {
1399            let divider_range = DIVIDER_REGEX
1400                .captures_iter(&bytes[prev_header_end..header_range.start])
1401                .filter_map(|m| {
1402                    let divider_delim_len = m.name("hyphens").map_or(80, |m| m.as_bytes().len());
1403                    let suffix = m
1404                        .name("suffix")
1405                        .map(|m| String::from_utf8_lossy(m.as_bytes()));
1406                    if suffix == first_suffix {
1407                        let range = m.get(0).unwrap().range();
1408                        Some((
1409                            divider_delim_len,
1410                            (prev_header_end + range.start)..(prev_header_end + range.end),
1411                        ))
1412                    } else {
1413                        None
1414                    }
1415                })
1416                .max_by_key(|(_, range)| range.len());
1417
1418            if let Some((divider_delim_len, divider_range)) = divider_range {
1419                if let Ok(output) = str::from_utf8(&bytes[divider_range.end..header_range.start]) {
1420                    let mut input = bytes[prev_header_end..divider_range.start].to_vec();
1421
1422                    // Remove trailing newline from the input.
1423                    input.pop();
1424                    if input.last() == Some(&b'\r') {
1425                        input.pop();
1426                    }
1427
1428                    let (output, has_fields) = if prev_attributes.cst {
1429                        (output.trim().to_string(), false)
1430                    } else {
1431                        // Remove all comments
1432                        let output = COMMENT_REGEX.replace_all(output, "").to_string();
1433
1434                        // Normalize the whitespace in the expected output.
1435                        let output = WHITESPACE_REGEX.replace_all(output.trim(), " ");
1436                        let output = output.replace(" )", ")");
1437
1438                        // Identify if the expected output has fields indicated. If not, then
1439                        // fields will not be checked.
1440                        let has_fields = SEXP_FIELD_REGEX.is_match(&output);
1441
1442                        (output, has_fields)
1443                    };
1444
1445                    let file_name = if let Some(ref path) = file_path {
1446                        path.file_name().map(|n| n.to_string_lossy().to_string())
1447                    } else {
1448                        None
1449                    };
1450
1451                    let t = TestEntry::Example {
1452                        name: prev_name,
1453                        input,
1454                        output,
1455                        header_delim_len: prev_header_len,
1456                        divider_delim_len,
1457                        has_fields,
1458                        attributes_str: prev_attributes_str,
1459                        attributes: prev_attributes,
1460                        file_name,
1461                    };
1462
1463                    children.push(t);
1464                }
1465            }
1466        }
1467        prev_attributes = attributes;
1468        prev_name = test_name.unwrap_or_default();
1469        prev_attributes_str = attributes_str.unwrap_or_default();
1470        prev_header_len = header_delim_len;
1471        prev_header_end = header_range.end;
1472    }
1473    TestEntry::Group {
1474        name,
1475        children,
1476        file_path,
1477    }
1478}
1479
1480#[cfg(test)]
1481mod tests {
1482    use serde_json::json;
1483
1484    use crate::tests::get_language;
1485
1486    use super::*;
1487
1488    #[test]
1489    fn test_parse_test_content_simple() {
1490        let entry = parse_test_content(
1491            "the-filename".to_string(),
1492            r"
1493===============
1494The first test
1495===============
1496
1497a b c
1498
1499---
1500
1501(a
1502    (b c))
1503
1504================
1505The second test
1506================
1507d
1508---
1509(d)
1510        "
1511            .trim(),
1512            None,
1513        );
1514
1515        assert_eq!(
1516            entry,
1517            TestEntry::Group {
1518                name: "the-filename".to_string(),
1519                children: vec![
1520                    TestEntry::Example {
1521                        name: "The first test".to_string(),
1522                        input: b"\na b c\n".to_vec(),
1523                        output: "(a (b c))".to_string(),
1524                        header_delim_len: 15,
1525                        divider_delim_len: 3,
1526                        has_fields: false,
1527                        attributes_str: String::new(),
1528                        attributes: TestAttributes::default(),
1529                        file_name: None,
1530                    },
1531                    TestEntry::Example {
1532                        name: "The second test".to_string(),
1533                        input: b"d".to_vec(),
1534                        output: "(d)".to_string(),
1535                        header_delim_len: 16,
1536                        divider_delim_len: 3,
1537                        has_fields: false,
1538                        attributes_str: String::new(),
1539                        attributes: TestAttributes::default(),
1540                        file_name: None,
1541                    },
1542                ],
1543                file_path: None,
1544            }
1545        );
1546    }
1547
1548    #[test]
1549    fn test_parse_test_content_with_dashes_in_source_code() {
1550        let entry = parse_test_content(
1551            "the-filename".to_string(),
1552            r"
1553==================
1554Code with dashes
1555==================
1556abc
1557---
1558defg
1559----
1560hijkl
1561-------
1562
1563(a (b))
1564
1565=========================
1566Code ending with dashes
1567=========================
1568abc
1569-----------
1570-------------------
1571
1572(c (d))
1573        "
1574            .trim(),
1575            None,
1576        );
1577
1578        assert_eq!(
1579            entry,
1580            TestEntry::Group {
1581                name: "the-filename".to_string(),
1582                children: vec![
1583                    TestEntry::Example {
1584                        name: "Code with dashes".to_string(),
1585                        input: b"abc\n---\ndefg\n----\nhijkl".to_vec(),
1586                        output: "(a (b))".to_string(),
1587                        header_delim_len: 18,
1588                        divider_delim_len: 7,
1589                        has_fields: false,
1590                        attributes_str: String::new(),
1591                        attributes: TestAttributes::default(),
1592                        file_name: None,
1593                    },
1594                    TestEntry::Example {
1595                        name: "Code ending with dashes".to_string(),
1596                        input: b"abc\n-----------".to_vec(),
1597                        output: "(c (d))".to_string(),
1598                        header_delim_len: 25,
1599                        divider_delim_len: 19,
1600                        has_fields: false,
1601                        attributes_str: String::new(),
1602                        attributes: TestAttributes::default(),
1603                        file_name: None,
1604                    },
1605                ],
1606                file_path: None,
1607            }
1608        );
1609    }
1610
1611    #[test]
1612    fn test_format_sexp() {
1613        assert_eq!(format_sexp("", 0), "");
1614        assert_eq!(
1615            format_sexp("(a b: (c) (d) e: (f (g (h (MISSING i)))))", 0),
1616            r"
1617(a
1618  b: (c)
1619  (d)
1620  e: (f
1621    (g
1622      (h
1623        (MISSING i)))))
1624"
1625            .trim()
1626        );
1627        assert_eq!(
1628            format_sexp("(program (ERROR (UNEXPECTED ' ')) (identifier))", 0),
1629            r"
1630(program
1631  (ERROR
1632    (UNEXPECTED ' '))
1633  (identifier))
1634"
1635            .trim()
1636        );
1637        assert_eq!(
1638            format_sexp(r#"(source_file (MISSING ")"))"#, 0),
1639            r#"
1640(source_file
1641  (MISSING ")"))
1642        "#
1643            .trim()
1644        );
1645        assert_eq!(
1646            format_sexp(
1647                r"(source_file (ERROR (UNEXPECTED 'f') (UNEXPECTED '+')))",
1648                0
1649            ),
1650            r"
1651(source_file
1652  (ERROR
1653    (UNEXPECTED 'f')
1654    (UNEXPECTED '+')))
1655"
1656            .trim()
1657        );
1658    }
1659
1660    #[test]
1661    fn test_write_tests_to_buffer() {
1662        let mut buffer = Vec::new();
1663        let corrected_entries = vec![
1664            TestCorrection::new(
1665                "title 1".to_string(),
1666                "input 1".to_string(),
1667                "output 1".to_string(),
1668                String::new(),
1669                80,
1670                80,
1671            ),
1672            TestCorrection::new(
1673                "title 2".to_string(),
1674                "input 2".to_string(),
1675                "output 2".to_string(),
1676                String::new(),
1677                80,
1678                80,
1679            ),
1680        ];
1681        write_tests_to_buffer(&mut buffer, &corrected_entries).unwrap();
1682        assert_eq!(
1683            String::from_utf8(buffer).unwrap(),
1684            r"
1685================================================================================
1686title 1
1687================================================================================
1688input 1
1689--------------------------------------------------------------------------------
1690
1691output 1
1692
1693================================================================================
1694title 2
1695================================================================================
1696input 2
1697--------------------------------------------------------------------------------
1698
1699output 2
1700"
1701            .trim_start()
1702            .to_string()
1703        );
1704    }
1705
1706    #[test]
1707    fn test_parse_test_content_with_comments_in_sexp() {
1708        let entry = parse_test_content(
1709            "the-filename".to_string(),
1710            r#"
1711==================
1712sexp with comment
1713==================
1714code
1715---
1716
1717; Line start comment
1718(a (b))
1719
1720==================
1721sexp with comment between
1722==================
1723code
1724---
1725
1726; Line start comment
1727(a
1728; ignore this
1729    (b)
1730    ; also ignore this
1731)
1732
1733=========================
1734sexp with ';'
1735=========================
1736code
1737---
1738
1739(MISSING ";")
1740        "#
1741            .trim(),
1742            None,
1743        );
1744
1745        assert_eq!(
1746            entry,
1747            TestEntry::Group {
1748                name: "the-filename".to_string(),
1749                children: vec![
1750                    TestEntry::Example {
1751                        name: "sexp with comment".to_string(),
1752                        input: b"code".to_vec(),
1753                        output: "(a (b))".to_string(),
1754                        header_delim_len: 18,
1755                        divider_delim_len: 3,
1756                        has_fields: false,
1757                        attributes_str: String::new(),
1758                        attributes: TestAttributes::default(),
1759                        file_name: None,
1760                    },
1761                    TestEntry::Example {
1762                        name: "sexp with comment between".to_string(),
1763                        input: b"code".to_vec(),
1764                        output: "(a (b))".to_string(),
1765                        header_delim_len: 18,
1766                        divider_delim_len: 3,
1767                        has_fields: false,
1768                        attributes_str: String::new(),
1769                        attributes: TestAttributes::default(),
1770                        file_name: None,
1771                    },
1772                    TestEntry::Example {
1773                        name: "sexp with ';'".to_string(),
1774                        input: b"code".to_vec(),
1775                        output: "(MISSING \";\")".to_string(),
1776                        header_delim_len: 25,
1777                        divider_delim_len: 3,
1778                        has_fields: false,
1779                        attributes_str: String::new(),
1780                        attributes: TestAttributes::default(),
1781                        file_name: None,
1782                    }
1783                ],
1784                file_path: None,
1785            }
1786        );
1787    }
1788
1789    #[test]
1790    fn test_parse_test_content_with_suffixes() {
1791        let entry = parse_test_content(
1792            "the-filename".to_string(),
1793            r"
1794==================asdf\()[]|{}*+?^$.-
1795First test
1796==================asdf\()[]|{}*+?^$.-
1797
1798=========================
1799NOT A TEST HEADER
1800=========================
1801-------------------------
1802
1803---asdf\()[]|{}*+?^$.-
1804
1805(a)
1806
1807==================asdf\()[]|{}*+?^$.-
1808Second test
1809==================asdf\()[]|{}*+?^$.-
1810
1811=========================
1812NOT A TEST HEADER
1813=========================
1814-------------------------
1815
1816---asdf\()[]|{}*+?^$.-
1817
1818(a)
1819
1820=========================asdf\()[]|{}*+?^$.-
1821Test name with = symbol
1822=========================asdf\()[]|{}*+?^$.-
1823
1824=========================
1825NOT A TEST HEADER
1826=========================
1827-------------------------
1828
1829---asdf\()[]|{}*+?^$.-
1830
1831(a)
1832
1833==============================asdf\()[]|{}*+?^$.-
1834Test containing equals
1835==============================asdf\()[]|{}*+?^$.-
1836
1837===
1838
1839------------------------------asdf\()[]|{}*+?^$.-
1840
1841(a)
1842
1843==============================asdf\()[]|{}*+?^$.-
1844Subsequent test containing equals
1845==============================asdf\()[]|{}*+?^$.-
1846
1847===
1848
1849------------------------------asdf\()[]|{}*+?^$.-
1850
1851(a)
1852"
1853            .trim(),
1854            None,
1855        );
1856
1857        let expected_input = b"\n=========================\n\
1858            NOT A TEST HEADER\n\
1859            =========================\n\
1860            -------------------------\n"
1861            .to_vec();
1862        pretty_assertions::assert_eq!(
1863            entry,
1864            TestEntry::Group {
1865                name: "the-filename".to_string(),
1866                children: vec![
1867                    TestEntry::Example {
1868                        name: "First test".to_string(),
1869                        input: expected_input.clone(),
1870                        output: "(a)".to_string(),
1871                        header_delim_len: 18,
1872                        divider_delim_len: 3,
1873                        has_fields: false,
1874                        attributes_str: String::new(),
1875                        attributes: TestAttributes::default(),
1876                        file_name: None,
1877                    },
1878                    TestEntry::Example {
1879                        name: "Second test".to_string(),
1880                        input: expected_input.clone(),
1881                        output: "(a)".to_string(),
1882                        header_delim_len: 18,
1883                        divider_delim_len: 3,
1884                        has_fields: false,
1885                        attributes_str: String::new(),
1886                        attributes: TestAttributes::default(),
1887                        file_name: None,
1888                    },
1889                    TestEntry::Example {
1890                        name: "Test name with = symbol".to_string(),
1891                        input: expected_input,
1892                        output: "(a)".to_string(),
1893                        header_delim_len: 25,
1894                        divider_delim_len: 3,
1895                        has_fields: false,
1896                        attributes_str: String::new(),
1897                        attributes: TestAttributes::default(),
1898                        file_name: None,
1899                    },
1900                    TestEntry::Example {
1901                        name: "Test containing equals".to_string(),
1902                        input: "\n===\n".into(),
1903                        output: "(a)".into(),
1904                        header_delim_len: 30,
1905                        divider_delim_len: 30,
1906                        has_fields: false,
1907                        attributes_str: String::new(),
1908                        attributes: TestAttributes::default(),
1909                        file_name: None,
1910                    },
1911                    TestEntry::Example {
1912                        name: "Subsequent test containing equals".to_string(),
1913                        input: "\n===\n".into(),
1914                        output: "(a)".into(),
1915                        header_delim_len: 30,
1916                        divider_delim_len: 30,
1917                        has_fields: false,
1918                        attributes_str: String::new(),
1919                        attributes: TestAttributes::default(),
1920                        file_name: None,
1921                    }
1922                ],
1923                file_path: None,
1924            }
1925        );
1926    }
1927
1928    #[test]
1929    fn test_parse_test_content_with_newlines_in_test_names() {
1930        let entry = parse_test_content(
1931            "the-filename".to_string(),
1932            r"
1933===============
1934name
1935with
1936newlines
1937===============
1938a
1939---
1940(b)
1941
1942====================
1943name with === signs
1944====================
1945code with ----
1946---
1947(d)
1948",
1949            None,
1950        );
1951
1952        assert_eq!(
1953            entry,
1954            TestEntry::Group {
1955                name: "the-filename".to_string(),
1956                file_path: None,
1957                children: vec![
1958                    TestEntry::Example {
1959                        name: "name\nwith\nnewlines".to_string(),
1960                        input: b"a".to_vec(),
1961                        output: "(b)".to_string(),
1962                        header_delim_len: 15,
1963                        divider_delim_len: 3,
1964                        has_fields: false,
1965                        attributes_str: String::new(),
1966                        attributes: TestAttributes::default(),
1967                        file_name: None,
1968                    },
1969                    TestEntry::Example {
1970                        name: "name with === signs".to_string(),
1971                        input: b"code with ----".to_vec(),
1972                        output: "(d)".to_string(),
1973                        header_delim_len: 20,
1974                        divider_delim_len: 3,
1975                        has_fields: false,
1976                        attributes_str: String::new(),
1977                        attributes: TestAttributes::default(),
1978                        file_name: None,
1979                    }
1980                ]
1981            }
1982        );
1983    }
1984
1985    #[test]
1986    fn test_parse_test_with_markers() {
1987        // do one with :skip, we should not see it in the entry output
1988
1989        let entry = parse_test_content(
1990            "the-filename".to_string(),
1991            r"
1992=====================
1993Test with skip marker
1994:skip
1995=====================
1996a
1997---
1998(b)
1999",
2000            None,
2001        );
2002
2003        assert_eq!(
2004            entry,
2005            TestEntry::Group {
2006                name: "the-filename".to_string(),
2007                file_path: None,
2008                children: vec![TestEntry::Example {
2009                    name: "Test with skip marker".to_string(),
2010                    input: b"a".to_vec(),
2011                    output: "(b)".to_string(),
2012                    header_delim_len: 21,
2013                    divider_delim_len: 3,
2014                    has_fields: false,
2015                    attributes_str: ":skip".to_string(),
2016                    attributes: TestAttributes {
2017                        skip: true,
2018                        platform: true,
2019                        fail_fast: false,
2020                        error: false,
2021                        cst: false,
2022                        languages: vec!["".into()]
2023                    },
2024                    file_name: None,
2025                }]
2026            }
2027        );
2028
2029        let entry = parse_test_content(
2030            "the-filename".to_string(),
2031            &format!(
2032                r"
2033=========================
2034Test with platform marker
2035:platform({})
2036:fail-fast
2037=========================
2038a
2039---
2040(b)
2041
2042=============================
2043Test with bad platform marker
2044:platform({})
2045
2046:language(foo)
2047=============================
2048a
2049---
2050(b)
2051
2052====================
2053Test with cst marker
2054:cst
2055====================
20561
2057---
20580:0 - 1:0   source_file
20590:0 - 0:1   expression
20600:0 - 0:1     number_literal `1`
2061",
2062                std::env::consts::OS,
2063                if std::env::consts::OS == "linux" {
2064                    "macos"
2065                } else {
2066                    "linux"
2067                }
2068            ),
2069            None,
2070        );
2071
2072        assert_eq!(
2073            entry,
2074            TestEntry::Group {
2075                name: "the-filename".to_string(),
2076                file_path: None,
2077                children: vec![
2078                    TestEntry::Example {
2079                        name: "Test with platform marker".to_string(),
2080                        input: b"a".to_vec(),
2081                        output: "(b)".to_string(),
2082                        header_delim_len: 25,
2083                        divider_delim_len: 3,
2084                        has_fields: false,
2085                        attributes_str: format!(":platform({})\n:fail-fast", std::env::consts::OS),
2086                        attributes: TestAttributes {
2087                            skip: false,
2088                            platform: true,
2089                            fail_fast: true,
2090                            error: false,
2091                            cst: false,
2092                            languages: vec!["".into()]
2093                        },
2094                        file_name: None,
2095                    },
2096                    TestEntry::Example {
2097                        name: "Test with bad platform marker".to_string(),
2098                        input: b"a".to_vec(),
2099                        output: "(b)".to_string(),
2100                        header_delim_len: 29,
2101                        divider_delim_len: 3,
2102                        has_fields: false,
2103                        attributes_str: if std::env::consts::OS == "linux" {
2104                            ":platform(macos)\n\n:language(foo)".to_string()
2105                        } else {
2106                            ":platform(linux)\n\n:language(foo)".to_string()
2107                        },
2108                        attributes: TestAttributes {
2109                            skip: false,
2110                            platform: false,
2111                            fail_fast: false,
2112                            error: false,
2113                            cst: false,
2114                            languages: vec!["foo".into()]
2115                        },
2116                        file_name: None,
2117                    },
2118                    TestEntry::Example {
2119                        name: "Test with cst marker".to_string(),
2120                        input: b"1".to_vec(),
2121                        output: "0:0 - 1:0   source_file
21220:0 - 0:1   expression
21230:0 - 0:1     number_literal `1`"
2124                            .to_string(),
2125                        header_delim_len: 20,
2126                        divider_delim_len: 3,
2127                        has_fields: false,
2128                        attributes_str: ":cst".to_string(),
2129                        attributes: TestAttributes {
2130                            skip: false,
2131                            platform: true,
2132                            fail_fast: false,
2133                            error: false,
2134                            cst: true,
2135                            languages: vec!["".into()]
2136                        },
2137                        file_name: None,
2138                    }
2139                ]
2140            }
2141        );
2142    }
2143
2144    fn clear_parse_rate(result: &mut TestResult) {
2145        let test_case_info = &mut result.info;
2146        match test_case_info {
2147            TestInfo::ParseTest {
2148                ref mut parse_rate, ..
2149            } => {
2150                assert!(parse_rate.is_some());
2151                *parse_rate = None;
2152            }
2153            TestInfo::Group { .. } | TestInfo::AssertionTest { .. } => {
2154                panic!("Unexpected test result")
2155            }
2156        }
2157    }
2158
2159    #[test]
2160    fn run_tests_simple() {
2161        let mut parser = Parser::new();
2162        let language = get_language("c");
2163        parser
2164            .set_language(&language)
2165            .expect("Failed to set language");
2166        let mut languages = BTreeMap::new();
2167        languages.insert("c", &language);
2168        let opts = TestOptions {
2169            path: PathBuf::from("foo"),
2170            debug: true,
2171            debug_graph: false,
2172            include: None,
2173            exclude: None,
2174            file_name: None,
2175            update: false,
2176            open_log: false,
2177            languages,
2178            color: true,
2179            show_fields: false,
2180            overview_only: false,
2181        };
2182
2183        // NOTE: The following test cases are combined to work around a race condition
2184        // in the loader
2185        {
2186            let test_entry = TestEntry::Group {
2187                name: "foo".to_string(),
2188                file_path: None,
2189                children: vec![TestEntry::Example {
2190                    name: "C Test 1".to_string(),
2191                    input: b"1;\n".to_vec(),
2192                    output: "(translation_unit (expression_statement (number_literal)))"
2193                        .to_string(),
2194                    header_delim_len: 25,
2195                    divider_delim_len: 3,
2196                    has_fields: false,
2197                    attributes_str: String::new(),
2198                    attributes: TestAttributes::default(),
2199                    file_name: None,
2200                }],
2201            };
2202
2203            let mut test_summary = TestSummary::new(true, TestStats::All, false, false, false);
2204            let mut corrected_entries = Vec::new();
2205            run_tests(
2206                &mut parser,
2207                test_entry,
2208                &opts,
2209                &mut test_summary,
2210                &mut corrected_entries,
2211                true,
2212            )
2213            .expect("Failed to run tests");
2214
2215            // parse rates will always be different, so we need to clear out these
2216            // fields to reliably assert equality below
2217            clear_parse_rate(&mut test_summary.parse_results.root_group[0]);
2218            test_summary.parse_stats.total_duration = Duration::from_secs(0);
2219
2220            let json_results = serde_json::to_string(&test_summary).unwrap();
2221
2222            assert_eq!(
2223                json_results,
2224                json!({
2225                  "parse_results": [
2226                    {
2227                      "name": "C Test 1",
2228                      "outcome": "Passed",
2229                      "parse_rate": null,
2230                      "test_num": 1
2231                    }
2232                  ],
2233                  "parse_failures": [],
2234                  "parse_stats": {
2235                    "successful_parses": 1,
2236                    "total_parses": 1,
2237                    "total_bytes": 3,
2238                    "total_duration": {
2239                      "secs": 0,
2240                      "nanos": 0,
2241                    }
2242                  },
2243                  "highlight_results": [],
2244                  "tag_results": [],
2245                  "query_results": []
2246                })
2247                .to_string()
2248            );
2249        }
2250        {
2251            let test_entry = TestEntry::Group {
2252                name: "corpus".to_string(),
2253                file_path: None,
2254                children: vec![
2255                    TestEntry::Group {
2256                        name: "group1".to_string(),
2257                        // This test passes
2258                        children: vec![TestEntry::Example {
2259                            name: "C Test 1".to_string(),
2260                            input: b"1;\n".to_vec(),
2261                            output: "(translation_unit (expression_statement (number_literal)))"
2262                                .to_string(),
2263                            header_delim_len: 25,
2264                            divider_delim_len: 3,
2265                            has_fields: false,
2266                            attributes_str: String::new(),
2267                            attributes: TestAttributes::default(),
2268                            file_name: None,
2269                        }],
2270                        file_path: None,
2271                    },
2272                    TestEntry::Group {
2273                        name: "group2".to_string(),
2274                        children: vec![
2275                            // This test passes
2276                            TestEntry::Example {
2277                                name: "C Test 2".to_string(),
2278                                input: b"1;\n".to_vec(),
2279                                output:
2280                                    "(translation_unit (expression_statement (number_literal)))"
2281                                        .to_string(),
2282                                header_delim_len: 25,
2283                                divider_delim_len: 3,
2284                                has_fields: false,
2285                                attributes_str: String::new(),
2286                                attributes: TestAttributes::default(),
2287                                file_name: None,
2288                            },
2289                            // This test fails, and is marked with fail-fast
2290                            TestEntry::Example {
2291                                name: "C Test 3".to_string(),
2292                                input: b"1;\n".to_vec(),
2293                                output:
2294                                    "(translation_unit (expression_statement (string_literal)))"
2295                                        .to_string(),
2296                                header_delim_len: 25,
2297                                divider_delim_len: 3,
2298                                has_fields: false,
2299                                attributes_str: String::new(),
2300                                attributes: TestAttributes {
2301                                    fail_fast: true,
2302                                    ..Default::default()
2303                                },
2304                                file_name: None,
2305                            },
2306                        ],
2307                        file_path: None,
2308                    },
2309                    // This group never runs because of the previous failure
2310                    TestEntry::Group {
2311                        name: "group3".to_string(),
2312                        // This test fails, and is marked with fail-fast
2313                        children: vec![TestEntry::Example {
2314                            name: "C Test 4".to_string(),
2315                            input: b"1;\n".to_vec(),
2316                            output: "(translation_unit (expression_statement (number_literal)))"
2317                                .to_string(),
2318                            header_delim_len: 25,
2319                            divider_delim_len: 3,
2320                            has_fields: false,
2321                            attributes_str: String::new(),
2322                            attributes: TestAttributes::default(),
2323                            file_name: None,
2324                        }],
2325                        file_path: None,
2326                    },
2327                ],
2328            };
2329
2330            let mut test_summary = TestSummary::new(true, TestStats::All, false, false, false);
2331            let mut corrected_entries = Vec::new();
2332            run_tests(
2333                &mut parser,
2334                test_entry,
2335                &opts,
2336                &mut test_summary,
2337                &mut corrected_entries,
2338                true,
2339            )
2340            .expect("Failed to run tests");
2341
2342            // parse rates will always be different, so we need to clear out these
2343            // fields to reliably assert equality below
2344            {
2345                let test_group_1_info = &mut test_summary.parse_results.root_group[0].info;
2346                match test_group_1_info {
2347                    TestInfo::Group {
2348                        ref mut children, ..
2349                    } => clear_parse_rate(&mut children[0]),
2350                    TestInfo::ParseTest { .. } | TestInfo::AssertionTest { .. } => {
2351                        panic!("Unexpected test result");
2352                    }
2353                }
2354                let test_group_2_info = &mut test_summary.parse_results.root_group[1].info;
2355                match test_group_2_info {
2356                    TestInfo::Group {
2357                        ref mut children, ..
2358                    } => {
2359                        clear_parse_rate(&mut children[0]);
2360                        clear_parse_rate(&mut children[1]);
2361                    }
2362                    TestInfo::ParseTest { .. } | TestInfo::AssertionTest { .. } => {
2363                        panic!("Unexpected test result");
2364                    }
2365                }
2366                test_summary.parse_stats.total_duration = Duration::from_secs(0);
2367            }
2368
2369            let json_results = serde_json::to_string(&test_summary).unwrap();
2370
2371            assert_eq!(
2372                json_results,
2373                json!({
2374                  "parse_results": [
2375                    {
2376                      "name": "group1",
2377                      "children": [
2378                        {
2379                          "name": "C Test 1",
2380                          "outcome": "Passed",
2381                          "parse_rate": null,
2382                          "test_num": 1
2383                        }
2384                      ]
2385                    },
2386                    {
2387                      "name": "group2",
2388                      "children": [
2389                        {
2390                          "name": "C Test 2",
2391                          "outcome": "Passed",
2392                          "parse_rate": null,
2393                          "test_num": 2
2394                        },
2395                        {
2396                          "name": "C Test 3",
2397                          "outcome": "Failed",
2398                          "parse_rate": null,
2399                          "test_num": 3
2400                        }
2401                      ]
2402                    }
2403                  ],
2404                  "parse_failures": [
2405                    {
2406                      "name": "C Test 3",
2407                      "actual": "(translation_unit (expression_statement (number_literal)))",
2408                      "expected": "(translation_unit (expression_statement (string_literal)))",
2409                      "is_cst": false,
2410                    }
2411                  ],
2412                  "parse_stats": {
2413                    "successful_parses": 2,
2414                    "total_parses": 3,
2415                    "total_bytes": 9,
2416                    "total_duration": {
2417                      "secs": 0,
2418                      "nanos": 0,
2419                    }
2420                  },
2421                  "highlight_results": [],
2422                  "tag_results": [],
2423                  "query_results": []
2424                })
2425                .to_string()
2426            );
2427        }
2428    }
2429}