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