1use std::collections::{BTreeMap, VecDeque};
51use std::ffi::OsStr;
52use std::path::{Path, PathBuf};
53
54use crate::discovery::Language;
55use crate::frontend::{Token, TokenKind};
56
57pub const TEST_CODE_VERSION: &str = "test-code-v1";
63
64pub const DEFAULT_TEST_PATHS: &[&str] = &[
71 "**/tests/**",
72 "**/test/**",
73 "**/__tests__/**",
74 "**/*_test.*",
75 "**/*_tests.*",
76 "**/test_*.*",
77 "**/*_spec.*",
78];
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
86#[serde(rename_all = "lowercase")]
87pub enum TestCodeEvidence {
88 Marker,
90 Path,
92}
93
94impl TestCodeEvidence {
95 #[must_use]
97 pub const fn name(self) -> &'static str {
98 match self {
99 Self::Marker => "marker",
100 Self::Path => "path",
101 }
102 }
103
104 #[must_use]
106 pub fn from_name(name: &str) -> Option<Self> {
107 match name {
108 "marker" => Some(Self::Marker),
109 "path" => Some(Self::Path),
110 _ => None,
111 }
112 }
113}
114
115#[must_use]
121pub fn aggregate_evidence(
122 evidence: impl IntoIterator<Item = Option<TestCodeEvidence>>,
123) -> Option<TestCodeEvidence> {
124 let mut any = false;
125 let mut marker = false;
126 for item in evidence {
127 let item = item?;
128 any = true;
129 marker |= item == TestCodeEvidence::Marker;
130 }
131 any.then_some(if marker {
132 TestCodeEvidence::Marker
133 } else {
134 TestCodeEvidence::Path
135 })
136}
137
138const TEST_IDENT: &str = "test";
144
145const CASE_MACROS: &[&str] = &[
163 "BENCHMARK_DEFINE_F",
164 "BENCHMARK_F",
165 "BENCHMARK_TEMPLATE_F",
166 "BOOST_AUTO_TEST_CASE",
167 "BOOST_AUTO_TEST_CASE_TEMPLATE",
168 "BOOST_DATA_TEST_CASE",
169 "BOOST_FIXTURE_TEST_CASE",
170 "SCENARIO",
171 "TEMPLATE_TEST_CASE",
172 "TEST",
173 "TEST_CASE",
174 "TEST_CASE_METHOD",
175 "TEST_F",
176 "TEST_P",
177 "TYPED_TEST",
178 "TYPED_TEST_P",
179];
180
181#[must_use]
190pub fn is_marked(language: Language, tokens: &[Token]) -> bool {
191 match language {
192 Language::Rust => rust_attributes(tokens).any(names_test),
193 Language::C | Language::Cpp => opens_a_case(tokens),
199 }
200}
201
202fn opens_a_case(tokens: &[Token]) -> bool {
207 let [name, open, ..] = tokens else {
208 return false;
209 };
210 name.kind == TokenKind::Identifier
211 && CASE_MACROS.contains(&&*name.text)
212 && open.kind == TokenKind::Punctuation
213 && open.text == "("
214}
215
216fn rust_attributes(tokens: &[Token]) -> impl Iterator<Item = &[Token]> {
219 let mut rest = tokens;
220 std::iter::from_fn(move || {
221 let (body, tail) = leading_attribute(rest)?;
222 rest = tail;
223 Some(body)
224 })
225}
226
227fn leading_attribute(tokens: &[Token]) -> Option<(&[Token], &[Token])> {
233 let after_hash = after_punctuation(tokens, "#")?;
234 let after_bang = after_punctuation(after_hash, "!").unwrap_or(after_hash);
235 let body = after_punctuation(after_bang, "[")?;
236 let end = closing_bracket(body)?;
237 Some((&body[..end], &body[end + 1..]))
238}
239
240fn after_attributes(tokens: &[Token]) -> &[Token] {
242 let mut rest = tokens;
243 while let Some((_, tail)) = leading_attribute(rest) {
244 rest = tail;
245 }
246 rest
247}
248
249fn after_punctuation<'a>(tokens: &'a [Token], text: &str) -> Option<&'a [Token]> {
251 let (first, rest) = tokens.split_first()?;
252 (first.kind == TokenKind::Punctuation && first.text == text).then_some(rest)
253}
254
255fn closing_bracket(body: &[Token]) -> Option<usize> {
258 closing(body, "[", "]")
259}
260
261fn closing(body: &[Token], open: &str, close: &str) -> Option<usize> {
264 let mut depth = 0usize;
265 for (index, token) in body.iter().enumerate() {
266 if token.kind != TokenKind::Punctuation {
267 continue;
268 }
269 if &*token.text == open {
270 depth += 1;
271 } else if &*token.text == close {
272 if depth == 0 {
273 return Some(index);
274 }
275 depth -= 1;
276 }
277 }
278 None
279}
280
281fn names_test(body: &[Token]) -> bool {
283 let Some((head, arguments)) = attribute_parts(body) else {
284 return false;
285 };
286 match (head, arguments) {
287 ("cfg", Some(predicate)) => {
288 predicate_values(predicate, false) & TRUE_VALUE == 0
289 && predicate_values(predicate, true) & TRUE_VALUE != 0
290 }
291 ("cfg_attr", Some(arguments)) => split_arguments(arguments)
292 .into_iter()
293 .skip(1)
294 .any(names_test),
295 (TEST_IDENT, _) => true,
296 _ => false,
297 }
298}
299
300fn attribute_parts(body: &[Token]) -> Option<(&str, Option<&[Token]>)> {
302 let open = body.iter().position(|token| token.text == "(");
303 let path = open.map_or(body, |index| &body[..index]);
304 let head = path
305 .iter()
306 .rev()
307 .find(|token| matches!(token.kind, TokenKind::Identifier | TokenKind::Keyword))?
308 .text
309 .as_str();
310 let arguments = open.and_then(|index| {
311 let tail = &body[index + 1..];
312 closing(tail, "(", ")").map(|end| &tail[..end])
313 });
314 Some((head, arguments))
315}
316
317const FALSE_VALUE: u8 = 1;
318const TRUE_VALUE: u8 = 2;
319const BOTH_VALUES: u8 = FALSE_VALUE | TRUE_VALUE;
320
321fn predicate_values(tokens: &[Token], test_enabled: bool) -> u8 {
323 let Some((head, arguments)) = attribute_parts(tokens) else {
324 return BOTH_VALUES;
325 };
326 match (head, arguments) {
327 (TEST_IDENT, None) => {
328 if test_enabled {
329 TRUE_VALUE
330 } else {
331 FALSE_VALUE
332 }
333 }
334 ("not", Some(arguments)) => {
335 let values = predicate_values(arguments, test_enabled);
336 ((values & FALSE_VALUE) << 1) | ((values & TRUE_VALUE) >> 1)
337 }
338 ("all", Some(arguments)) => split_arguments(arguments)
339 .into_iter()
340 .map(|argument| predicate_values(argument, test_enabled))
341 .fold(TRUE_VALUE, possible_and),
342 ("any", Some(arguments)) => split_arguments(arguments)
343 .into_iter()
344 .map(|argument| predicate_values(argument, test_enabled))
345 .fold(FALSE_VALUE, possible_or),
346 _ => BOTH_VALUES,
347 }
348}
349
350fn possible_and(left: u8, right: u8) -> u8 {
351 possible_binary(left, right, |a, b| a && b)
352}
353
354fn possible_or(left: u8, right: u8) -> u8 {
355 possible_binary(left, right, |a, b| a || b)
356}
357
358fn possible_binary(left: u8, right: u8, operation: impl Fn(bool, bool) -> bool) -> u8 {
359 let mut values = 0;
360 for left_value in [false, true] {
361 if left & value_bit(left_value) == 0 {
362 continue;
363 }
364 for right_value in [false, true] {
365 if right & value_bit(right_value) != 0 {
366 values |= value_bit(operation(left_value, right_value));
367 }
368 }
369 }
370 values
371}
372
373const fn value_bit(value: bool) -> u8 {
374 if value { TRUE_VALUE } else { FALSE_VALUE }
375}
376
377fn split_arguments(tokens: &[Token]) -> Vec<&[Token]> {
379 let mut arguments = Vec::new();
380 let mut start = 0;
381 let mut depth = 0usize;
382 for (index, token) in tokens.iter().enumerate() {
383 match token.text.as_str() {
384 "(" | "[" | "{" => depth += 1,
385 ")" | "]" | "}" => depth = depth.saturating_sub(1),
386 "," if depth == 0 => {
387 arguments.push(&tokens[start..index]);
388 start = index + 1;
389 }
390 _ => {}
391 }
392 }
393 arguments.push(&tokens[start..]);
394 arguments
395}
396
397#[derive(Debug, Clone, Copy)]
399pub struct ModuleFile<'a> {
400 pub path: &'a Path,
404 pub language: Language,
406 pub tokens: &'a [Token],
408}
409
410#[must_use]
422pub fn declared_test_modules(files: &[ModuleFile<'_>]) -> Vec<bool> {
423 let mut suite = vec![false; files.len()];
424 let by_path: BTreeMap<&Path, usize> = files
425 .iter()
426 .enumerate()
427 .filter(|(_, file)| file.language == Language::Rust)
428 .map(|(index, file)| (file.path, index))
429 .collect();
430 let declared: Vec<Vec<Declaration<'_>>> = files
431 .iter()
432 .map(|file| {
433 if file.language == Language::Rust {
434 module_declarations(file.tokens)
435 } else {
436 Vec::new()
437 }
438 })
439 .collect();
440
441 let mut pending = VecDeque::new();
442 let enter = |name: &str, from: &Path, suite: &mut Vec<bool>, pending: &mut VecDeque<_>| {
443 for candidate in module_bodies(from, name) {
444 if let Some(&index) = by_path.get(candidate.as_path()) {
445 if !suite[index] {
446 suite[index] = true;
447 pending.push_back(index);
448 }
449 }
450 }
451 };
452
453 for (index, declarations) in declared.iter().enumerate() {
454 for declaration in declarations.iter().filter(|entry| entry.marked) {
455 enter(
456 declaration.name,
457 files[index].path,
458 &mut suite,
459 &mut pending,
460 );
461 }
462 }
463 while let Some(index) = pending.pop_front() {
466 for declaration in &declared[index] {
467 enter(
468 declaration.name,
469 files[index].path,
470 &mut suite,
471 &mut pending,
472 );
473 }
474 }
475 suite
476}
477
478struct Declaration<'a> {
481 name: &'a str,
482 marked: bool,
483}
484
485fn module_declarations(tokens: &[Token]) -> Vec<Declaration<'_>> {
487 let mut declarations = Vec::new();
488 for item in top_level_items(tokens) {
489 if let Some(name) = bodiless_module(item) {
490 declarations.push(Declaration {
491 name,
492 marked: rust_attributes(item).any(names_test),
493 });
494 }
495 }
496 declarations
497}
498
499fn top_level_items(tokens: &[Token]) -> impl Iterator<Item = &[Token]> {
507 let mut start = 0usize;
508 let mut depth = 0usize;
509 let mut index = 0usize;
510 std::iter::from_fn(move || {
511 while index < tokens.len() {
512 let token = &tokens[index];
513 index += 1;
514 if token.kind != TokenKind::Punctuation {
515 continue;
516 }
517 let ends = match &*token.text {
518 "{" | "(" | "[" => {
519 depth += 1;
520 false
521 }
522 "}" | ")" | "]" => {
526 depth = depth.saturating_sub(1);
527 depth == 0 && token.text == "}"
528 }
529 ";" => depth == 0,
530 _ => false,
531 };
532 if ends {
533 let item = &tokens[start..index];
534 start = index;
535 return Some(item);
536 }
537 }
538 None
539 })
540}
541
542fn bodiless_module(item: &[Token]) -> Option<&str> {
549 let rest = after_visibility(after_attributes(item));
550 let [keyword, name, terminator] = rest else {
551 return None;
552 };
553 let declares = word_is(keyword, "mod")
554 && name.kind == TokenKind::Identifier
555 && terminator.kind == TokenKind::Punctuation
556 && terminator.text == ";";
557 declares.then(|| &*name.text)
558}
559
560fn after_visibility(tokens: &[Token]) -> &[Token] {
563 let Some((first, rest)) = tokens.split_first() else {
564 return tokens;
565 };
566 if !word_is(first, "pub") {
567 return tokens;
568 }
569 let Some(restriction) = after_punctuation(rest, "(") else {
570 return rest;
571 };
572 closing(restriction, "(", ")").map_or(rest, |end| &restriction[end + 1..])
573}
574
575fn word_is(token: &Token, word: &str) -> bool {
577 matches!(token.kind, TokenKind::Identifier | TokenKind::Keyword) && token.text == word
578}
579
580fn module_bodies(from: &Path, name: &str) -> [PathBuf; 2] {
586 let directory = from.parent().unwrap_or_else(|| Path::new(""));
587 let base = match from.file_stem().and_then(OsStr::to_str) {
590 Some("mod" | "lib" | "main") | None => directory.to_path_buf(),
591 Some(stem) => directory.join(stem),
592 };
593 [
594 base.join(format!("{name}.rs")),
595 base.join(name).join("mod.rs"),
596 ]
597}
598
599#[cfg(test)]
600mod tests {
601 #![allow(clippy::unwrap_used)]
602
603 use super::*;
604 use crate::frontend::{Lexeme, SourceSpan};
605
606 fn tokens(pieces: &[&str]) -> Vec<Token> {
609 pieces
610 .iter()
611 .map(|piece| Token {
612 kind: if piece.chars().next().is_some_and(char::is_alphanumeric) {
613 TokenKind::Identifier
614 } else {
615 TokenKind::Punctuation
616 },
617 text: Lexeme::from(*piece),
618 span: SourceSpan {
619 start_byte: 0,
620 end_byte: 0,
621 start_line: 1,
622 start_column: 1,
623 },
624 })
625 .collect()
626 }
627
628 #[test]
629 fn a_test_attribute_marks_the_item_it_precedes() {
630 let source = tokens(&["#", "[", "test", "]", "fn", "check", "(", ")"]);
631 assert!(is_marked(Language::Rust, &source));
632 }
633
634 #[test]
635 fn a_configuration_predicate_naming_tests_marks_the_item() {
636 let source = tokens(&["#", "[", "cfg", "(", "test", ")", "]", "mod", "tests", "{"]);
637 assert!(is_marked(Language::Rust, &source));
638 }
639
640 #[test]
641 fn a_runtime_qualified_test_attribute_is_still_a_test_attribute() {
642 let source = tokens(&["#", "[", "tokio", ":", ":", "test", "]", "fn", "check"]);
643 assert!(is_marked(Language::Rust, &source));
644 }
645
646 #[test]
647 fn the_marker_is_read_only_from_the_leading_attributes() {
648 let source = tokens(&["#", "[", "inline", "]", "fn", "test", "(", "test", ")"]);
652 assert!(!is_marked(Language::Rust, &source));
653 }
654
655 #[test]
656 fn a_nested_attribute_is_searched_to_its_own_end() {
657 let source = tokens(&[
658 "#", "[", "cfg", "(", "all", "(", "unix", ",", "test", ")", ")", "]", "fn", "check",
659 ]);
660 assert!(is_marked(Language::Rust, &source));
661 }
662
663 #[test]
664 fn negated_test_cfg_marks_production_code_not_test_code() {
665 let source = tokens(&[
666 "#",
667 "[",
668 "cfg",
669 "(",
670 "not",
671 "(",
672 "test",
673 ")",
674 ")",
675 "]",
676 "fn",
677 "production",
678 ]);
679 assert!(!is_marked(Language::Rust, &source));
680
681 let double_negated = tokens(&[
682 "#", "[", "cfg", "(", "not", "(", "not", "(", "test", ")", ")", ")", "]", "fn", "check",
683 ]);
684 assert!(is_marked(Language::Rust, &double_negated));
685 }
686
687 #[test]
688 fn cfg_attr_condition_is_not_mistaken_for_the_applied_attribute() {
689 let production = tokens(&[
690 "#",
691 "[",
692 "cfg_attr",
693 "(",
694 "test",
695 ",",
696 "allow",
697 "(",
698 "dead_code",
699 ")",
700 ")",
701 "]",
702 "fn",
703 "production",
704 ]);
705 assert!(!is_marked(Language::Rust, &production));
706
707 let test = tokens(&[
708 "#", "[", "cfg_attr", "(", "feature", "=", "runtime", ",", "tokio", ":", ":", "test",
709 ")", "]", "fn", "check",
710 ]);
711 assert!(is_marked(Language::Rust, &test));
712 }
713
714 #[test]
715 fn a_marker_after_an_unrelated_attribute_is_still_found() {
716 let source = tokens(&[
717 "#",
718 "[",
719 "allow",
720 "(",
721 "dead_code",
722 ")",
723 "]",
724 "#",
725 "[",
726 "test",
727 "]",
728 "fn",
729 "check",
730 ]);
731 assert!(is_marked(Language::Rust, &source));
732 }
733
734 #[test]
735 fn an_inner_attribute_is_read_like_an_outer_one() {
736 let source = tokens(&["#", "!", "[", "cfg", "(", "test", ")", "]", "fn", "check"]);
737 assert!(is_marked(Language::Rust, &source));
738 }
739
740 #[test]
741 fn a_truncated_attribute_marks_nothing() {
742 let source = tokens(&["#", "[", "test", "fn", "check"]);
745 assert!(!is_marked(Language::Rust, &source));
746 }
747
748 #[test]
749 fn a_name_that_merely_contains_the_word_is_not_a_marker() {
750 let source = tokens(&["#", "[", "test_util", ":", ":", "setup", "]", "fn", "check"]);
751 assert!(!is_marked(Language::Rust, &source));
752 }
753
754 #[test]
755 fn attribute_syntax_marks_nothing_in_c_or_cpp() {
756 let source = tokens(&["#", "[", "test", "]", "void", "check", "(", ")"]);
759 assert!(!is_marked(Language::C, &source));
760 assert!(!is_marked(Language::Cpp, &source));
761 }
762
763 #[test]
764 fn a_case_macro_marks_the_definition_it_opens() {
765 for name in ["TEST", "TEST_F", "BOOST_AUTO_TEST_CASE", "TEST_CASE"] {
766 let source = tokens(&[name, "(", "Suite", ",", "Name", ")", "{"]);
767 assert!(is_marked(Language::Cpp, &source), "{name}");
768 assert!(is_marked(Language::C, &source), "{name}");
769 }
770 }
771
772 #[test]
773 fn a_case_macro_is_a_marker_only_where_it_declares_something() {
774 let source = tokens(&["TEST", ";"]);
776 assert!(!is_marked(Language::Cpp, &source));
777 let source = tokens(&["void", "run", "(", ")", "{", "TEST", "(", "x", ")"]);
779 assert!(!is_marked(Language::Cpp, &source));
780 }
781
782 #[test]
783 fn a_name_that_merely_starts_with_a_case_macro_is_not_one() {
784 let source = tokens(&["TEST_HELPER", "(", "x", ")", "{"]);
785 assert!(!is_marked(Language::Cpp, &source));
786 }
787
788 #[test]
789 fn rust_does_not_read_the_c_markers() {
790 let source = tokens(&["TEST", "(", "Suite", ",", "Name", ")", "{"]);
792 assert!(!is_marked(Language::Rust, &source));
793 }
794
795 #[test]
796 fn the_case_macro_list_is_sorted_and_free_of_repeats() {
797 let mut sorted = CASE_MACROS.to_vec();
800 sorted.sort_unstable();
801 sorted.dedup();
802 assert_eq!(sorted, CASE_MACROS);
803 }
804
805 #[test]
806 fn an_empty_item_marks_nothing() {
807 assert!(!is_marked(Language::Rust, &[]));
808 }
809
810 #[test]
811 fn default_test_paths_cover_directories_and_rust_c_cpp_file_conventions() {
812 assert_eq!(
813 DEFAULT_TEST_PATHS,
814 [
815 "**/tests/**",
816 "**/test/**",
817 "**/__tests__/**",
818 "**/*_test.*",
819 "**/*_tests.*",
820 "**/test_*.*",
821 "**/*_spec.*",
822 ]
823 );
824 }
825
826 #[test]
827 fn aggregate_evidence_requires_every_member_and_prefers_markers() {
828 assert_eq!(
829 aggregate_evidence([Some(TestCodeEvidence::Path), Some(TestCodeEvidence::Path)]),
830 Some(TestCodeEvidence::Path)
831 );
832 assert_eq!(
833 aggregate_evidence([Some(TestCodeEvidence::Path), Some(TestCodeEvidence::Marker),]),
834 Some(TestCodeEvidence::Marker)
835 );
836 assert_eq!(
837 aggregate_evidence([Some(TestCodeEvidence::Marker), None]),
838 None
839 );
840 }
841
842 fn suite_over(files: &[(&str, &[&str])]) -> Vec<bool> {
845 let streams: Vec<Vec<Token>> = files.iter().map(|(_, pieces)| tokens(pieces)).collect();
846 let inputs: Vec<ModuleFile<'_>> = files
847 .iter()
848 .zip(&streams)
849 .map(|((path, _), stream)| ModuleFile {
850 path: Path::new(path),
851 language: Language::Rust,
852 tokens: stream,
853 })
854 .collect();
855 declared_test_modules(&inputs)
856 }
857
858 #[test]
859 fn a_declared_test_module_puts_the_file_it_names_in_the_suite() {
860 let suite = suite_over(&[
861 (
862 "src/lib.rs",
863 &["#", "[", "cfg", "(", "test", ")", "]", "mod", "tests", ";"],
864 ),
865 ("src/tests.rs", &["fn", "check", "(", ")", "{", "}"]),
866 ]);
867 assert_eq!(suite, vec![false, true]);
868 }
869
870 #[test]
871 fn a_test_module_hands_the_suite_on_to_what_it_declares() {
872 let suite = suite_over(&[
875 (
876 "src/lib.rs",
877 &["#", "[", "cfg", "(", "test", ")", "]", "mod", "tests", ";"],
878 ),
879 ("src/tests.rs", &["mod", "parser", ";"]),
880 ("src/tests/parser.rs", &["fn", "check", "(", ")", "{", "}"]),
881 ]);
882 assert_eq!(suite, vec![false, true, true]);
883 }
884
885 #[test]
886 fn a_declaration_below_the_code_it_covers_is_still_found() {
887 let suite = suite_over(&[
890 (
891 "src/lib.rs",
892 &[
893 "pub", "fn", "width", "(", ")", "{", "text", ".", "count", "(", ")", "}", "#",
894 "[", "cfg", "(", "test", ")", "]", "mod", "tests", ";",
895 ],
896 ),
897 ("src/tests.rs", &["fn", "check", "(", ")", "{", "}"]),
898 ]);
899 assert_eq!(suite, vec![false, true]);
900 }
901
902 #[test]
903 fn a_module_whose_body_is_a_directory_is_found_there() {
904 let suite = suite_over(&[
905 (
906 "src/lib.rs",
907 &["#", "[", "cfg", "(", "test", ")", "]", "mod", "tests", ";"],
908 ),
909 ("src/tests/mod.rs", &["fn", "check", "(", ")", "{", "}"]),
910 ]);
911 assert_eq!(suite, vec![false, true]);
912 }
913
914 #[test]
915 fn a_module_declared_without_the_marker_is_ordinary_code() {
916 let suite = suite_over(&[
917 ("src/lib.rs", &["mod", "parser", ";"]),
918 ("src/parser.rs", &["fn", "check", "(", ")", "{", "}"]),
919 ]);
920 assert_eq!(suite, vec![false, false]);
921 }
922
923 #[test]
924 fn a_directory_named_for_tests_that_nobody_declared_is_not_a_marked_module() {
925 let suite = suite_over(&[
929 ("src/lib.rs", &["fn", "run", "(", ")", "{", "}"]),
930 ("src/tests/parser.rs", &["fn", "check", "(", ")", "{", "}"]),
931 ]);
932 assert_eq!(suite, vec![false, false]);
933 }
934
935 #[test]
936 fn a_module_written_where_it_is_declared_claims_no_file() {
937 let suite = suite_over(&[
940 (
941 "src/lib.rs",
942 &[
943 "#", "[", "cfg", "(", "test", ")", "]", "mod", "tests", "{", "fn", "check",
944 "(", ")", "{", "}", "}",
945 ],
946 ),
947 ("src/tests.rs", &["fn", "other", "(", ")", "{", "}"]),
948 ]);
949 assert_eq!(suite, vec![false, false]);
950 }
951
952 #[test]
953 fn a_declaration_is_read_through_its_visibility() {
954 let suite = suite_over(&[
955 (
956 "src/lib.rs",
957 &[
958 "#", "[", "cfg", "(", "test", ")", "]", "pub", "(", "crate", ")", "mod",
959 "tests", ";",
960 ],
961 ),
962 ("src/tests.rs", &["fn", "check", "(", ")", "{", "}"]),
963 ]);
964 assert_eq!(suite, vec![false, true]);
965 }
966
967 #[test]
968 fn a_declaration_the_source_is_truncated_before_marks_nothing() {
969 let suite = suite_over(&[
972 (
973 "src/lib.rs",
974 &["#", "[", "cfg", "(", "test", ")", "]", "mod", "tests"],
975 ),
976 ("src/tests.rs", &["fn", "check", "(", ")", "{", "}"]),
977 ]);
978 assert_eq!(suite, vec![false, false]);
979 }
980
981 #[test]
982 fn only_rust_files_are_read_for_declarations() {
983 let declaration = tokens(&["#", "[", "cfg", "(", "test", ")", "]", "mod", "tests", ";"]);
986 let body = tokens(&["fn", "check", "(", ")", "{", "}"]);
987 let suite = declared_test_modules(&[
988 ModuleFile {
989 path: Path::new("src/lib.rs"),
990 language: Language::Cpp,
991 tokens: &declaration,
992 },
993 ModuleFile {
994 path: Path::new("src/tests.rs"),
995 language: Language::Rust,
996 tokens: &body,
997 },
998 ]);
999 assert_eq!(suite, vec![false, false]);
1000 }
1001}