1use itertools::Itertools;
61use lsp_positions::Position;
62use lsp_positions::PositionedSubstring;
63use lsp_positions::SpanCalculator;
64use once_cell::sync::Lazy;
65use regex::Regex;
66use stack_graphs::arena::Handle;
67use stack_graphs::assert::Assertion;
68use stack_graphs::assert::AssertionError;
69use stack_graphs::assert::AssertionSource;
70use stack_graphs::assert::AssertionTarget;
71use stack_graphs::graph::File;
72use stack_graphs::graph::Node;
73use stack_graphs::graph::SourceInfo;
74use stack_graphs::graph::StackGraph;
75use stack_graphs::partial::PartialPaths;
76use stack_graphs::stitching::Database;
77use stack_graphs::stitching::StitcherConfig;
78use std::collections::HashMap;
79use std::path::Path;
80use std::path::PathBuf;
81use thiserror::Error;
82use tree_sitter_graph::Variables;
83
84use crate::CancellationFlag;
85
86const DEFINED: &'static str = "defined";
87const DEFINES: &'static str = "defines";
88const REFERS: &'static str = "refers";
89
90static PATH_REGEX: Lazy<Regex> =
91 Lazy::new(|| Regex::new(r#"---\s*path:\s*([^\s]+)\s*---"#).unwrap());
92static GLOBAL_REGEX: Lazy<Regex> =
93 Lazy::new(|| Regex::new(r#"---\s*global:\s*([^\s]+)=([^\s]+)\s*---"#).unwrap());
94static ASSERTION_REGEX: Lazy<Regex> =
95 Lazy::new(|| Regex::new(r#"(\^)\s*(\w+):\s*([^\s,]+(?:\s*,\s*[^\s,]+)*)?"#).unwrap());
96static LINE_NUMBER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\d+"#).unwrap());
97static NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"[^\s,]+"#).unwrap());
98
99#[derive(Debug, Error)]
101pub enum TestError {
102 AssertionRefersToNonSourceLine(usize),
103 DuplicateGlobalVariable(usize, String),
104 DuplicatePath(usize, String),
105 GlobalBeforeFirstFragment(usize),
106 InvalidAssertion(usize, String),
107 InvalidColumn(usize, usize, usize),
108}
109
110impl std::fmt::Display for TestError {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 match self {
113 Self::AssertionRefersToNonSourceLine(line) => {
114 write!(
115 f,
116 "Assertion on line {} refers to non-source line",
117 line + 1
118 )
119 }
120 Self::DuplicateGlobalVariable(line, global) => {
121 write!(
122 f,
123 "Duplicate global variable {} on line {}",
124 global,
125 line + 1
126 )
127 }
128 Self::DuplicatePath(line, path) => {
129 write!(f, "Duplicate path {} on line {}", path, line + 1)
130 }
131 Self::GlobalBeforeFirstFragment(line) => {
132 write!(f, "Global set before first fragment on line {}", line + 1)
133 }
134 Self::InvalidAssertion(line, assertion) => {
135 write!(f, "Invalid assertion {} on line {}", assertion, line + 1)
136 }
137 Self::InvalidColumn(line, column, regular_line) => write!(
138 f,
139 "Assertion on line {} refers to missing column {} on line {}",
140 line + 1,
141 column + 1,
142 regular_line + 1
143 ),
144 }
145 }
146}
147
148pub struct Test {
150 pub path: PathBuf,
151 pub fragments: Vec<TestFragment>,
152 pub graph: StackGraph,
153}
154
155#[derive(Debug, Clone)]
157pub struct TestFragment {
158 pub file: Handle<File>,
159 pub path: PathBuf,
160 pub source: String,
161 pub assertions: Vec<Assertion>,
162 pub globals: HashMap<String, String>,
163}
164
165impl Test {
166 pub fn from_source(
169 path: &Path,
170 source: &str,
171 default_fragment_path: &Path,
172 ) -> Result<Self, TestError> {
173 let mut graph = StackGraph::new();
174 let mut fragments = Vec::new();
175 let mut have_fragments = false;
176 let mut current_path = default_fragment_path.to_path_buf();
177 let mut current_source = String::new();
178 let mut current_globals = HashMap::new();
179 let mut have_globals = false;
180 let mut prev_source = String::new();
181 let mut line_files = Vec::new();
182 let mut line_count = 0;
183 for (current_line_number, current_line) in
184 PositionedSubstring::lines_iter(source).enumerate()
185 {
186 line_count += 1;
187 if let Some(m) = PATH_REGEX.captures_iter(current_line.content).next() {
188 if have_fragments {
192 let file = graph
193 .add_file(¤t_path.to_string_lossy())
194 .map_err(|_| {
195 TestError::DuplicatePath(
196 line_files.len(),
197 format!("{}", current_path.display()),
198 )
199 })?;
200 (line_files.len()..current_line_number)
201 .for_each(|_| line_files.push(Some(file)));
202 fragments.push(TestFragment {
203 file,
204 path: current_path,
205 source: current_source,
206 assertions: Vec::new(),
207 globals: current_globals,
208 });
209 } else {
210 if have_globals {
211 return Err(TestError::GlobalBeforeFirstFragment(current_line_number));
212 }
213 have_fragments = true;
214 (line_files.len()..current_line_number).for_each(|_| line_files.push(None));
215 }
216 current_path = m.get(1).unwrap().as_str().into();
217 current_source = prev_source.clone();
218 current_globals = HashMap::new();
219
220 Self::push_whitespace_for(¤t_line, &mut current_source);
221 } else if let Some(m) = GLOBAL_REGEX.captures_iter(current_line.content).next() {
222 have_globals = true;
223 let global_name = m.get(1).unwrap().as_str();
224 let global_value = m.get(2).unwrap().as_str();
225 if current_globals
226 .insert(global_name.into(), global_value.into())
227 .is_some()
228 {
229 return Err(TestError::DuplicateGlobalVariable(
230 current_line_number,
231 global_name.to_string(),
232 ));
233 }
234
235 Self::push_whitespace_for(¤t_line, &mut current_source);
236 } else {
237 current_source.push_str(current_line.content);
238 }
239 current_source.push_str("\n");
240
241 Self::push_whitespace_for(¤t_line, &mut prev_source);
242 prev_source.push_str("\n");
243 }
244 {
245 let file = graph
246 .add_file(¤t_path.to_string_lossy())
247 .map_err(|_| {
248 TestError::DuplicatePath(
249 line_files.len(),
250 format!("{}", current_path.display()),
251 )
252 })?;
253 (line_files.len()..line_count).for_each(|_| line_files.push(Some(file)));
254 fragments.push(TestFragment {
255 file,
256 path: current_path,
257 source: current_source,
258 assertions: Vec::new(),
259 globals: current_globals,
260 });
261 }
262
263 for fragment in &mut fragments {
264 fragment
265 .parse_assertions(&mut graph, |line| line_files.get(line).cloned().flatten())?;
266 }
267
268 Ok(Self {
269 path: path.to_path_buf(),
270 fragments,
271 graph,
272 })
273 }
274
275 fn push_whitespace_for(line: &PositionedSubstring, into: &mut String) {
290 (0..line.utf8_bounds.end).for_each(|_| into.push_str(" "));
291 }
292}
293
294impl TestFragment {
295 fn parse_assertions<F>(&mut self, graph: &mut StackGraph, line_file: F) -> Result<(), TestError>
297 where
298 F: Fn(usize) -> Option<Handle<File>>,
299 {
300 self.assertions.clear();
301
302 let mut current_line_span_calculator = SpanCalculator::new(&self.source);
303 let mut last_regular_line: Option<PositionedSubstring> = None;
304 let mut last_regular_line_number = None;
305 let mut last_regular_line_span_calculator = SpanCalculator::new(&self.source);
306 for (current_line_number, current_line) in
307 PositionedSubstring::lines_iter(&self.source).enumerate()
308 {
309 if let Some(m) = ASSERTION_REGEX.captures_iter(current_line.content).next() {
310 let last_regular_line = last_regular_line.as_ref().ok_or_else(|| {
312 TestError::AssertionRefersToNonSourceLine(current_line_number)
313 })?;
314 let last_regular_line_number = last_regular_line_number.unwrap();
315
316 let carret_match = m.get(1).unwrap();
317 let assertion_match = m.get(2).unwrap();
318 let values_match = m.get(3);
319
320 let column_utf8_offset = carret_match.start();
321 let column_grapheme_offset = current_line_span_calculator
322 .for_line_and_column(
323 current_line_number,
324 current_line.utf8_bounds.start,
325 column_utf8_offset,
326 )
327 .column
328 .grapheme_offset;
329 if column_grapheme_offset >= last_regular_line.grapheme_length {
330 return Err(TestError::InvalidColumn(
331 current_line_number,
332 column_grapheme_offset,
333 last_regular_line_number,
334 ));
335 }
336 let position = last_regular_line_span_calculator.for_line_and_grapheme(
337 last_regular_line_number,
338 last_regular_line.utf8_bounds.start,
339 column_grapheme_offset,
340 );
341 let source = AssertionSource {
342 file: self.file,
343 position,
344 };
345
346 match assertion_match.as_str() {
347 DEFINED => {
348 let mut targets = Vec::new();
349 for line in LINE_NUMBER_REGEX
350 .find_iter(values_match.map(|m| m.as_str()).unwrap_or(""))
351 {
352 let line = line.as_str().parse::<usize>().unwrap() - 1;
353 let file = line_file(line).ok_or(
354 TestError::AssertionRefersToNonSourceLine(current_line_number),
355 )?;
356 targets.push(AssertionTarget { file, line });
357 }
358 self.assertions.push(Assertion::Defined { source, targets });
359 }
360 DEFINES => {
361 let mut symbols = Vec::new();
362 for name in
363 NAME_REGEX.find_iter(values_match.map(|m| m.as_str()).unwrap_or(""))
364 {
365 let symbol = graph.add_symbol(name.as_str());
366 symbols.push(symbol);
367 }
368 self.assertions.push(Assertion::Defines { source, symbols });
369 }
370 REFERS => {
371 let mut symbols = Vec::new();
372 for name in
373 NAME_REGEX.find_iter(values_match.map(|m| m.as_str()).unwrap_or(""))
374 {
375 let symbol = graph.add_symbol(name.as_str());
376 symbols.push(symbol);
377 }
378 self.assertions.push(Assertion::Refers { source, symbols });
379 }
380 _ => {
381 return Err(TestError::InvalidAssertion(
382 current_line_number,
383 assertion_match.as_str().to_string(),
384 ));
385 }
386 }
387 } else {
388 last_regular_line = Some(current_line);
390 last_regular_line_number = Some(current_line_number);
391 }
392 }
393
394 Ok(())
395 }
396}
397
398#[derive(Debug, Clone)]
400pub struct TestResult {
401 success_count: usize,
402 failures: Vec<TestFailure>,
403}
404
405impl TestResult {
406 pub fn new() -> Self {
407 Self {
408 failures: Vec::new(),
409 success_count: 0,
410 }
411 }
412
413 fn add_success(&mut self) {
414 self.success_count += 1;
415 }
416
417 fn add_failure(&mut self, reason: TestFailure) {
418 self.failures.push(reason);
419 }
420
421 pub fn success_count(&self) -> usize {
423 self.success_count
424 }
425
426 pub fn failure_count(&self) -> usize {
428 self.failures.len()
429 }
430
431 pub fn failures_iter(&self) -> std::slice::Iter<'_, TestFailure> {
432 self.failures.iter()
433 }
434
435 pub fn into_failures_iter(self) -> std::vec::IntoIter<TestFailure> {
436 self.failures.into_iter()
437 }
438
439 pub fn count(&self) -> usize {
441 self.success_count() + self.failure_count()
442 }
443
444 pub fn absorb(&mut self, other: TestResult) {
445 self.success_count += other.success_count;
446 let mut failures = other.failures;
447 self.failures.append(&mut failures);
448 }
449}
450
451impl std::fmt::Display for TestResult {
452 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
453 write!(
454 f,
455 "{} tests: {} passed, {} failed",
456 self.count(),
457 self.success_count(),
458 self.failure_count()
459 )
460 }
461}
462
463#[derive(Debug, Clone)]
470pub enum TestFailure {
471 NoReferences {
472 path: PathBuf,
473 position: Position,
474 },
475 IncorrectResolutions {
476 path: PathBuf,
477 position: Position,
478 references: Vec<String>,
479 missing_lines: Vec<usize>,
480 unexpected_lines: HashMap<String, Vec<Option<usize>>>,
481 },
482 IncorrectDefinitions {
483 path: PathBuf,
484 position: Position,
485 missing_symbols: Vec<String>,
486 unexpected_symbols: Vec<String>,
487 },
488 IncorrectReferences {
489 path: PathBuf,
490 position: Position,
491 missing_symbols: Vec<String>,
492 unexpected_symbols: Vec<String>,
493 },
494 Cancelled(stack_graphs::CancellationError),
495}
496
497impl std::fmt::Display for TestFailure {
498 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
499 match self {
500 Self::NoReferences { path, position } => {
501 write!(
502 f,
503 "{}:{}:{}: no references found",
504 path.display(),
505 position.line + 1,
506 position.column.grapheme_offset + 1
507 )
508 }
509 Self::IncorrectResolutions {
510 path,
511 position,
512 references,
513 missing_lines,
514 unexpected_lines,
515 } => {
516 write!(
517 f,
518 "{}:{}:{}: ",
519 path.display(),
520 position.line + 1,
521 position.column.grapheme_offset + 1
522 )?;
523 write!(f, "definition(s) for reference(s)")?;
524 for reference in references {
525 write!(f, " ‘{}’", reference)?;
526 }
527 if !missing_lines.is_empty() {
528 write!(
529 f,
530 " missing expected on line(s) {}",
531 missing_lines.iter().map(|l| l + 1).format(", ")
532 )?;
533 }
534 if !unexpected_lines.is_empty() {
535 write!(f, " found unexpected",)?;
536 let mut first = true;
537 for (definition, lines) in unexpected_lines.into_iter() {
538 if first {
539 first = false;
540 } else {
541 write!(f, ",")?;
542 }
543 write!(f, " ‘{}’ on lines(s) ", definition)?;
544 write!(
545 f,
546 "{}",
547 lines
548 .into_iter()
549 .map(|l| l.map(|l| format!("{}", l + 1)).unwrap_or("?".into()))
550 .format(", ")
551 )?;
552 }
553 }
554 Ok(())
555 }
556 Self::IncorrectDefinitions {
557 path,
558 position,
559 missing_symbols,
560 unexpected_symbols,
561 } => {
562 write!(
563 f,
564 "{}:{}:{}: definitions",
565 path.display(),
566 position.line + 1,
567 position.column.grapheme_offset + 1
568 )?;
569 if !missing_symbols.is_empty() {
570 write!(
571 f,
572 " missing expected {}",
573 missing_symbols.iter().format(", ")
574 )?;
575 }
576 if !unexpected_symbols.is_empty() {
577 write!(
578 f,
579 " found unexpected {}",
580 unexpected_symbols.iter().format(", ")
581 )?;
582 }
583 Ok(())
584 }
585 Self::IncorrectReferences {
586 path,
587 position,
588 missing_symbols,
589 unexpected_symbols,
590 } => {
591 write!(
592 f,
593 "{}:{}:{}: references",
594 path.display(),
595 position.line + 1,
596 position.column.grapheme_offset + 1
597 )?;
598 if !missing_symbols.is_empty() {
599 write!(
600 f,
601 " missing expected {}",
602 missing_symbols.iter().format(", ")
603 )?;
604 }
605 if !unexpected_symbols.is_empty() {
606 write!(
607 f,
608 " found unexpected {}",
609 unexpected_symbols.iter().format(", ")
610 )?;
611 }
612 Ok(())
613 }
614 Self::Cancelled(err) => write!(f, "{}", err),
615 }
616 }
617}
618
619impl Test {
620 pub fn run(
624 &mut self,
625 partials: &mut PartialPaths,
626 db: &mut Database,
627 stitcher_config: StitcherConfig,
628 cancellation_flag: &dyn CancellationFlag,
629 ) -> Result<TestResult, stack_graphs::CancellationError> {
630 let mut result = TestResult::new();
631 for fragment in &self.fragments {
632 for assertion in &fragment.assertions {
633 match assertion
634 .run(
635 &self.graph,
636 partials,
637 db,
638 stitcher_config,
639 &cancellation_flag,
640 )
641 .map_or_else(|e| self.from_error(e), |v| Ok(v))
642 {
643 Ok(_) => result.add_success(),
644 Err(f) => result.add_failure(f),
645 }
646 }
647 }
648 Ok(result)
649 }
650
651 fn from_error(&self, err: AssertionError) -> Result<(), TestFailure> {
653 match err {
654 AssertionError::NoReferences { source } => Err(TestFailure::NoReferences {
655 path: self.path.clone(),
656 position: source.position,
657 }),
658 AssertionError::IncorrectlyDefined {
659 source,
660 references,
661 missing_targets,
662 unexpected_paths,
663 } => {
664 let references = references
665 .into_iter()
666 .map(|r| self.graph[self.graph[r].symbol().unwrap()].to_string())
667 .unique()
668 .sorted()
669 .collect();
670 let missing_lines = missing_targets
671 .into_iter()
672 .map(|t| t.line)
673 .unique()
674 .sorted()
675 .collect::<Vec<_>>();
676 let unexpected_lines = unexpected_paths
677 .into_iter()
678 .filter(|p| {
679 self.fragments
681 .iter()
682 .any(|f| f.file == self.graph[p.end_node].id().file().unwrap())
683 })
684 .map(|p| {
685 let symbol =
686 self.graph[self.graph[p.end_node].symbol().unwrap()].to_string();
687 let line = self
688 .get_source_info(p.end_node)
689 .map(|si| si.span.start.line);
690 (symbol, line)
691 })
692 .unique()
693 .sorted()
694 .into_group_map();
695 if missing_lines.is_empty() && unexpected_lines.is_empty() {
696 return Ok(());
697 }
698 Err(TestFailure::IncorrectResolutions {
699 path: self.path.clone(),
700 position: source.position,
701 references,
702 missing_lines,
703 unexpected_lines,
704 })
705 }
706 AssertionError::IncorrectDefinitions {
707 source,
708 missing_symbols,
709 unexpected_symbols,
710 } => {
711 let missing_symbols = missing_symbols
712 .iter()
713 .map(|s| self.graph[*s].to_string())
714 .collect::<Vec<_>>();
715 let unexpected_symbols = unexpected_symbols
716 .iter()
717 .map(|s| self.graph[*s].to_string())
718 .collect::<Vec<_>>();
719 Err(TestFailure::IncorrectDefinitions {
720 path: self.path.clone(),
721 position: source.position,
722 missing_symbols,
723 unexpected_symbols,
724 })
725 }
726 AssertionError::IncorrectReferences {
727 source,
728 missing_symbols,
729 unexpected_symbols,
730 } => {
731 let missing_symbols = missing_symbols
732 .iter()
733 .map(|s| self.graph[*s].to_string())
734 .collect::<Vec<_>>();
735 let unexpected_symbols = unexpected_symbols
736 .iter()
737 .map(|s| self.graph[*s].to_string())
738 .collect::<Vec<_>>();
739 Err(TestFailure::IncorrectReferences {
740 path: self.path.clone(),
741 position: source.position,
742 missing_symbols,
743 unexpected_symbols,
744 })
745 }
746 AssertionError::Cancelled(err) => Err(TestFailure::Cancelled(err)),
747 }
748 }
749
750 fn get_source_info(&self, node: Handle<Node>) -> Option<&SourceInfo> {
752 self.graph.source_info(node).filter(|si| {
753 !(si.span.start.line == 0
754 && si.span.start.column.utf8_offset == 0
755 && si.span.end.line == 0
756 && si.span.end.column.utf8_offset == 0)
757 })
758 }
759}
760
761impl TestFragment {
762 pub fn add_globals_to(&self, variables: &mut Variables) {
763 for (name, value) in self.globals.iter() {
764 variables
765 .add(name.as_str().into(), value.as_str().into())
766 .unwrap();
767 }
768 }
769}