1#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct ExtractedTest {
5 pub id: String,
6 pub name: String,
7 pub markers: Vec<String>,
8 pub content: String,
9}
10
11#[derive(Debug, thiserror::Error)]
12pub enum ExtractError {
13 #[error("parse error in {path}")]
14 Parse { path: String },
15}
16
17pub fn extract_go_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
21 let mut parser = tree_sitter::Parser::new();
22 parser
23 .set_language(&tree_sitter_go::LANGUAGE.into())
24 .expect("valid go grammar");
25 let tree = parser
26 .parse(source, None)
27 .ok_or_else(|| ExtractError::Parse {
28 path: path.to_string(),
29 })?;
30 let root = tree.root_node();
31 if root.has_error() {
32 return Err(ExtractError::Parse {
33 path: path.to_string(),
34 });
35 }
36
37 let dir = dir_of(path);
38 let mut results = Vec::new();
39
40 let mut cursor = root.walk();
41 let children: Vec<tree_sitter::Node> = root.children(&mut cursor).collect();
42 for (idx, node) in children.iter().enumerate() {
43 if node.kind() != "function_declaration" {
44 continue;
45 }
46 let Some(name_node) = node.child_by_field_name("name") else {
47 continue;
48 };
49 let name = name_node.utf8_text(source.as_bytes()).unwrap_or("");
50 if !name.starts_with("Test") {
51 continue;
52 }
53 let markers = collect_preceding_markers(&children, idx, source, "comment");
54 let content = node.utf8_text(source.as_bytes()).unwrap_or("").to_string();
55 results.push(ExtractedTest {
56 id: format!("{dir}:{name}"),
57 name: name.to_string(),
58 markers,
59 content,
60 });
61 }
62 Ok(results)
63}
64
65pub fn extract_rust_tests(
72 path: &str,
73 source: &str,
74 extra_attributes: &[String],
75) -> Result<Vec<ExtractedTest>, ExtractError> {
76 let mut parser = tree_sitter::Parser::new();
77 parser
78 .set_language(&tree_sitter_rust::LANGUAGE.into())
79 .expect("valid rust grammar");
80 let tree = parser
81 .parse(source, None)
82 .ok_or_else(|| ExtractError::Parse {
83 path: path.to_string(),
84 })?;
85 let root = tree.root_node();
86 if root.has_error() {
87 return Err(ExtractError::Parse {
88 path: path.to_string(),
89 });
90 }
91
92 let base = path.strip_suffix(".rs").unwrap_or(path);
93 let mut results = Vec::new();
94 walk_rust_items(root, source, base, &[], extra_attributes, &mut results);
95 Ok(results)
96}
97
98fn walk_rust_items(
99 container: tree_sitter::Node,
100 source: &str,
101 base: &str,
102 mods: &[String],
103 extra_attributes: &[String],
104 results: &mut Vec<ExtractedTest>,
105) {
106 let mut cursor = container.walk();
107 let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
108 for (idx, node) in children.iter().enumerate() {
109 match node.kind() {
110 "function_item" => {
111 let Some(name_node) = node.child_by_field_name("name") else {
112 continue;
113 };
114 let name = name_node
115 .utf8_text(source.as_bytes())
116 .unwrap_or("")
117 .to_string();
118 let Some(markers) = rust_test_markers(&children, idx, source, extra_attributes)
119 else {
120 continue;
121 };
122 let content = node.utf8_text(source.as_bytes()).unwrap_or("").to_string();
123 let id = if mods.is_empty() {
124 format!("{base}::{name}")
125 } else {
126 format!("{base}:{}:{name}", mods.join("/"))
127 };
128 results.push(ExtractedTest {
129 id,
130 name,
131 markers,
132 content,
133 });
134 }
135 "mod_item" => {
136 let Some(name_node) = node.child_by_field_name("name") else {
137 continue;
138 };
139 let Some(body) = node.child_by_field_name("body") else {
140 continue;
141 };
142 let mod_name = name_node
143 .utf8_text(source.as_bytes())
144 .unwrap_or("")
145 .to_string();
146 let mut new_mods = mods.to_vec();
147 new_mods.push(mod_name);
148 walk_rust_items(body, source, base, &new_mods, extra_attributes, results);
149 }
150 _ => {}
151 }
152 }
153}
154
155fn comment_end_row(node: tree_sitter::Node) -> usize {
159 let end = node.end_position();
160 if end.column == 0 && end.row > node.start_position().row {
161 end.row - 1
162 } else {
163 end.row
164 }
165}
166
167fn is_own_line_comment(parsed: &str, node: tree_sitter::Node) -> bool {
170 let s = node.start_byte();
171 let Some(head) = parsed.get(..s) else {
172 return false;
173 };
174 let ls = head.rfind('\n').map_or(0, |i| i + 1);
175 head[ls..]
176 .chars()
177 .all(|c| c.is_whitespace() || c == '\u{feff}')
178}
179
180fn collect_preceding_markers_with<'a>(
189 children: &[tree_sitter::Node],
190 target_idx: usize,
191 parsed: &str,
192 text_of: &dyn Fn(tree_sitter::Node) -> Option<&'a str>,
193 comment_kinds: &[&str],
194 is_doc: &dyn Fn(tree_sitter::Node) -> bool,
195) -> Vec<String> {
196 let mut next_start_row = children[target_idx].start_position().row;
197 let mut j = target_idx as isize - 1;
198 let mut comments = Vec::new();
199 while j >= 0 {
200 let node = children[j as usize];
201 if !comment_kinds.contains(&node.kind()) {
202 break;
203 }
204 let Some(text) = text_of(node) else {
205 break;
206 };
207 if comment_end_row(node) + 1 != next_start_row || !is_own_line_comment(parsed, node) {
208 break;
209 }
210 if !is_doc(node) {
211 comments.push(text);
212 }
213 next_start_row = node.start_position().row;
214 j -= 1;
215 }
216 comments.reverse();
217 let mut markers = Vec::new();
218 for text in comments {
219 markers.extend(extract_markers(text));
220 }
221 markers
222}
223
224fn collect_preceding_markers(
226 children: &[tree_sitter::Node],
227 target_idx: usize,
228 source: &str,
229 comment_kind: &str,
230) -> Vec<String> {
231 collect_preceding_markers_with(
232 children,
233 target_idx,
234 source,
235 &|n| n.utf8_text(source.as_bytes()).ok(),
236 &[comment_kind],
237 &|_| false,
238 )
239}
240
241fn is_rust_doc_comment(node: tree_sitter::Node) -> bool {
245 let mut cursor = node.walk();
246 let found = node.children(&mut cursor).any(|c| {
247 matches!(
248 c.kind(),
249 "outer_doc_comment_marker" | "inner_doc_comment_marker"
250 )
251 });
252 found
253}
254
255fn rust_test_markers(
262 children: &[tree_sitter::Node],
263 func_idx: usize,
264 source: &str,
265 extra_attributes: &[String],
266) -> Option<Vec<String>> {
267 let mut test_attr_idx: Option<usize> = None;
268 let mut j = func_idx as isize - 1;
269 while j >= 0 {
270 let node = children[j as usize];
271 if node.kind() == "attribute_item" {
272 if rust_is_test_attribute(node, source, extra_attributes) {
273 test_attr_idx = Some(j as usize);
274 }
275 } else if !RUST_COMMENT_KINDS.contains(&node.kind()) {
276 break;
277 }
278 j -= 1;
279 }
280 let test_attr_idx = test_attr_idx?;
281 Some(collect_preceding_markers_with(
282 children,
283 test_attr_idx,
284 source,
285 &|n| n.utf8_text(source.as_bytes()).ok(),
286 &RUST_COMMENT_KINDS,
287 &is_rust_doc_comment,
288 ))
289}
290
291fn rust_is_test_attribute(
298 item: tree_sitter::Node,
299 source: &str,
300 extra_attributes: &[String],
301) -> bool {
302 let mut cursor = item.walk();
303 let Some(attribute) = item
304 .named_children(&mut cursor)
305 .find(|c| c.kind() == "attribute")
306 else {
307 return false;
308 };
309 let Some(path) = attribute.named_child(0) else {
310 return false;
311 };
312 let segment = match path.kind() {
313 "identifier" => path,
314 "scoped_identifier" => match path.child_by_field_name("name") {
315 Some(n) => n,
316 None => return false,
317 },
318 _ => return false,
319 };
320 let text = segment.utf8_text(source.as_bytes()).unwrap_or("");
321 text == "test" || extra_attributes.iter().any(|a| a == text)
322}
323
324const RUST_COMMENT_KINDS: [&str; 2] = ["line_comment", "block_comment"];
325
326fn is_doc_comment(text: &str) -> bool {
329 text.starts_with("/**")
330}
331
332fn is_valid_test_name(name: &str) -> bool {
335 !name.is_empty()
336 && !name
337 .chars()
338 .any(|c| c.is_control() || matches!(c, '\u{85}' | '\u{2028}' | '\u{2029}'))
339}
340
341fn extract_markers(text: &str) -> Vec<String> {
344 let mut markers = Vec::new();
345 let mut rest = text;
346 while let Some(start) = rest.find('[') {
347 let after_open = &rest[start + 1..];
348 let Some(end) = after_open.find(']') else {
349 break;
350 };
351 let inner = &after_open[..end];
352 if !inner.is_empty()
353 && inner
354 .chars()
355 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-')
356 {
357 markers.push(inner.to_string());
358 }
359 rest = &after_open[end + 1..];
360 }
361 markers
362}
363
364fn dir_of(path: &str) -> String {
365 match path.rsplit_once('/') {
366 Some((dir, _)) => dir.to_string(),
367 None => ".".to_string(),
368 }
369}
370
371const CSHARP_TEST_ATTRIBUTES: [&str; 7] = [
372 "Fact",
373 "Theory",
374 "Test",
375 "TestCase",
376 "TestCaseSource",
377 "TestMethod",
378 "DataTestMethod",
379];
380
381pub fn extract_csharp_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
386 let mut parser = tree_sitter::Parser::new();
387 parser
388 .set_language(&tree_sitter_c_sharp::LANGUAGE.into())
389 .expect("valid c# grammar");
390 let tree = parser
391 .parse(source, None)
392 .ok_or_else(|| ExtractError::Parse {
393 path: path.to_string(),
394 })?;
395 let root = tree.root_node();
396 if root.has_error() {
397 return Err(ExtractError::Parse {
398 path: path.to_string(),
399 });
400 }
401 let base = path.strip_suffix(".cs").unwrap_or(path);
402 let mut results = Vec::new();
403 walk_csharp_container(root, source, base, &[], &mut results);
404 Ok(results)
405}
406
407fn walk_csharp_container(
408 container: tree_sitter::Node,
409 source: &str,
410 base: &str,
411 types: &[String],
412 results: &mut Vec<ExtractedTest>,
413) {
414 let mut cursor = container.walk();
415 let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
416 for (idx, node) in children.iter().enumerate() {
417 match node.kind() {
418 "preproc_if" | "preproc_elif" | "preproc_else" => {
419 walk_csharp_container(*node, source, base, types, results);
420 }
421 "namespace_declaration" => {
422 if let Some(body) = node.child_by_field_name("body") {
423 walk_csharp_container(body, source, base, types, results);
424 }
425 }
426 "class_declaration"
427 | "struct_declaration"
428 | "record_declaration"
429 | "interface_declaration" => {
430 let (Some(name_node), Some(body)) = (
431 node.child_by_field_name("name"),
432 node.child_by_field_name("body"),
433 ) else {
434 continue;
435 };
436 let name = name_node.utf8_text(source.as_bytes()).unwrap_or("");
437 let mut new_types = types.to_vec();
438 new_types.push(name.to_string());
439 walk_csharp_container(body, source, base, &new_types, results);
440 }
441 "method_declaration" if !types.is_empty() => {
442 if !csharp_is_test_method(*node, source) {
443 continue;
444 }
445 let Some(name_node) = node.child_by_field_name("name") else {
446 continue;
447 };
448 let name = name_node
449 .utf8_text(source.as_bytes())
450 .unwrap_or("")
451 .to_string();
452 let mut c = node.walk();
453 let Some(first) = node
454 .children(&mut c)
455 .find(|ch| ch.kind() != "attribute_list" && ch.kind() != "comment")
456 else {
457 continue;
458 };
459 let content = source[first.start_byte()..node.end_byte()].to_string();
460 let markers = collect_preceding_markers(&children, idx, source, "comment");
461 results.push(ExtractedTest {
462 id: format!("{base}:{}:{name}", types.join("/")),
463 name,
464 markers,
465 content,
466 });
467 }
468 _ => {}
469 }
470 }
471}
472
473fn csharp_is_test_method(method: tree_sitter::Node, source: &str) -> bool {
474 let mut cursor = method.walk();
475 for list in method.children(&mut cursor) {
476 if list.kind() != "attribute_list" {
477 continue;
478 }
479 let mut c2 = list.walk();
480 for attr in list.children(&mut c2) {
481 if attr.kind() != "attribute" {
482 continue;
483 }
484 let Some(mut name) = attr.child_by_field_name("name") else {
485 continue;
486 };
487 while name.kind() == "qualified_name" {
488 match name.child_by_field_name("name") {
489 Some(n) => name = n,
490 None => break,
491 }
492 }
493 let text = name.utf8_text(source.as_bytes()).unwrap_or("");
494 if CSHARP_TEST_ATTRIBUTES.contains(&text) {
495 return true;
496 }
497 }
498 }
499 false
500}
501
502const TYPESCRIPT_TEST_MODIFIERS: [&str; 6] =
503 ["skip", "only", "todo", "fails", "concurrent", "sequential"];
504const TYPESCRIPT_DESCRIBE_MODIFIERS: [&str; 6] = [
505 "skip",
506 "only",
507 "todo",
508 "concurrent",
509 "sequential",
510 "shuffle",
511];
512
513pub const TYPESCRIPT_TEST_EXTENSIONS: [&str; 8] =
516 ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
517
518pub fn is_typescript_test_file(path: &str) -> bool {
521 TYPESCRIPT_TEST_EXTENSIONS.iter().any(|ext| {
522 path.ends_with(&format!(".test.{ext}")) || path.ends_with(&format!(".spec.{ext}"))
523 })
524}
525
526pub fn extract_typescript_tests(
532 path: &str,
533 source: &str,
534) -> Result<Vec<ExtractedTest>, ExtractError> {
535 let mut parser = tree_sitter::Parser::new();
536 let lang: tree_sitter::Language = if [".ts", ".mts", ".cts"]
537 .iter()
538 .any(|ext| path.ends_with(ext))
539 {
540 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
541 } else {
542 tree_sitter_typescript::LANGUAGE_TSX.into()
543 };
544 parser
545 .set_language(&lang)
546 .expect("valid typescript grammar");
547 let tree = parser
548 .parse(source, None)
549 .ok_or_else(|| ExtractError::Parse {
550 path: path.to_string(),
551 })?;
552 let root = tree.root_node();
553 if root.has_error() {
554 return Err(ExtractError::Parse {
555 path: path.to_string(),
556 });
557 }
558 let base = TYPESCRIPT_TEST_EXTENSIONS
559 .iter()
560 .find_map(|ext| path.strip_suffix(&format!(".{ext}")))
561 .unwrap_or(path);
562 let mut results = Vec::new();
563 walk_typescript_statements(root, source, base, &[], &mut results);
564 Ok(results)
565}
566
567fn typescript_callee_chain<'a>(
570 callee: tree_sitter::Node,
571 source: &'a str,
572) -> Option<(&'a str, Vec<&'a str>)> {
573 let mut props = Vec::new();
574 let mut cur = callee;
575 loop {
576 match cur.kind() {
577 "identifier" => {
578 props.reverse();
579 return Some((cur.utf8_text(source.as_bytes()).ok()?, props));
580 }
581 "member_expression" => {
582 let mut c = cur.walk();
583 if cur.children(&mut c).any(|ch| ch.kind() == "optional_chain") {
584 return None;
585 }
586 let prop = cur.child_by_field_name("property")?;
587 if prop.kind() != "property_identifier" {
588 return None;
589 }
590 props.push(prop.utf8_text(source.as_bytes()).ok()?);
591 cur = cur.child_by_field_name("object")?;
592 }
593 _ => return None,
594 }
595 }
596}
597
598fn typescript_name_arg<'a>(args: tree_sitter::Node, source: &'a str) -> Option<&'a str> {
601 let first = args.named_child(0)?;
602 match first.kind() {
603 "string" => {}
604 "template_string" => {
605 let mut c = first.walk();
606 if first
607 .children(&mut c)
608 .any(|ch| ch.kind() == "template_substitution")
609 {
610 return None;
611 }
612 }
613 _ => return None,
614 }
615 let text = first.utf8_text(source.as_bytes()).ok()?;
616 if text.len() < 2 {
617 return None;
618 }
619 let name = &text[1..text.len() - 1];
620 if !is_valid_test_name(name) {
621 return None;
622 }
623 Some(name)
624}
625
626fn walk_typescript_statements(
627 container: tree_sitter::Node,
628 source: &str,
629 base: &str,
630 describes: &[String],
631 results: &mut Vec<ExtractedTest>,
632) {
633 let mut cursor = container.walk();
634 let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
635 for (idx, node) in children.iter().enumerate() {
636 if node.kind() != "expression_statement" {
637 continue;
638 }
639 let Some(call) = node.named_child(0) else {
640 continue;
641 };
642 if call.kind() != "call_expression" {
643 continue;
644 }
645 let (Some(callee), Some(args)) = (
646 call.child_by_field_name("function"),
647 call.child_by_field_name("arguments"),
648 ) else {
649 continue;
650 };
651 let Some((head, props)) = typescript_callee_chain(callee, source) else {
652 continue;
653 };
654 let Some(name) = typescript_name_arg(args, source) else {
655 continue;
656 };
657 if (head == "test" || head == "it")
658 && props.iter().all(|p| TYPESCRIPT_TEST_MODIFIERS.contains(p))
659 {
660 let markers = collect_preceding_markers(&children, idx, source, "comment");
661 let content = node.utf8_text(source.as_bytes()).unwrap_or("").to_string();
662 results.push(ExtractedTest {
663 id: format!("{base}:{}:{name}", describes.join("/")),
664 name: name.to_string(),
665 markers,
666 content,
667 });
668 } else if head == "describe"
669 && props
670 .iter()
671 .all(|p| TYPESCRIPT_DESCRIBE_MODIFIERS.contains(p))
672 {
673 let Some(cb) = args.named_child(1) else {
674 continue;
675 };
676 if cb.kind() != "arrow_function" && cb.kind() != "function_expression" {
677 continue;
678 }
679 let Some(body) = cb.child_by_field_name("body") else {
680 continue;
681 };
682 if body.kind() != "statement_block" {
683 continue;
684 }
685 let mut new_desc = describes.to_vec();
686 new_desc.push(name.to_string());
687 walk_typescript_statements(body, source, base, &new_desc, results);
688 }
689 }
690}
691
692const JVM_TEST_ANNOTATIONS: [&str; 5] = [
697 "Test",
698 "ParameterizedTest",
699 "RepeatedTest",
700 "TestFactory",
701 "TestTemplate",
702];
703
704const JVM_COMMENT_KINDS: [&str; 2] = ["line_comment", "block_comment"];
705
706fn jvm_content_start(decl: tree_sitter::Node) -> Option<usize> {
710 fn skip(kind: &str) -> bool {
711 matches!(
712 kind,
713 "annotation" | "marker_annotation" | "line_comment" | "block_comment"
714 )
715 }
716 let mut cursor = decl.walk();
717 for child in decl.children(&mut cursor) {
718 if child.kind() == "modifiers" {
719 let mut c2 = child.walk();
720 let first = child.children(&mut c2).find(|n| !skip(n.kind()));
721 if let Some(first) = first {
722 return Some(first.start_byte());
723 }
724 } else if !skip(child.kind()) {
725 return Some(child.start_byte());
726 }
727 }
728 None
729}
730
731fn jvm_name_is_test(name: &str) -> bool {
732 JVM_TEST_ANNOTATIONS.contains(&name)
733}
734
735pub fn extract_java_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
742 let mut parser = tree_sitter::Parser::new();
743 parser
744 .set_language(&tree_sitter_java::LANGUAGE.into())
745 .expect("valid java grammar");
746 let tree = parser
747 .parse(source, None)
748 .ok_or_else(|| ExtractError::Parse {
749 path: path.to_string(),
750 })?;
751 let root = tree.root_node();
752 if root.has_error() {
753 return Err(ExtractError::Parse {
754 path: path.to_string(),
755 });
756 }
757 let base = path.strip_suffix(".java").unwrap_or(path);
758 let mut results = Vec::new();
759 walk_java_container(root, source, base, &[], &mut results);
760 Ok(results)
761}
762
763fn walk_java_container(
764 container: tree_sitter::Node,
765 source: &str,
766 base: &str,
767 types: &[String],
768 results: &mut Vec<ExtractedTest>,
769) {
770 let mut cursor = container.walk();
771 let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
772 for (idx, node) in children.iter().enumerate() {
773 match node.kind() {
774 "class_declaration"
775 | "record_declaration"
776 | "interface_declaration"
777 | "enum_declaration" => {
778 let (Some(name_node), Some(body)) = (
779 node.child_by_field_name("name"),
780 node.child_by_field_name("body"),
781 ) else {
782 continue;
783 };
784 let name = name_node.utf8_text(source.as_bytes()).unwrap_or("");
785 let mut new_types = types.to_vec();
786 new_types.push(name.to_string());
787 if body.kind() == "enum_body" {
788 let mut c = body.walk();
789 let decls: Vec<_> = body
790 .children(&mut c)
791 .filter(|n| n.kind() == "enum_body_declarations")
792 .collect();
793 for d in decls {
794 walk_java_container(d, source, base, &new_types, results);
795 }
796 } else {
797 walk_java_container(body, source, base, &new_types, results);
798 }
799 }
800 "method_declaration" if !types.is_empty() => {
801 if !java_is_test_method(*node, source) {
802 continue;
803 }
804 let Some(name_node) = node.child_by_field_name("name") else {
805 continue;
806 };
807 let Some(start) = jvm_content_start(*node) else {
808 continue;
809 };
810 let name = name_node
811 .utf8_text(source.as_bytes())
812 .unwrap_or("")
813 .to_string();
814 results.push(ExtractedTest {
815 id: format!("{base}:{}:{name}", types.join("/")),
816 name,
817 markers: collect_preceding_markers_with(
818 &children,
819 idx,
820 source,
821 &|n| n.utf8_text(source.as_bytes()).ok(),
822 &JVM_COMMENT_KINDS,
823 &|n| n.utf8_text(source.as_bytes()).is_ok_and(is_doc_comment),
824 ),
825 content: source[start..node.end_byte()].to_string(),
826 });
827 }
828 _ => {}
829 }
830 }
831}
832
833fn java_is_test_method(method: tree_sitter::Node, source: &str) -> bool {
834 let mut cursor = method.walk();
835 let Some(modifiers) = method
836 .children(&mut cursor)
837 .find(|n| n.kind() == "modifiers")
838 else {
839 return false;
840 };
841 let mut c2 = modifiers.walk();
842 for ann in modifiers.children(&mut c2) {
843 if ann.kind() != "marker_annotation" && ann.kind() != "annotation" {
844 continue;
845 }
846 let Some(mut name) = ann.child_by_field_name("name") else {
847 continue;
848 };
849 while name.kind() == "scoped_identifier" {
850 match name.child_by_field_name("name") {
851 Some(n) => name = n,
852 None => break,
853 }
854 }
855 if jvm_name_is_test(name.utf8_text(source.as_bytes()).unwrap_or("")) {
856 return true;
857 }
858 }
859 false
860}
861
862struct KotlinShadow<'a> {
871 original: &'a str,
872 shadow: String,
873 map: Vec<(usize, usize)>,
874}
875
876impl<'a> KotlinShadow<'a> {
877 fn new(original: &'a str) -> Self {
878 let mut shadow = String::with_capacity(original.len());
879 let mut map = vec![(0usize, 0usize)];
880 for (off, ch) in original.char_indices() {
881 if ch.is_ascii() {
882 shadow.push(ch);
883 } else {
884 shadow.push(if ch.is_whitespace() || ch == '\u{feff}' {
885 ' '
886 } else {
887 'x'
888 });
889 map.push((shadow.len(), off + ch.len_utf8()));
890 }
891 }
892 KotlinShadow {
893 original,
894 shadow,
895 map,
896 }
897 }
898
899 fn blank_ranges(&mut self, ranges: &[(usize, usize)]) {
901 let mut bytes = std::mem::take(&mut self.shadow).into_bytes();
902 for &(a, b) in ranges {
903 for byte in &mut bytes[a..b] {
904 if *byte != b'\n' {
905 *byte = b' ';
906 }
907 }
908 }
909 self.shadow = String::from_utf8(bytes).expect("ascii shadow");
910 }
911
912 fn to_original(&self, shadow_offset: usize) -> usize {
913 let i = self.map.partition_point(|&(s, _)| s <= shadow_offset) - 1;
914 let (s, o) = self.map[i];
915 o + (shadow_offset - s)
916 }
917
918 fn slice(&self, start: usize, end: usize) -> Option<&'a str> {
919 self.original
920 .get(self.to_original(start)..self.to_original(end))
921 }
922
923 fn node_text(&self, node: tree_sitter::Node) -> Option<&'a str> {
924 self.slice(node.start_byte(), node.end_byte())
925 }
926}
927
928pub fn extract_kotlin_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
933 kotlin_extract(path, source, true)
934}
935
936const KOTLIN_ROOT_KINDS: [&str; 11] = [
940 "shebang",
941 "file_annotation",
942 "package_header",
943 "import",
944 "class_declaration",
945 "object_declaration",
946 "function_declaration",
947 "property_declaration",
948 "type_alias",
949 "line_comment",
950 "block_comment",
951];
952
953fn kotlin_extract(
956 path: &str,
957 source: &str,
958 retry_annotated_expression: bool,
959) -> Result<Vec<ExtractedTest>, ExtractError> {
960 let parse_err = || ExtractError::Parse {
961 path: path.to_string(),
962 };
963 let mut shadow = KotlinShadow::new(source);
964 let mut parser = tree_sitter::Parser::new();
965 parser
966 .set_language(&tree_sitter_kotlin_ng::LANGUAGE.into())
967 .expect("valid kotlin grammar");
968 let mut tree = parser.parse(&shadow.shadow, None).ok_or_else(parse_err)?;
969 if tree.root_node().has_error() {
970 return Err(parse_err());
971 }
972 if retry_annotated_expression && kotlin_has_annotated_expression(tree.root_node()) {
977 let ranges = kotlin_annotated_expression_annotations(tree.root_node());
978 drop(tree);
979 shadow.blank_ranges(&ranges);
980 tree = parser.parse(&shadow.shadow, None).ok_or_else(parse_err)?;
981 if tree.root_node().has_error() {
982 return Err(parse_err());
983 }
984 }
985 let root = tree.root_node();
986 let mut cursor = root.walk();
989 let unexpected = root
990 .children(&mut cursor)
991 .any(|n| !KOTLIN_ROOT_KINDS.contains(&n.kind()));
992 if unexpected {
993 return Err(parse_err());
994 }
995 let base = path.strip_suffix(".kt").unwrap_or(path);
996 let mut results = Vec::new();
997 walk_kotlin_container(root, &shadow, base, &[], &mut results).map_err(|()| parse_err())?;
998 Ok(results)
999}
1000
1001fn kotlin_has_annotated_expression(root: tree_sitter::Node) -> bool {
1002 let mut cursor = root.walk();
1003 let found = root
1004 .children(&mut cursor)
1005 .any(|n| n.kind() == "annotated_expression");
1006 found
1007}
1008
1009fn kotlin_annotated_expression_annotations(root: tree_sitter::Node) -> Vec<(usize, usize)> {
1010 let mut ranges = Vec::new();
1011 let mut cursor = root.walk();
1012 for expr in root.children(&mut cursor) {
1013 if expr.kind() != "annotated_expression" {
1014 continue;
1015 }
1016 let mut c2 = expr.walk();
1017 for child in expr.children(&mut c2) {
1018 if child.kind() == "annotation" {
1019 ranges.push((child.start_byte(), child.end_byte()));
1020 }
1021 }
1022 }
1023 ranges
1024}
1025
1026fn kotlin_ident(text: &str) -> String {
1027 text.strip_prefix('`')
1028 .and_then(|t| t.strip_suffix('`'))
1029 .unwrap_or(text)
1030 .to_string()
1031}
1032
1033fn walk_kotlin_container(
1034 container: tree_sitter::Node,
1035 shadow: &KotlinShadow,
1036 base: &str,
1037 types: &[String],
1038 results: &mut Vec<ExtractedTest>,
1039) -> Result<(), ()> {
1040 let mut cursor = container.walk();
1041 let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
1042 for (idx, node) in children.iter().enumerate() {
1043 match node.kind() {
1044 "class_declaration" | "object_declaration" | "companion_object" => {
1045 let name = match node.child_by_field_name("name") {
1046 Some(n) => match shadow.node_text(n) {
1047 Some(t) => kotlin_ident(t),
1048 None => {
1049 debug_assert!(false, "kotlin name range off a char boundary");
1050 return Err(());
1051 }
1052 },
1053 None if node.kind() == "companion_object" => "Companion".to_string(),
1054 None => continue,
1055 };
1056 let mut c = node.walk();
1057 let Some(body) = node
1058 .children(&mut c)
1059 .find(|n| matches!(n.kind(), "class_body" | "enum_class_body"))
1060 else {
1061 continue;
1062 };
1063 let mut new_types = types.to_vec();
1064 new_types.push(name);
1065 walk_kotlin_container(body, shadow, base, &new_types, results)?;
1066 }
1067 "function_declaration" => {
1068 if !kotlin_is_test_function(*node, shadow) {
1069 continue;
1070 }
1071 let Some(name_node) = node.child_by_field_name("name") else {
1072 continue;
1073 };
1074 let Some(name_text) = shadow.node_text(name_node) else {
1075 debug_assert!(false, "kotlin name range off a char boundary");
1076 return Err(());
1077 };
1078 let name = kotlin_ident(name_text);
1079 if !is_valid_test_name(&name) {
1080 continue;
1081 }
1082 let Some(start) = jvm_content_start(*node) else {
1083 continue;
1084 };
1085 let Some(content) = shadow.slice(start, node.end_byte()) else {
1086 debug_assert!(false, "kotlin content range off a char boundary");
1087 return Err(());
1088 };
1089 results.push(ExtractedTest {
1090 id: format!("{base}:{}:{name}", types.join("/")),
1091 name,
1092 markers: collect_preceding_markers_with(
1095 &children,
1096 idx,
1097 &shadow.shadow,
1098 &|n| shadow.node_text(n),
1099 &JVM_COMMENT_KINDS,
1100 &|n| shadow.node_text(n).is_some_and(is_doc_comment),
1101 ),
1102 content: content.to_string(),
1103 });
1104 }
1105 _ => {}
1106 }
1107 }
1108 Ok(())
1109}
1110
1111fn kotlin_is_test_function(func: tree_sitter::Node, shadow: &KotlinShadow) -> bool {
1112 let mut cursor = func.walk();
1113 let Some(modifiers) = func.children(&mut cursor).find(|n| n.kind() == "modifiers") else {
1114 return false;
1115 };
1116 let mut c2 = modifiers.walk();
1117 let found = modifiers
1118 .children(&mut c2)
1119 .filter(|n| n.kind() == "annotation")
1120 .any(|ann| kotlin_annotation_has_test(ann, shadow));
1121 found
1122}
1123
1124fn kotlin_annotation_has_test(node: tree_sitter::Node, shadow: &KotlinShadow) -> bool {
1127 let mut cursor = node.walk();
1128 for child in node.children(&mut cursor) {
1129 match child.kind() {
1130 "user_type" => {
1131 let mut c = child.walk();
1132 let last = child
1133 .children(&mut c)
1134 .filter(|n| matches!(n.kind(), "identifier" | "type_identifier"))
1135 .last();
1136 if let Some(last) = last {
1137 if jvm_name_is_test(shadow.node_text(last).unwrap_or("")) {
1138 return true;
1139 }
1140 }
1141 }
1142 "value_arguments" | "use_site_target" => {}
1143 _ => {
1144 if kotlin_annotation_has_test(child, shadow) {
1145 return true;
1146 }
1147 }
1148 }
1149 }
1150 false
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155 use super::*;
1156
1157 fn find<'a>(tests: &'a [ExtractedTest], name: &str) -> &'a ExtractedTest {
1158 tests
1159 .iter()
1160 .find(|t| t.name == name)
1161 .unwrap_or_else(|| panic!("test named {name} not found in {tests:?}"))
1162 }
1163
1164 #[test]
1166 fn goは直前に隣接するコメントをマーカーとして抽出する() {
1167 let source = r#"package auth
1168
1169// [FR-001]
1170func TestRejectsShortPassword(t *testing.T) {
1171 doSomething()
1172}
1173"#;
1174 let tests = extract_go_tests("internal/auth/login_test.go", source).unwrap();
1175 let t = find(&tests, "TestRejectsShortPassword");
1176 assert_eq!(t.markers, vec!["FR-001".to_string()]);
1177 }
1178
1179 #[test]
1181 fn goは空行を挟んだコメントや欠落をマーカーなしとして無視する() {
1182 let source = r#"package auth
1183
1184// [FR-002]
1185
1186func TestWithBlankLine(t *testing.T) {
1187}
1188
1189func TestNoComment(t *testing.T) {
1190}
1191"#;
1192 let tests = extract_go_tests("internal/auth/login_test.go", source).unwrap();
1193 assert!(find(&tests, "TestWithBlankLine").markers.is_empty());
1194 assert!(find(&tests, "TestNoComment").markers.is_empty());
1195 }
1196
1197 #[test]
1199 fn goのtest識別子はディレクトリと関数名を組み合わせた形式になる() {
1200 let source = "package auth\n\n// [FR-001]\nfunc TestLogin(t *testing.T) {}\n";
1201 let tests = extract_go_tests("internal/auth/login_test.go", source).unwrap();
1202 assert_eq!(find(&tests, "TestLogin").id, "internal/auth:TestLogin");
1203 }
1204
1205 #[test]
1207 fn goのtest識別子はリポジトリ直下ならドットになる() {
1208 let source = "package main\n\n// [FR-001]\nfunc TestRoot(t *testing.T) {}\n";
1209 let tests = extract_go_tests("login_test.go", source).unwrap();
1210 assert_eq!(find(&tests, "TestRoot").id, ".:TestRoot");
1211 }
1212
1213 #[test]
1215 fn rustの関数本体の変更はcontentに反映される() {
1216 let base = r#"// [FR-001]
1217#[test]
1218fn rejects_short_password() {
1219 assert!(true);
1220}
1221"#;
1222 let changed = r#"// [FR-001]
1223#[test]
1224fn rejects_short_password() {
1225 assert!(false);
1226}
1227"#;
1228 let a = find(
1229 &extract_rust_tests("src/auth.rs", base, &[]).unwrap(),
1230 "rejects_short_password",
1231 )
1232 .content
1233 .clone();
1234 let b = find(
1235 &extract_rust_tests("src/auth.rs", changed, &[]).unwrap(),
1236 "rejects_short_password",
1237 )
1238 .content
1239 .clone();
1240 assert_ne!(a, b);
1241 assert!(a.contains("fn rejects_short_password"));
1242 assert!(a.contains("assert!(true)"));
1243 }
1244
1245 #[test]
1247 fn goの関数本体の変更はcontentに反映される() {
1248 let base = "package auth\n\n// [FR-001]\nfunc TestRejectsShortPassword(t *testing.T) {\n\tassertTrue(t, true)\n}\n";
1249 let changed = "package auth\n\n// [FR-001]\nfunc TestRejectsShortPassword(t *testing.T) {\n\tassertTrue(t, false)\n}\n";
1250 let a = find(
1251 &extract_go_tests("internal/auth/login_test.go", base).unwrap(),
1252 "TestRejectsShortPassword",
1253 )
1254 .content
1255 .clone();
1256 let b = find(
1257 &extract_go_tests("internal/auth/login_test.go", changed).unwrap(),
1258 "TestRejectsShortPassword",
1259 )
1260 .content
1261 .clone();
1262 assert_ne!(a, b);
1263 assert!(a.contains("func TestRejectsShortPassword"));
1264 assert!(a.contains("assertTrue(t, true)"));
1265 }
1266
1267 #[test]
1269 fn rustのcontentは外側の属性とマーカーコメントを含まない() {
1270 let source = r#"// [FR-001]
1271#[test]
1272fn rejects_short_password() {
1273 assert!(true);
1274}
1275"#;
1276 let extracted = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1277 let t = find(&extracted, "rejects_short_password");
1278 assert!(!t.content.contains("#[test]"));
1279 assert!(!t.content.contains("[FR-001]"));
1280 assert!(t.content.contains("fn rejects_short_password"));
1281 }
1282
1283 fn rs(source: &str, extra: &[&str]) -> Vec<ExtractedTest> {
1284 let extra: Vec<String> = extra.iter().map(|s| s.to_string()).collect();
1285 extract_rust_tests("src/auth.rs", source, &extra).unwrap()
1286 }
1287
1288 fn has(tests: &[ExtractedTest], name: &str) -> bool {
1289 tests.iter().any(|t| t.name == name)
1290 }
1291
1292 #[test]
1294 fn rustは属性パスの末尾がtestならtestとして抽出する() {
1295 let source = r#"// [FR-001]
1296#[test]
1297fn plain_test() {}
1298
1299// [FR-002]
1300#[tokio::test]
1301async fn async_test() {}
1302
1303// [FR-003]
1304#[async_std::test]
1305async fn async_std_test() {}
1306
1307// [FR-004]
1308#[tokio::test(flavor = "multi_thread")]
1309async fn multi_thread_test() {}
1310
1311// [FR-005]
1312#[ test ]
1313fn spaced_test() {}
1314"#;
1315 let tests = rs(source, &[]);
1316 for name in [
1317 "plain_test",
1318 "async_test",
1319 "async_std_test",
1320 "multi_thread_test",
1321 "spaced_test",
1322 ] {
1323 assert!(has(&tests, name), "{name} should be extracted");
1324 }
1325 assert_eq!(
1326 find(&tests, "async_test").markers,
1327 vec!["FR-002".to_string()]
1328 );
1329 }
1330
1331 #[test]
1333 fn rustのrstestは追加指定がなければ対象外で指定があれば対象になる() {
1334 let source = "// [FR-001]\n#[rstest]\nfn t() {}\n";
1335 assert!(!has(&rs(source, &[]), "t"));
1336 assert!(has(&rs(source, &["rstest"]), "t"));
1337
1338 let scoped = "// [FR-001]\n#[rstest::rstest]\nfn t() {}\n";
1339 assert!(!has(&rs(scoped, &[]), "t"));
1340 assert!(has(&rs(scoped, &["rstest"]), "t"));
1341 }
1342
1343 #[test]
1345 fn rustのtest_caseとcfg_attrのtestは対象外() {
1346 let source = r#"// [FR-001]
1347#[test_case(1)]
1348fn case_test() {}
1349
1350// [FR-002]
1351#[cfg_attr(x, test)]
1352fn cfg_attr_test() {}
1353"#;
1354 let tests = rs(source, &[]);
1355 assert!(!has(&tests, "case_test"));
1356 assert!(!has(&tests, "cfg_attr_test"));
1357
1358 let tests = rs(source, &["test_case"]);
1359 assert!(has(&tests, "case_test"));
1360 assert!(!has(&tests, "cfg_attr_test"));
1361 }
1362
1363 #[test]
1365 fn rustのinner_attributeのtestはtestを作らない() {
1366 let source = "#![test]\n\n// [FR-001]\nfn helper() {}\n";
1367 assert!(rs(source, &[]).is_empty());
1368 }
1369
1370 #[test]
1372 fn rustは最初の対象属性の直前のコメントをマーカーにする() {
1373 let source = "// [FR-001]\n#[rstest]\n#[case(1)]\n#[tokio::test]\nfn t() {}\n";
1374 let tests = rs(source, &["rstest"]);
1375 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
1376 }
1377
1378 #[test]
1380 fn rustは対象属性の設定によりマーカー位置が変わる() {
1381 let source = "#[rstest]\n// [FR-001]\n#[tokio::test]\nfn t() {}\n";
1382 let without = rs(source, &[]);
1383 assert_eq!(find(&without, "t").markers, vec!["FR-001".to_string()]);
1384 let with = rs(source, &["rstest"]);
1385 assert!(find(&with, "t").markers.is_empty());
1386 }
1387
1388 #[test]
1390 fn rustは属性間や属性末尾のコメントがあってもtestとして検出しマーカーを取る() {
1391 let between = "// [FR-001]\n#[test]\n// note\n#[ignore]\nfn between() {}\n";
1392 let tests = rs(between, &[]);
1393 assert_eq!(find(&tests, "between").markers, vec!["FR-001".to_string()]);
1394
1395 let trailing = "// [FR-001]\n#[test] // trailing\n#[ignore]\nfn trailing() {}\n";
1396 let tests = rs(trailing, &[]);
1397 assert_eq!(find(&tests, "trailing").markers, vec!["FR-001".to_string()]);
1398
1399 let block = "// [FR-001]\n#[test]\n/* block */\n#[ignore]\nfn block() {}\n";
1400 assert_eq!(
1401 find(&rs(block, &[]), "block").markers,
1402 vec!["FR-001".to_string()]
1403 );
1404 }
1405
1406 #[test]
1408 fn tokioのtest属性はcontentに含まれない() {
1409 let source = "// [FR-001]\n#[tokio::test]\nasync fn t() { assert!(true); }\n";
1410 let tests = rs(source, &[]);
1411 let t = find(&tests, "t");
1412 assert!(!t.content.contains("tokio::test"));
1413 assert!(!t.content.contains("#["));
1414 assert!(t.content.contains("async fn t"));
1415 }
1416
1417 #[test]
1419 fn rustはtest属性の直前に隣接するコメントをマーカーとする() {
1420 let source = r#"// [FR-001]
1421#[test]
1422fn adjacent() {}
1423
1424// [FR-002]
1425
1426#[test]
1427fn blank_line_between() {}
1428
1429#[test]
1430fn no_comment() {}
1431"#;
1432 let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1433 assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
1434 assert!(find(&tests, "blank_line_between").markers.is_empty());
1435 assert!(find(&tests, "no_comment").markers.is_empty());
1436 }
1437
1438 #[test]
1440 fn rustのtest識別子はトップレベルなら空モジュールパスになる() {
1441 let source = "// [FR-001]\n#[test]\nfn roundtrip() {}\n";
1442 let tests = extract_rust_tests("src/lock.rs", source, &[]).unwrap();
1443 assert_eq!(find(&tests, "roundtrip").id, "src/lock::roundtrip");
1444 }
1445
1446 #[test]
1448 fn rustのtest識別子はinline_modを外側から内側の順に連結する() {
1449 let source = r#"mod tests {
1450 // [FR-001]
1451 #[test]
1452 fn roundtrip() {}
1453}
1454"#;
1455 let tests = extract_rust_tests("src/lock.rs", source, &[]).unwrap();
1456 assert_eq!(find(&tests, "roundtrip").id, "src/lock:tests:roundtrip");
1457 }
1458
1459 #[test]
1461 fn rustのtest識別子はネストしたinline_modをスラッシュで連結する() {
1462 let source = r#"mod outer {
1463 mod inner {
1464 // [FR-001]
1465 #[test]
1466 fn t() {}
1467 }
1468}
1469"#;
1470 let tests = extract_rust_tests("src/x.rs", source, &[]).unwrap();
1471 assert_eq!(find(&tests, "t").id, "src/x:outer/inner:t");
1472 }
1473
1474 #[test]
1476 fn rustは属性が間に挟まるとtestの直前のコメントをマーカーにしない() {
1477 let source = r#"// [FR-001]
1478#[cfg(feature = "x")]
1479#[test]
1480fn not_adjacent() {}
1481"#;
1482 let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1483 assert!(find(&tests, "not_adjacent").markers.is_empty());
1484 }
1485
1486 #[test]
1488 fn rustは他の属性の後ろでもtestの直前のコメントはマーカーになる() {
1489 let source = r#"#[cfg(feature = "x")]
1490// [FR-001]
1491#[test]
1492fn adjacent_after_other_attr() {}
1493"#;
1494 let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1495 assert_eq!(
1496 find(&tests, "adjacent_after_other_attr").markers,
1497 vec!["FR-001".to_string()]
1498 );
1499 }
1500
1501 #[test]
1503 fn rustはtestとfnの間にコメントがあってもtestとして検出しマーカーを採用する() {
1504 let source = r#"// [FR-001]
1505#[test]
1506// 補足
1507fn t() {}
1508"#;
1509 let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1510 let found = find(&tests, "t");
1511 assert_eq!(found.markers, vec!["FR-001".to_string()]);
1512 assert!(!found.content.contains("補足"));
1513 }
1514
1515 fn cs(source: &str) -> Vec<ExtractedTest> {
1518 extract_csharp_tests("tests/AuthTests.cs", source).unwrap()
1519 }
1520
1521 fn cs_wrap(method: &str) -> String {
1522 format!("public class AuthTests\n{{\n{method}\n}}\n")
1523 }
1524
1525 #[test]
1527 fn csharpはif_endif内のtestメソッドも検出する() {
1528 let source = "public class A {\n#if DEBUG\n // [FR-001]\n [Fact]\n public void T() { }\n#endif\n}\n";
1529 let tests = extract_csharp_tests("tests/A.cs", source).unwrap();
1530 assert_eq!(tests.len(), 1);
1531 assert_eq!(tests[0].id, "tests/A:A:T");
1532 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
1533 }
1534
1535 #[test]
1537 fn csharpは7種類の属性をそれぞれtestとして検出する() {
1538 for attr in [
1539 "Fact",
1540 "Theory",
1541 "Test",
1542 "TestCase",
1543 "TestCaseSource",
1544 "TestMethod",
1545 "DataTestMethod",
1546 ] {
1547 let source = cs_wrap(&format!(" [{attr}]\n public void T() {{ }}"));
1548 let tests = cs(&source);
1549 assert_eq!(tests.len(), 1, "attribute {attr} should be detected");
1550 assert_eq!(tests[0].name, "T");
1551 }
1552 }
1553
1554 #[test]
1556 fn csharpはattribute接尾辞付きや無関係な属性のみのメソッドを検出しない() {
1557 let source = cs_wrap(
1558 " [FactAttribute]\n public void A() { }\n\n [Obsolete]\n public void B() { }\n\n public void C() { }\n\n [Category(\"x\")]\n public void D() { }",
1559 );
1560 assert!(cs(&source).is_empty());
1561 }
1562
1563 #[test]
1565 fn csharpは引数付きの属性も検出する() {
1566 let source = cs_wrap(" [Fact(Skip = \"x\")]\n public void T() { }");
1567 assert_eq!(cs(&source).len(), 1);
1568 }
1569
1570 #[test]
1572 fn csharpは名前空間修飾された属性も検出する() {
1573 let source = cs_wrap(
1574 " [Xunit.Fact]\n public void A() { }\n\n [global::Xunit.Fact]\n public void B() { }",
1575 );
1576 let tests = cs(&source);
1577 assert_eq!(tests.len(), 2);
1578 find(&tests, "A");
1579 find(&tests, "B");
1580 }
1581
1582 #[test]
1584 fn csharpはtarget指定付きの属性も検出する() {
1585 let source = cs_wrap(" [method: Fact]\n public void T() { }");
1586 assert_eq!(cs(&source).len(), 1);
1587 }
1588
1589 #[test]
1591 fn csharpは同一属性リスト内の他の属性と並んだ属性も検出する() {
1592 let source = cs_wrap(" [Trait(\"a\", \"b\"), Fact]\n public void T() { }");
1593 assert_eq!(cs(&source).len(), 1);
1594 }
1595
1596 #[test]
1598 fn csharpは複数の属性リストの順序に関わらず検出する() {
1599 let source = cs_wrap(
1600 " [Theory]\n [InlineData(1)]\n public void A(int x) { }\n\n [InlineData(1)]\n [Theory]\n public void B(int x) { }",
1601 );
1602 let tests = cs(&source);
1603 assert_eq!(tests.len(), 2);
1604 find(&tests, "A");
1605 find(&tests, "B");
1606 }
1607
1608 #[test]
1610 fn csharpは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
1611 let source = cs_wrap(
1612 " // [FR-001]\n [Fact]\n public void Adjacent() { }\n\n // [FR-002]\n\n [Fact]\n public void Blank() { }\n\n [Fact]\n public void None() { }",
1613 );
1614 let tests = cs(&source);
1615 assert_eq!(find(&tests, "Adjacent").markers, vec!["FR-001".to_string()]);
1616 assert!(find(&tests, "Blank").markers.is_empty());
1617 assert!(find(&tests, "None").markers.is_empty());
1618 }
1619
1620 #[test]
1622 fn csharpはドキュメントコメントとブロックコメントもマーカーにできる() {
1623 let source = cs_wrap(
1624 " /// [FR-001]\n [Fact]\n public void Doc() { }\n\n /* [FR-002] */\n [Fact]\n public void Block() { }",
1625 );
1626 let tests = cs(&source);
1627 assert_eq!(find(&tests, "Doc").markers, vec!["FR-001".to_string()]);
1628 assert_eq!(find(&tests, "Block").markers, vec!["FR-002".to_string()]);
1629 }
1630
1631 #[test]
1633 fn csharpは属性と署名の間のコメントをマーカーにせずcontentにも含めない() {
1634 let source = cs_wrap(
1635 " [Fact]\n // [FR-001]\n public void T() { }\n\n [Fact]\n [Trait(\"a\", \"b\")]\n // 補足\n public void U() { }",
1636 );
1637 let tests = cs(&source);
1638 let t = find(&tests, "T");
1639 assert!(t.markers.is_empty());
1640 assert!(!t.content.contains("FR-001"));
1641 let u = find(&tests, "U");
1642 assert!(!u.content.contains("補足"));
1643 }
1644
1645 #[test]
1647 fn csharpは属性リストの間のコメントをマーカーにせずcontentにも含めない() {
1648 let source = cs_wrap(
1649 " [Theory]\n // [FR-001]\n [InlineData(1)]\n public void T(int x) { }",
1650 );
1651 let tests = cs(&source);
1652 let t = find(&tests, "T");
1653 assert!(t.markers.is_empty());
1654 assert!(!t.content.contains("FR-001"));
1655 }
1656
1657 #[test]
1659 fn csharpのtest識別子はトップレベルclassならファイルパスと型名とメソッド名になる() {
1660 let source =
1661 "public class AuthTests\n{\n [Fact]\n public void RejectsShort() { }\n}\n";
1662 let tests = cs(source);
1663 assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:RejectsShort");
1664 }
1665
1666 #[test]
1668 fn csharpのtest識別子はネストした型を外側から内側へスラッシュで連結する() {
1669 let source = "public class Outer\n{\n public class Inner\n {\n [Test]\n public void T3() { }\n }\n}\n";
1670 let tests = cs(source);
1671 assert_eq!(tests[0].id, "tests/AuthTests:Outer/Inner:T3");
1672 }
1673
1674 #[test]
1676 fn csharpのtest識別子はstruct_record_interfaceの中でも型名を辿る() {
1677 let source = "public struct S\n{\n [Fact]\n public void A() { }\n}\n\npublic record R\n{\n [Fact]\n public void B() { }\n}\n\npublic interface I\n{\n [Fact]\n public void C() { }\n}\n";
1678 let tests = cs(source);
1679 assert_eq!(find(&tests, "A").id, "tests/AuthTests:S:A");
1680 assert_eq!(find(&tests, "B").id, "tests/AuthTests:R:B");
1681 assert_eq!(find(&tests, "C").id, "tests/AuthTests:I:C");
1682 }
1683
1684 #[test]
1686 fn csharpのtest識別子はblock_namespaceを含まない() {
1687 let source = "namespace Foo.Bar\n{\n public class AuthTests\n {\n [Fact]\n public void T() { }\n }\n}\n";
1688 let tests = cs(source);
1689 assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:T");
1690 }
1691
1692 #[test]
1694 fn csharpのtest識別子はfile_scoped_namespaceを含まない() {
1695 let source =
1696 "namespace Foo;\n\npublic class AuthTests\n{\n [Fact]\n public void T() { }\n}\n";
1697 let tests = cs(source);
1698 assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:T");
1699 }
1700
1701 #[test]
1703 fn csharpのcontentは属性を含まず修飾子から始まる() {
1704 let source = cs_wrap(
1705 " // [FR-001]\n [Fact]\n [Trait(\"a\", \"b\")]\n public void T() { Assert.True(true); }",
1706 );
1707 let tests = cs(&source);
1708 let c = &tests[0].content;
1709 assert!(c.starts_with("public void T()"), "content = {c:?}");
1710 assert!(c.ends_with('}'));
1711 assert!(!c.contains("Fact"));
1712 assert!(!c.contains("Trait"));
1713 assert!(!c.contains("FR-001"));
1714 }
1715
1716 #[test]
1718 fn csharpの本体変更はcontentに反映される() {
1719 let a = cs(&cs_wrap(
1720 " [Fact]\n public void T() { Assert.True(true); }",
1721 ));
1722 let b = cs(&cs_wrap(
1723 " [Fact]\n public void T() { Assert.True(false); }",
1724 ));
1725 assert_ne!(a[0].content, b[0].content);
1726 }
1727
1728 #[test]
1730 fn csharpの属性だけの変更はcontentを変えない() {
1731 let a = cs(&cs_wrap(
1732 " [Fact]\n public void T() { Assert.True(true); }",
1733 ));
1734 let b = cs(&cs_wrap(
1735 " [Fact(Skip = \"x\")]\n [Trait(\"a\", \"b\")]\n public void T() { Assert.True(true); }",
1736 ));
1737 assert_eq!(a[0].content, b[0].content);
1738 }
1739
1740 #[test]
1742 fn csharpは構文エラーのソースでエラーを返す() {
1743 let source = "public class A {\n [Fact]\n public void T( {\n}\n";
1744 assert!(extract_csharp_tests("tests/A.cs", source).is_err());
1745 }
1746
1747 fn ts(source: &str) -> Vec<ExtractedTest> {
1750 extract_typescript_tests("src/a.test.ts", source).unwrap()
1751 }
1752
1753 #[test]
1755 fn typescriptはtestとitの呼び出しを検出する() {
1756 let tests = ts("test(\"a\", () => {});\nit(\"b\", () => {});\n");
1757 assert_eq!(tests.len(), 2);
1758 find(&tests, "a");
1759 find(&tests, "b");
1760 }
1761
1762 #[test]
1764 fn typescriptは許可された修飾子付きの呼び出しを検出する() {
1765 let source = "it.skip(\"a\", () => {});\ntest.only(\"b\", () => {});\ntest.concurrent.only(\"c\", () => {});\ntest.todo(\"d\", () => {});\n";
1766 let tests = ts(source);
1767 assert_eq!(tests.len(), 4);
1768 for n in ["a", "b", "c", "d"] {
1769 find(&tests, n);
1770 }
1771 }
1772
1773 #[test]
1775 fn typescriptはdescribe_skipの中のtestも検出する() {
1776 let tests = ts("describe.skip(\"d\", () => {\n it(\"a\", () => {});\n});\n");
1777 assert_eq!(tests.len(), 1);
1778 assert_eq!(tests[0].id, "src/a.test:d:a");
1779 }
1780
1781 #[test]
1783 fn typescriptはfunction式のコールバックも検出する() {
1784 let source = "describe(\"d\", function () {\n it(\"a\", function () {});\n});\n";
1785 let tests = ts(source);
1786 assert_eq!(tests.len(), 1);
1787 assert_eq!(tests[0].id, "src/a.test:d:a");
1788 }
1789
1790 #[test]
1792 fn typescriptはtsxファイルのjsxを含むソースも解析できる() {
1793 let source = "it(\"renders\", () => {\n const x = <div className=\"a\">hi</div>;\n expect(x).toBeTruthy();\n});\n";
1794 let tests = extract_typescript_tests("src/a.test.tsx", source).unwrap();
1795 assert_eq!(tests.len(), 1);
1796 assert_eq!(tests[0].id, "src/a.test::renders");
1797 }
1798
1799 #[test]
1801 fn typescriptは許可外の修飾子や呼び出し結果の呼び出しを検出しない() {
1802 let source = r#"test.each([1, 2])("each %i", (n) => {});
1803test.skipIf(c)("skipif", () => {});
1804test.extend({ a: 1 });
1805test.describe("desc", () => {});
1806test.runIf(c)("runif", () => {});
1807it?.skip("optional", () => {});
1808"#;
1809 assert!(ts(source).is_empty());
1810 }
1811
1812 #[test]
1814 fn typescriptはexport_defaultやskipif_describeやforループ内のitを検出しない() {
1815 let source = r#"export default describe("d", () => { it("y", () => {}); })
1816describe.skipIf(c)("d", () => { it("x", () => {}); });
1817for (const n of [1]) {
1818 it("loop", () => {});
1819}
1820"#;
1821 assert!(ts(source).is_empty());
1822 }
1823
1824 #[test]
1826 fn typescriptは許可された修飾子を検出し走査する() {
1827 let source = r#"test.fails("f", () => {});
1828it.sequential("s", () => {});
1829describe.shuffle("d", () => {
1830 it("inner", () => {});
1831});
1832"#;
1833 let tests = ts(source);
1834 assert_eq!(tests.len(), 3);
1835 find(&tests, "f");
1836 find(&tests, "s");
1837 assert_eq!(find(&tests, "inner").id, "src/a.test:d:inner");
1838 }
1839
1840 #[test]
1842 fn typescriptはdescribe_eachの中のtestを検出しない() {
1843 let source = "describe.each([1])(\"d %i\", (n) => {\n it(\"a\", () => {});\n});\n";
1844 assert!(ts(source).is_empty());
1845 }
1846
1847 #[test]
1849 fn typescriptはawaitなど式文直下でない呼び出しを検出しない() {
1850 let source = "await it(\"a\", () => {});\nvoid it(\"b\", () => {});\n";
1851 assert!(ts(source).is_empty());
1852 }
1853
1854 #[test]
1856 fn typescriptは第1引数が文字列リテラルでない呼び出しを検出しない() {
1857 let source = "const name = \"x\";\nit(name, () => {});\nit(\"a\" + \"b\", () => {});\n";
1858 assert!(ts(source).is_empty());
1859 }
1860
1861 #[test]
1863 fn typescriptはhelper関数やtest本体やif文の中のitを検出しない() {
1864 let source = r#"function helper() {
1865 it("in helper", () => {});
1866}
1867test("outer", () => {
1868 it("in test", () => {});
1869});
1870if (cond) {
1871 it("in if", () => {});
1872}
1873"#;
1874 let tests = ts(source);
1875 assert_eq!(tests.len(), 1);
1876 assert_eq!(tests[0].name, "outer");
1877 }
1878
1879 #[test]
1881 fn typescriptは置換なしのテンプレート文字列を対象にし置換ありを無視する() {
1882 let source = "it(`plain`, () => {});\nit(`with ${x}`, () => {});\n";
1883 let tests = ts(source);
1884 assert_eq!(tests.len(), 1);
1885 assert_eq!(tests[0].name, "plain");
1886 }
1887
1888 #[test]
1890 fn typescriptは空文字名と改行を含む名前を無視する() {
1891 let source = "it(\"\", () => {});\nit(`line1\nline2`, () => {});\nit(\"ok\", () => {});\n";
1892 let tests = ts(source);
1893 assert_eq!(tests.len(), 1);
1894 assert_eq!(tests[0].name, "ok");
1895 }
1896
1897 #[test]
1899 fn typescriptは行区切り文字を含む名前を無視する() {
1900 let source = "it(\"a\u{2028}b\", () => {});\nit(\"ok\", () => {});\n";
1901 let tests = ts(source);
1902 assert_eq!(tests.len(), 1);
1903 assert_eq!(tests[0].name, "ok");
1904 }
1905
1906 #[test]
1908 fn typescriptはエスケープを解釈せずソース表記のまま名前にする() {
1909 let source = "it('it\\'s', () => {});\n";
1910 let tests = ts(source);
1911 assert_eq!(tests.len(), 1);
1912 assert_eq!(tests[0].name, "it\\'s");
1913 assert_eq!(tests[0].id, "src/a.test::it\\'s");
1914 }
1915
1916 #[test]
1918 fn typescriptのtest識別子はdescribeの外なら空のdescribeパスになる() {
1919 let tests = ts("it(\"top\", () => {});\n");
1920 assert_eq!(tests[0].id, "src/a.test::top");
1921 }
1922
1923 #[test]
1925 fn typescriptのtest識別子はdescribeを外側から内側へスラッシュで連結する() {
1926 let source = "describe(\"auth\", () => {\n it(\"one\", () => {});\n describe(\"inner\", () => {\n it(\"two\", () => {});\n });\n});\n";
1927 let tests = ts(source);
1928 assert_eq!(find(&tests, "one").id, "src/a.test:auth:one");
1929 assert_eq!(find(&tests, "two").id, "src/a.test:auth/inner:two");
1930 }
1931
1932 #[test]
1934 fn typescriptのtest識別子は対象拡張子をすべて除去しtestやspecは残す() {
1935 let src = "it(\"t\", () => {});\n";
1936 for (path, id) in [
1937 ("src/a.test.ts", "src/a.test::t"),
1938 ("src/a.spec.tsx", "src/a.spec::t"),
1939 ("src/a.test.mjs", "src/a.test::t"),
1940 ("src/a.spec.cts", "src/a.spec::t"),
1941 ("src/a.test.jsx", "src/a.test::t"),
1942 ("src/a.test.js", "src/a.test::t"),
1943 ("src/a.spec.cjs", "src/a.spec::t"),
1944 ("src/a.test.mts", "src/a.test::t"),
1945 ] {
1946 let tests = extract_typescript_tests(path, src).unwrap();
1947 assert_eq!(tests[0].id, id, "{path}");
1948 }
1949 }
1950
1951 #[test]
1953 fn typescriptのtest識別子は対象外の拡張子を除去しない() {
1954 let tests =
1955 extract_typescript_tests("src/a.test.ts.bak", "it(\"t\", () => {});\n").unwrap();
1956 assert_eq!(tests[0].id, "src/a.test.ts.bak::t");
1957 }
1958
1959 #[test]
1961 fn typescriptはjs系拡張子のファイルからtestを検出する() {
1962 for path in [
1963 "src/a.test.js",
1964 "src/a.spec.mjs",
1965 "src/a.test.cjs",
1966 "src/a.spec.mts",
1967 "src/a.test.cts",
1968 ] {
1969 let tests = extract_typescript_tests(path, "it(\"t\", () => {});\n").unwrap();
1970 assert_eq!(tests.len(), 1, "{path}");
1971 }
1972 }
1973
1974 #[test]
1976 fn typescriptはts系拡張子でtsx文法が拒否する角括弧キャストを解析できる() {
1977 let source = "it(\"t\", () => {\n const n = <number>x;\n});\n";
1978 for path in ["src/a.test.ts", "src/a.test.mts", "src/a.spec.cts"] {
1979 let tests = extract_typescript_tests(path, source).unwrap();
1980 assert_eq!(tests.len(), 1, "{path}");
1981 }
1982 }
1983
1984 #[test]
1986 fn typescriptはjsxとjsファイルのjsxを含むソースも解析できる() {
1987 let source = "it(\"renders\", () => {\n render(<App />);\n});\n";
1988 for path in ["src/a.test.jsx", "src/a.test.js"] {
1989 let tests = extract_typescript_tests(path, source).unwrap();
1990 assert_eq!(tests.len(), 1, "{path}");
1991 assert_eq!(tests[0].id, "src/a.test::renders", "{path}");
1992 }
1993 }
1994
1995 #[test]
1997 fn typescriptは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
1998 let source = "// [FR-001]\nit(\"adjacent\", () => {});\n\n// [FR-002]\n\nit(\"blank\", () => {});\n\nit(\"none\", () => {});\n";
1999 let tests = ts(source);
2000 assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
2001 assert!(find(&tests, "blank").markers.is_empty());
2002 assert!(find(&tests, "none").markers.is_empty());
2003 }
2004
2005 #[test]
2007 fn typescriptはdescribe内のtestでも直前のコメントをマーカーにする() {
2008 let source = "describe(\"d\", () => {\n // [FR-001]\n it(\"a\", () => {});\n});\n";
2009 let tests = ts(source);
2010 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
2011 }
2012
2013 #[test]
2015 fn typescriptのcontentは呼び出し文全体でマーカーコメントを含まない() {
2016 let source = "// [FR-001]\nit(\"a\", () => {\n expect(1).toBe(1);\n});\n";
2017 let tests = ts(source);
2018 let c = &tests[0].content;
2019 assert!(c.starts_with("it(\"a\""), "content = {c:?}");
2020 assert!(c.ends_with(");"), "content = {c:?}");
2021 assert!(!c.contains("FR-001"));
2022 }
2023
2024 #[test]
2026 fn typescriptの本体変更はcontentに反映される() {
2027 let a = ts("it(\"a\", () => { expect(1).toBe(1); });\n");
2028 let b = ts("it(\"a\", () => { expect(1).toBe(2); });\n");
2029 assert_ne!(a[0].content, b[0].content);
2030 }
2031
2032 #[test]
2034 fn typescriptは構文エラーのソースでエラーを返す() {
2035 let source = "it(\"a\", () => {\n";
2036 assert!(extract_typescript_tests("src/a.test.ts", source).is_err());
2037 }
2038
2039 fn jv(source: &str) -> Vec<ExtractedTest> {
2042 extract_java_tests("tests/AuthTests.java", source).unwrap()
2043 }
2044
2045 fn jv_wrap(body: &str) -> String {
2046 format!("public class AuthTests {{\n{body}\n}}\n")
2047 }
2048
2049 #[test]
2051 fn javaは5種類の注釈をそれぞれtestとして検出する() {
2052 for ann in [
2053 "Test",
2054 "ParameterizedTest",
2055 "RepeatedTest",
2056 "TestFactory",
2057 "TestTemplate",
2058 ] {
2059 let source = jv_wrap(&format!(" @{ann}\n void t() {{ }}"));
2060 let tests = jv(&source);
2061 assert_eq!(tests.len(), 1, "annotation {ann} should be detected");
2062 assert_eq!(tests[0].name, "t");
2063 }
2064 }
2065
2066 #[test]
2068 fn javaは修飾名の注釈と引数付きの注釈も検出する() {
2069 let source = jv_wrap(
2070 " @org.junit.jupiter.api.Test\n void a() { }\n\n @Test(timeout = 1)\n void b() { }",
2071 );
2072 let tests = jv(&source);
2073 assert_eq!(tests.len(), 2);
2074 find(&tests, "a");
2075 find(&tests, "b");
2076 }
2077
2078 #[test]
2080 fn javaは無関係な注釈のみのメソッドを検出しない() {
2081 let source = jv_wrap(
2082 " @Disabled\n void a() { }\n\n void b() { }\n\n @Tag(\"x\")\n void c() { }",
2083 );
2084 assert!(jv(&source).is_empty());
2085 }
2086
2087 #[test]
2089 fn javaは修飾子の後ろに書かれた注釈も検出する() {
2090 let source = jv_wrap(" public @Test void t() { }");
2091 let tests = jv(&source);
2092 assert_eq!(tests.len(), 1);
2093 assert!(tests[0].content.contains("@Test"), "{:?}", tests[0].content);
2094 }
2095
2096 #[test]
2098 fn javaは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
2099 let source = jv_wrap(
2100 " // [FR-001]\n @Test\n void adjacent() { }\n\n // [FR-002]\n\n @Test\n void blank() { }\n\n @Test\n void none() { }",
2101 );
2102 let tests = jv(&source);
2103 assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
2104 assert!(find(&tests, "blank").markers.is_empty());
2105 assert!(find(&tests, "none").markers.is_empty());
2106 }
2107
2108 #[test]
2110 fn javaはブロックコメントをマーカーにできる() {
2111 let source = jv_wrap(" /* [FR-002] */\n @Test\n void t() { }");
2112 assert_eq!(jv(&source)[0].markers, vec!["FR-002".to_string()]);
2113 }
2114
2115 #[test]
2117 fn javaはjavadocコメントをマーカーにしない() {
2118 let source = jv_wrap(" /** [FR-001] */\n @Test\n void t() { }");
2119 assert!(jv(&source)[0].markers.is_empty());
2120 }
2121
2122 #[test]
2124 fn javaは注釈と署名の間のコメントをマーカーにせずcontentにも含めない() {
2125 let source = jv_wrap(" @Test\n // [FR-001]\n // 補足\n void t() { }");
2126 let tests = jv(&source);
2127 assert!(tests[0].markers.is_empty());
2128 assert!(!tests[0].content.contains("FR-001"));
2129 assert!(!tests[0].content.contains("補足"));
2130 }
2131
2132 #[test]
2134 fn javaのtest識別子はclassならファイルパスと型名とメソッド名になる() {
2135 let tests = jv(&jv_wrap(" @Test\n void rejectsShort() { }"));
2136 assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:rejectsShort");
2137 }
2138
2139 #[test]
2141 fn javaのtest識別子はネストした型を外側から内側へスラッシュで連結する() {
2142 let source = "public class Outer {\n @Nested\n class Inner {\n @Test\n void t() { }\n }\n}\n";
2143 let tests = jv(source);
2144 assert_eq!(tests[0].id, "tests/AuthTests:Outer/Inner:t");
2145 }
2146
2147 #[test]
2149 fn javaのtest識別子はrecord_interface_enumの中でも型名を辿る() {
2150 let source = "record R() {\n @Test\n void a() { }\n}\n\ninterface I {\n @Test\n default void b() { }\n}\n\nenum E {\n X, Y;\n\n @Test\n void c() { }\n}\n";
2151 let tests = jv(source);
2152 assert_eq!(find(&tests, "a").id, "tests/AuthTests:R:a");
2153 assert_eq!(find(&tests, "b").id, "tests/AuthTests:I:b");
2154 assert_eq!(find(&tests, "c").id, "tests/AuthTests:E:c");
2155 }
2156
2157 #[test]
2159 fn javaのtest識別子はpackageを含まない() {
2160 let source =
2161 "package com.example;\n\npublic class AuthTests {\n @Test\n void t() { }\n}\n";
2162 assert_eq!(jv(source)[0].id, "tests/AuthTests:AuthTests:t");
2163 }
2164
2165 #[test]
2167 fn javaはトップレベルメソッドを対象外にする() {
2168 let source = "@Test\nvoid t() { }\n";
2169 let tests = extract_java_tests("tests/AuthTests.java", source).unwrap();
2170 assert!(tests.is_empty());
2171 let source = "class A {\n @Test\n void u() { }\n}\n";
2173 assert_eq!(jv(source).len(), 1);
2174 }
2175
2176 #[test]
2178 fn javaは匿名クラスとenum定数本体の中を対象外にする() {
2179 let source = "public class A {\n void helper() {\n Runnable r = new Runnable() {\n @Test\n public void run() { }\n };\n }\n\n @Test\n void real() { }\n}\n\nenum E {\n X {\n @Test\n void inConst() { }\n };\n\n @Test\n void outside() { }\n}\n";
2180 let tests = jv(source);
2181 let names: Vec<&str> = tests.iter().map(|t| t.name.as_str()).collect();
2182 assert_eq!(names, vec!["real", "outside"], "{names:?}");
2183 }
2184
2185 #[test]
2187 fn javaのcontentは注釈を含まず修飾子から始まる() {
2188 let source = jv_wrap(
2189 " // [FR-001]\n @Test\n @Tag(\"a\")\n public void t() { assertTrue(true); }",
2190 );
2191 let c = &jv(&source)[0].content;
2192 assert!(c.starts_with("public void t()"), "content = {c:?}");
2193 assert!(c.ends_with('}'));
2194 assert!(!c.contains("@Test"));
2195 assert!(!c.contains("@Tag"));
2196 assert!(!c.contains("FR-001"));
2197 }
2198
2199 #[test]
2201 fn javaのcontentは型パラメータ付きでも修飾子から始まる() {
2202 let source = jv_wrap(" @Test\n static <T> void t() { }");
2203 let c = &jv(&source)[0].content;
2204 assert!(c.starts_with("static <T> void t()"), "content = {c:?}");
2205 }
2206
2207 #[test]
2209 fn javaのcontentは修飾子なしなら型パラメータから始まる() {
2210 let source = jv_wrap(" @Test\n <T> void t() { }");
2211 let c = &jv(&source)[0].content;
2212 assert!(c.starts_with("<T> void t()"), "content = {c:?}");
2213 }
2214
2215 #[test]
2217 fn javaの本体変更はcontentに反映される() {
2218 let a = jv(&jv_wrap(" @Test\n void t() { assertTrue(true); }"));
2219 let b = jv(&jv_wrap(" @Test\n void t() { assertTrue(false); }"));
2220 assert_ne!(a[0].content, b[0].content);
2221 }
2222
2223 #[test]
2225 fn javaの注釈だけの変更はcontentを変えない() {
2226 let a = jv(&jv_wrap(" @Test\n void t() { assertTrue(true); }"));
2227 let b = jv(&jv_wrap(
2228 " @Test(timeout = 1)\n @Tag(\"a\")\n void t() { assertTrue(true); }",
2229 ));
2230 assert_eq!(a[0].content, b[0].content);
2231 }
2232
2233 #[test]
2235 fn javaは構文エラーのソースでエラーを返す() {
2236 let source = "public class A {\n @Test\n void t( {\n}\n";
2237 assert!(extract_java_tests("tests/A.java", source).is_err());
2238 }
2239
2240 fn kt(source: &str) -> Vec<ExtractedTest> {
2243 extract_kotlin_tests("tests/AuthTest.kt", source).unwrap()
2244 }
2245
2246 fn kt_wrap(body: &str) -> String {
2247 format!("class AuthTest {{\n{body}\n}}\n")
2248 }
2249
2250 #[test]
2252 fn kotlinは5種類の注釈をそれぞれtestとして検出する() {
2253 for ann in [
2254 "Test",
2255 "ParameterizedTest",
2256 "RepeatedTest",
2257 "TestFactory",
2258 "TestTemplate",
2259 ] {
2260 let source = kt_wrap(&format!(" @{ann}\n fun t() {{ }}"));
2261 let tests = kt(&source);
2262 assert_eq!(tests.len(), 1, "annotation {ann} should be detected");
2263 assert_eq!(tests[0].name, "t");
2264 }
2265 }
2266
2267 #[test]
2269 fn kotlinは修飾名と複数注釈リストとuse_site_target付きの注釈も検出する() {
2270 let source = kt_wrap(
2271 " @org.junit.jupiter.api.Test\n fun a() { }\n\n @[Test Tag(\"a\")]\n fun b() { }\n\n @get:Test\n fun c() { }",
2272 );
2273 let tests = kt(&source);
2274 assert_eq!(tests.len(), 3, "{tests:?}");
2275 find(&tests, "a");
2276 find(&tests, "b");
2277 find(&tests, "c");
2278 }
2279
2280 #[test]
2282 fn kotlinは無関係な注釈のみの関数を検出しない() {
2283 let source = kt_wrap(" @Disabled\n fun a() { }\n\n fun b() { }");
2284 assert!(kt(&source).is_empty());
2285 }
2286
2287 #[test]
2289 fn kotlinは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
2290 let source = kt_wrap(
2291 " // [FR-001]\n @Test\n fun adjacent() { }\n\n // [FR-002]\n\n @Test\n fun blank() { }\n\n /* [FR-003] */\n @Test\n fun block() { }",
2292 );
2293 let tests = kt(&source);
2294 assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
2295 assert!(find(&tests, "blank").markers.is_empty());
2296 assert_eq!(find(&tests, "block").markers, vec!["FR-003".to_string()]);
2297 }
2298
2299 #[test]
2301 fn kotlinはkdocコメントをマーカーにしない() {
2302 let source = kt_wrap(" /** [Foo] */\n @Test\n fun t() { }");
2303 assert!(kt(&source)[0].markers.is_empty());
2304 }
2305
2306 #[test]
2308 fn kotlinは注釈と署名の間のコメントをマーカーにせずcontentにも含めない() {
2309 let source = kt_wrap(
2310 " @Test // [FR-001]\n fun a() { }\n\n @Test\n // [FR-002]\n fun b() { }",
2311 );
2312 let tests = kt(&source);
2313 for name in ["a", "b"] {
2314 let t = find(&tests, name);
2315 assert!(t.markers.is_empty(), "{name}: {:?}", t.markers);
2316 assert!(!t.content.contains("FR-00"), "{name}: {:?}", t.content);
2317 }
2318 }
2319
2320 #[test]
2322 fn kotlinのバッククォート名はバッククォートを除いた名前になる() {
2323 let source = kt_wrap(" @Test\n fun `一覧 取得 は 200`() { }");
2324 let tests = kt(&source);
2325 assert_eq!(tests[0].name, "一覧 取得 は 200");
2326 assert_eq!(tests[0].id, "tests/AuthTest:AuthTest:一覧 取得 は 200");
2327 }
2328
2329 #[test]
2331 fn kotlinのtest識別子はネストした型を外側から内側へスラッシュで連結する() {
2332 let source = "class AuthTest {\n @Nested\n inner class Inner {\n @Test\n fun t() { }\n }\n}\n";
2333 assert_eq!(kt(source)[0].id, "tests/AuthTest:AuthTest/Inner:t");
2334 }
2335
2336 #[test]
2338 fn kotlinのtest識別子はobjectとcompanion_objectを辿る() {
2339 let source = "object O {\n @Test\n fun a() { }\n}\n\nclass C {\n companion object {\n @Test\n fun b() { }\n }\n\n companion object Factory {\n @Test\n fun c() { }\n }\n}\n";
2340 let tests = kt(source);
2343 assert_eq!(find(&tests, "a").id, "tests/AuthTest:O:a");
2344 assert_eq!(find(&tests, "b").id, "tests/AuthTest:C/Companion:b");
2345 assert_eq!(find(&tests, "c").id, "tests/AuthTest:C/Factory:c");
2346 }
2347
2348 #[test]
2350 fn kotlinのトップレベル関数は型部分が空の識別子になる() {
2351 let source = "@Test\nfun top() { }\n";
2352 let tests = kt(source);
2353 assert_eq!(tests.len(), 1);
2354 assert_eq!(tests[0].id, "tests/AuthTest::top");
2355 }
2356
2357 #[test]
2359 fn kotlinのtest識別子はpackageを含まない() {
2360 let source = "package com.example\n\nclass AuthTest {\n @Test\n fun t() { }\n}\n";
2361 assert_eq!(kt(source)[0].id, "tests/AuthTest:AuthTest:t");
2362 }
2363
2364 #[test]
2366 fn kotlinはコンストラクタ注釈付きの主コンストラクタを持つクラスでもtestを取れる() {
2367 let source = "@SpringBootTest\nclass X @Autowired constructor(private val a: Int) : Base(a) {\n @Test\n fun `日本語名`() { }\n}\n";
2368 let tests = kt(source);
2369 assert_eq!(tests.len(), 1, "{tests:?}");
2370 assert_eq!(tests[0].id, "tests/AuthTest:X:日本語名");
2371 }
2372
2373 #[test]
2375 fn kotlinは関数本体内のローカル関数や匿名objectやenum定数本体やgetterを対象外にする() {
2376 let source = "class A {\n @Test\n fun real() {\n @Test\n fun local() { }\n }\n\n val x: Int\n @Test\n get() = 1\n\n val r = object : Runnable {\n @Test\n fun inObj() { }\n }\n}\n\nenum class E {\n X {\n @Test\n fun inConst() { }\n };\n\n @Test\n fun outside() { }\n}\n";
2377 let tests = kt(source);
2378 let names: Vec<&str> = tests.iter().map(|t| t.name.as_str()).collect();
2379 assert_eq!(names, vec!["real", "outside"], "{names:?}");
2380 }
2381
2382 #[test]
2384 fn kotlinは漢字の関数名と型名のtestを抽出し識別子とcontentが元の文字を保つ() {
2385 let source = "class AuthTest {\n @Nested\n inner class 一覧取得 {\n @Test\n fun 取得() { println(\"漢字\") }\n }\n}\n";
2386 let tests = kt(source);
2387 assert_eq!(tests.len(), 1, "{tests:?}");
2388 assert_eq!(tests[0].id, "tests/AuthTest:AuthTest/一覧取得:取得");
2389 assert_eq!(tests[0].name, "取得");
2390 assert!(tests[0].content.contains("取得"));
2391 assert!(tests[0].content.contains("漢字"));
2392 assert!(!tests[0].content.contains('x'), "{:?}", tests[0].content);
2393 }
2394
2395 #[test]
2397 fn kotlinはカタカナとハングルの関数名も抽出する() {
2398 let source = kt_wrap(" @Test\n fun テスト() { }\n\n @Test\n fun 테스트() { }");
2399 let tests = kt(&source);
2400 assert_eq!(tests.len(), 2, "{tests:?}");
2401 find(&tests, "テスト");
2402 find(&tests, "테스트");
2403 }
2404
2405 #[test]
2407 fn kotlinは非asciiの文字リテラルを含むファイルも解析できる() {
2408 let source =
2409 kt_wrap(" @Test\n fun t() {\n val c = 'あ'\n val d = '漢'\n }");
2410 let tests = kt(&source);
2411 assert_eq!(tests.len(), 1);
2412 assert!(tests[0].content.contains("'あ'"));
2413 }
2414
2415 #[test]
2417 fn kotlinはbom付きファイルも解析できる() {
2418 let source = format!("\u{feff}{}", kt_wrap(" @Test\n fun t() { }"));
2419 let tests = kt(&source);
2420 assert_eq!(tests.len(), 1);
2421 assert_eq!(tests[0].name, "t");
2422 }
2423
2424 #[test]
2426 fn kotlinのcontentは注釈を含まず修飾子から始まる() {
2427 let source = kt_wrap(
2428 " // [FR-001]\n @Test\n internal suspend fun t() { assertTrue(true) }",
2429 );
2430 let c = &kt(&source)[0].content;
2431 assert!(c.starts_with("internal suspend fun t()"), "content = {c:?}");
2432 assert!(c.ends_with('}'));
2433 assert!(!c.contains("@Test"));
2434 assert!(!c.contains("FR-001"));
2435 }
2436
2437 #[test]
2439 fn kotlinのcontentは注釈のみならfunから始まる() {
2440 let source = kt_wrap(" @Test\n @Tag(\"a\")\n fun t() { }");
2441 let c = &kt(&source)[0].content;
2442 assert!(c.starts_with("fun t()"), "content = {c:?}");
2443 }
2444
2445 #[test]
2447 fn kotlinの修飾子の後ろに書かれた注釈はcontentに含まれる() {
2448 let source = kt_wrap(" internal @Test fun t() { }");
2449 let tests = kt(&source);
2450 assert_eq!(tests.len(), 1);
2451 assert!(
2452 tests[0].content.starts_with("internal"),
2453 "{:?}",
2454 tests[0].content
2455 );
2456 assert!(tests[0].content.contains("@Test"), "{:?}", tests[0].content);
2457 }
2458
2459 #[test]
2461 fn kotlinの式本体は終端までcontentに含まれる() {
2462 let source = kt_wrap(" @Test\n fun t() = assertTrue(computeSomething())");
2463 let c = &kt(&source)[0].content;
2464 assert!(c.ends_with("computeSomething())"), "content = {c:?}");
2465 }
2466
2467 #[test]
2469 fn kotlinの本体変更はcontentに反映される() {
2470 let a = kt(&kt_wrap(" @Test\n fun t() { assertTrue(true) }"));
2471 let b = kt(&kt_wrap(" @Test\n fun t() { assertTrue(false) }"));
2472 assert_ne!(a[0].content, b[0].content);
2473 }
2474
2475 #[test]
2477 fn kotlinの注釈だけの変更はcontentを変えない() {
2478 let a = kt(&kt_wrap(" @Test\n fun t() { assertTrue(true) }"));
2479 let b = kt(&kt_wrap(
2480 " @Test\n @Tag(\"a\")\n fun t() { assertTrue(true) }",
2481 ));
2482 assert_eq!(a[0].content, b[0].content);
2483 }
2484
2485 #[test]
2487 fn kotlinは構文エラーのソースでエラーを返す() {
2488 let source = "class A { fun x() {} }\n";
2489 assert!(extract_kotlin_tests("tests/A.kt", source).is_err());
2490 }
2491
2492 #[test]
2494 fn kotlinのマーカーは元のソースから取り非asciiの角括弧は偽マーカーにならない() {
2495 let source = kt_wrap(
2496 " // [正常系]\n // [FR-001]\n @Test\n fun t() { }\n\n // [仮] メモ\n @Test\n fun u() { }\n\n // [FR\u{2010}001]\n @Test\n fun v() { }",
2497 );
2498 let tests = kt(&source);
2499 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2500 assert!(find(&tests, "u").markers.is_empty());
2501 assert!(find(&tests, "v").markers.is_empty());
2502 }
2503
2504 #[test]
2506 fn kotlinはdocコメントを挟んでも手前のマーカーが有効になる() {
2507 let source = kt_wrap(" // [FR-001]\n /** KDoc */\n @Test\n fun b() { }");
2508 assert_eq!(kt(&source)[0].markers, vec!["FR-001".to_string()]);
2509 }
2510
2511 #[test]
2513 fn javaはdocコメントを挟んでも手前のマーカーが有効になる() {
2514 let source = jv_wrap(" // [FR-001]\n /** Javadoc */\n @Test\n void b() { }");
2515 assert_eq!(jv(&source)[0].markers, vec!["FR-001".to_string()]);
2516 }
2517
2518 #[test]
2520 fn javaは注釈同士の間のコメントをマーカーにせずcontentにも含めない() {
2521 let source =
2522 jv_wrap(" @Test\n // [FR-001]\n @Tag(\"a\")\n // 補足\n void t() { }");
2523 let t = &jv(&source)[0];
2524 assert!(t.markers.is_empty());
2525 assert!(!t.content.contains("FR-001"));
2526 assert!(!t.content.contains("補足"));
2527 }
2528
2529 #[test]
2531 fn kotlinは注釈の間と直後のコメントをcontentに含めない() {
2532 let source = kt_wrap(
2533 " @Test\n // 間のメモ\n @Tag(\"a\")\n // 直後のメモ\n fun t() { }",
2534 );
2535 let t = &kt(&source)[0];
2536 assert!(!t.content.contains("メモ"), "{:?}", t.content);
2537 assert!(t.content.starts_with("fun t()"), "{:?}", t.content);
2538 }
2539
2540 #[test]
2542 fn kotlinは注釈同士の間のコメントをマーカーにしない() {
2543 let source = kt_wrap(" @Test\n // [FR-001]\n @Tag(\"a\")\n fun t() { }");
2544 assert!(kt(&source)[0].markers.is_empty());
2545 }
2546
2547 #[test]
2549 fn kotlinはdata_classとinterfaceの直下のtestも辿る() {
2550 let source = "data class D(val a: Int) {\n @Test\n fun a() { }\n}\n\ninterface I {\n @Test\n fun b() { }\n}\n";
2551 let tests = kt(source);
2552 assert_eq!(find(&tests, "a").id, "tests/AuthTest:D:a");
2553 assert_eq!(find(&tests, "b").id, "tests/AuthTest:I:b");
2554 }
2555
2556 #[test]
2558 fn kotlinは制御文字や空を含む名前のtestを無視する() {
2559 let source =
2560 kt_wrap(" @Test\n fun `tab\there`() { }\n\n @Test\n fun `ok name`() { }");
2561 let tests = kt(&source);
2562 assert_eq!(tests.len(), 1, "{tests:?}");
2563 assert_eq!(tests[0].name, "ok name");
2564 }
2565
2566 #[test]
2568 fn kotlinは注釈2つで基底型なしのクラス内の注釈付きinner_classでもtestを取れる() {
2569 let source = "@A\n@C\nclass P {\n @Nested\n inner class X {\n // [FR-001]\n @Test\n fun t() { }\n }\n}\n";
2570 let tests = kt(source);
2571 assert_eq!(tests.len(), 1, "{tests:?}");
2572 assert_eq!(tests[0].id, "tests/AuthTest:P/X:t");
2573 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
2574 }
2575
2576 #[test]
2578 fn kotlinはnestedを含む複数注釈クラスでも非asciiの名前を元のまま取る() {
2579 let source = "@A\n@C\nclass P {\n @Nested\n inner class 一覧取得 {\n @Test\n fun `取得 する`() { }\n }\n}\n";
2580 let tests = kt(source);
2581 assert_eq!(tests[0].id, "tests/AuthTest:P/一覧取得:取得 する");
2582 }
2583
2584 #[test]
2586 fn kotlinは宣言以外のトップレベル構造やキーワードを含む文字列でもok空になる() {
2587 for source in [
2588 "package a\n// helper for class X\nval x = 1\n",
2589 "package a\nval fixture = object : Runnable {\n override fun run() {}\n}\n",
2590 "package a\nval f = fun() { }\n",
2591 "package a\nconst val NAME = \"class\"\n",
2592 ] {
2593 let tests = kt(source);
2594 assert!(tests.is_empty(), "{source:?}: {tests:?}");
2595 }
2596 }
2597
2598 #[test]
2600 fn kotlinは文法の不具合で宣言が認識されなかった場合に構文エラーを返す() {
2601 let source = "@A\n@C\nclass P {\n @Nested\n inner class X {\n }\n}\n";
2602 assert!(kotlin_extract("tests/P.kt", source, true).is_ok());
2603 assert!(kotlin_extract("tests/P.kt", source, false).is_err());
2604 }
2605
2606 #[test]
2608 fn kotlinはshebangやimportやtypealiasなど宣言以外の通常の構造でok空になる() {
2609 let source = "#!/usr/bin/env kotlin\n@file:JvmName(\"X\")\npackage a\n\nimport b.C\n\ntypealias N = String\nval x = 1\nval y: Int\n get() = 2\n";
2610 assert!(kt(source).is_empty());
2611 }
2612
2613 #[test]
2617 fn rustはdocコメントを挟んでもマーカーを取る() {
2618 let tests = rs("// [FR-001]\n/// doc\n#[test]\nfn t() {}\n", &[]);
2619 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2620 }
2621
2622 #[test]
2624 fn rustの外側docコメントはマーカーにならない() {
2625 let tests = rs("/// [FR-001]\n#[test]\nfn t() {}\n", &[]);
2626 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2627 }
2628
2629 #[test]
2631 fn rustの内側docコメントはマーカーにならない() {
2632 let tests = rs("mod m {\n//! [FR-001]\n#[test]\nfn t() {}\n}\n", &[]);
2633 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2634 }
2635
2636 #[test]
2638 fn rustはブロックdocコメントを挟んでもマーカーを取る() {
2639 let tests = rs("// [FR-001]\n/** doc */\n#[test]\nfn t() {}\n", &[]);
2640 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2641 }
2642
2643 #[test]
2645 fn rustのスラッシュ4つのコメントは通常コメントとしてマーカーになる() {
2646 let tests = rs("//// [FR-001]\n#[test]\nfn t() {}\n", &[]);
2647 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2648 }
2649
2650 #[test]
2652 fn rustはdocコメントの後に空行があるとマーカーを取らない() {
2653 let tests = rs("// [FR-001]\n/// doc\n\n#[test]\nfn t() {}\n", &[]);
2654 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2655 }
2656
2657 #[test]
2659 fn rustはcrlfでもdocコメントを挟んでマーカーを取る() {
2660 let tests = rs("// [FR-001]\r\n/// doc\r\n#[test]\r\nfn t() {}\r\n", &[]);
2661 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2662 }
2663
2664 #[test]
2666 fn goは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2667 let source =
2668 "package a\n\nfunc TestA(t *testing.T) {} // [FR-001]\nfunc TestB(t *testing.T) {}\n";
2669 let tests = extract_go_tests("a/x_test.go", source).unwrap();
2670 assert!(find(&tests, "TestB").markers.is_empty());
2671 }
2672
2673 #[test]
2675 fn rustは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2676 let tests = rs("fn x() {} // [FR-001]\n#[test]\nfn t() {}\n", &[]);
2677 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2678 }
2679
2680 #[test]
2682 fn csharpは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2683 let source =
2684 cs_wrap(" public void X() {} // [FR-001]\n [Fact]\n public void T() {}");
2685 let tests = cs(&source);
2686 assert!(find(&tests, "T").markers.is_empty());
2687 }
2688
2689 #[test]
2691 fn typescriptは前の文の末尾行コメントを次のtestのマーカーにしない() {
2692 let tests = ts("it(\"a\", () => {}); // [FR-001]\nit(\"b\", () => {});\n");
2693 assert!(find(&tests, "b").markers.is_empty());
2694 }
2695
2696 #[test]
2698 fn javaは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2699 let source = jv_wrap(" void x() {} // [FR-001]\n @Test\n void t() {}");
2700 let tests = jv(&source);
2701 assert!(find(&tests, "t").markers.is_empty());
2702 }
2703
2704 #[test]
2706 fn kotlinは非ascii識別子の後の末尾行コメントを次のtestのマーカーにしない() {
2707 let source = kt_wrap(" fun `日本語`() {} // [FR-001]\n @Test\n fun t() {}");
2708 let tests = kt(&source);
2709 assert!(find(&tests, "t").markers.is_empty());
2710 }
2711
2712 #[test]
2714 fn rustはインデント付きの行頭コメントをマーカーにする() {
2715 let tests = rs(
2716 "mod m {\n // [FR-001]\n #[test]\n fn t() {}\n}\n",
2717 &[],
2718 );
2719 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2720 }
2721
2722 #[test]
2724 fn typescriptは同じ行に複数のコメントがあると2つ目以降は行頭でないので打ち切る() {
2725 let tests = ts("/* [FR-001] */ // note\nit(\"b\", () => {});\n");
2726 assert!(find(&tests, "b").markers.is_empty());
2727 }
2728
2729 #[test]
2731 fn rustはbomの直後の1行目マーカーを取る() {
2732 let tests = rs("\u{feff}// [FR-001]\n#[test]\nfn t() {}\n", &[]);
2733 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2734 }
2735
2736 #[test]
2738 fn goはbomの直後の1行目マーカーを取る() {
2739 let source = "\u{feff}// [FR-001]\nfunc TestA(t *testing.T) {}\n";
2740 let tests = extract_go_tests("a/x_test.go", source).unwrap();
2741 assert_eq!(find(&tests, "TestA").markers, vec!["FR-001".to_string()]);
2742 }
2743
2744 #[test]
2746 fn rustの内側ブロックdocコメントはマーカーにならない() {
2747 let tests = rs("mod m {\n/*! [FR-001] */\n#[test]\nfn t() {}\n}\n", &[]);
2748 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2749 }
2750
2751 #[test]
2753 fn rustのスラッシュ星3つのコメントは通常コメントとしてマーカーになる() {
2754 let tests = rs("/*** [FR-001] */\n#[test]\nfn t() {}\n", &[]);
2755 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2756 }
2757
2758 #[test]
2760 fn rustの外側ブロックdocコメントはマーカーにならない() {
2761 let tests = rs("/** [FR-001] */\n#[test]\nfn t() {}\n", &[]);
2762 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2763 }
2764}