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#[derive(Debug, Default, Serialize, JsonSchema)]
144pub struct TestSummary {
145 #[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 #[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 #[schemars(skip)]
171 #[serde(skip)]
172 pub test_num: usize,
173 #[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
226impl TestResultHierarchy {
229 fn push_traversal(&mut self, idx: usize) {
231 self.traversal_idxs.push(idx);
232 }
233
234 pub fn pop_traversal(&mut self) {
237 self.traversal_idxs.pop();
238 }
239
240 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 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 fn push(&mut self, result: TestResult) {
263 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 #[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 Passed,
382 Failed,
383 Updated,
384 Skipped,
385 Platform,
386
387 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 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 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 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
813fn 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 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 if actual.contains("ERROR") || actual.contains("MISSING") {
1003 test_summary.has_parse_errors = true;
1004
1005 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 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 return Ok(false);
1129 }
1130 }
1131 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
1146fn 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#[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 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 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 if skip {
1346 error = false;
1347 }
1348
1349 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 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 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 let output = COMMENT_REGEX.replace_all(output, "").to_string();
1433
1434 let output = WHITESPACE_REGEX.replace_all(output.trim(), " ");
1436 let output = output.replace(" )", ")");
1437
1438 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 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 {
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 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 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 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 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 TestEntry::Group {
2311 name: "group3".to_string(),
2312 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 {
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}