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