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 comment_body(text: &str) -> (&str, bool) {
346 if let Some(rest) = text.strip_prefix("//") {
347 return (rest.strip_prefix('/').unwrap_or(rest), false);
348 }
349 match text.strip_prefix("/*") {
350 Some(rest) => {
351 let rest = rest.strip_suffix("*/").unwrap_or(rest);
352 (rest.strip_prefix('*').unwrap_or(rest), true)
353 }
354 None => (text, false),
355 }
356}
357
358fn extract_markers(text: &str) -> Vec<String> {
364 let (body, is_block) = comment_body(text);
365 let mut markers = Vec::new();
366 for (i, line) in body.lines().enumerate() {
367 let line = match (is_block && i > 0, line.trim_start().strip_prefix('*')) {
369 (true, Some(rest)) => rest,
370 _ => line,
371 };
372 for token in line.split_whitespace() {
373 let Some(inner) = token
374 .strip_prefix('[')
375 .and_then(|t| t.strip_suffix(']'))
376 .filter(|inner| {
377 !inner.is_empty()
378 && inner
379 .chars()
380 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-')
381 })
382 else {
383 return Vec::new();
384 };
385 markers.push(inner.to_string());
386 }
387 }
388 markers
389}
390
391fn dir_of(path: &str) -> String {
392 match path.rsplit_once('/') {
393 Some((dir, _)) => dir.to_string(),
394 None => ".".to_string(),
395 }
396}
397
398const CSHARP_TEST_ATTRIBUTES: [&str; 7] = [
399 "Fact",
400 "Theory",
401 "Test",
402 "TestCase",
403 "TestCaseSource",
404 "TestMethod",
405 "DataTestMethod",
406];
407
408pub fn extract_csharp_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
413 let mut parser = tree_sitter::Parser::new();
414 parser
415 .set_language(&tree_sitter_c_sharp::LANGUAGE.into())
416 .expect("valid c# grammar");
417 let tree = parser
418 .parse(source, None)
419 .ok_or_else(|| ExtractError::Parse {
420 path: path.to_string(),
421 })?;
422 let root = tree.root_node();
423 if root.has_error() {
424 return Err(ExtractError::Parse {
425 path: path.to_string(),
426 });
427 }
428 let base = path.strip_suffix(".cs").unwrap_or(path);
429 let mut results = Vec::new();
430 walk_csharp_container(root, source, base, &[], &mut results);
431 Ok(results)
432}
433
434fn walk_csharp_container(
435 container: tree_sitter::Node,
436 source: &str,
437 base: &str,
438 types: &[String],
439 results: &mut Vec<ExtractedTest>,
440) {
441 let mut cursor = container.walk();
442 let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
443 for (idx, node) in children.iter().enumerate() {
444 match node.kind() {
445 "preproc_if" | "preproc_elif" | "preproc_else" => {
446 walk_csharp_container(*node, source, base, types, results);
447 }
448 "namespace_declaration" => {
449 if let Some(body) = node.child_by_field_name("body") {
450 walk_csharp_container(body, source, base, types, results);
451 }
452 }
453 "class_declaration"
454 | "struct_declaration"
455 | "record_declaration"
456 | "interface_declaration" => {
457 let (Some(name_node), Some(body)) = (
458 node.child_by_field_name("name"),
459 node.child_by_field_name("body"),
460 ) else {
461 continue;
462 };
463 let name = name_node.utf8_text(source.as_bytes()).unwrap_or("");
464 let mut new_types = types.to_vec();
465 new_types.push(name.to_string());
466 walk_csharp_container(body, source, base, &new_types, results);
467 }
468 "method_declaration" if !types.is_empty() => {
469 if !csharp_is_test_method(*node, source) {
470 continue;
471 }
472 let Some(name_node) = node.child_by_field_name("name") else {
473 continue;
474 };
475 let name = name_node
476 .utf8_text(source.as_bytes())
477 .unwrap_or("")
478 .to_string();
479 let mut c = node.walk();
480 let Some(first) = node
481 .children(&mut c)
482 .find(|ch| ch.kind() != "attribute_list" && ch.kind() != "comment")
483 else {
484 continue;
485 };
486 let content = source[first.start_byte()..node.end_byte()].to_string();
487 let markers = collect_preceding_markers(&children, idx, source, "comment");
488 results.push(ExtractedTest {
489 id: format!("{base}:{}:{name}", types.join("/")),
490 name,
491 markers,
492 content,
493 });
494 }
495 _ => {}
496 }
497 }
498}
499
500fn csharp_is_test_method(method: tree_sitter::Node, source: &str) -> bool {
501 let mut cursor = method.walk();
502 for list in method.children(&mut cursor) {
503 if list.kind() != "attribute_list" {
504 continue;
505 }
506 let mut c2 = list.walk();
507 for attr in list.children(&mut c2) {
508 if attr.kind() != "attribute" {
509 continue;
510 }
511 let Some(mut name) = attr.child_by_field_name("name") else {
512 continue;
513 };
514 while name.kind() == "qualified_name" {
515 match name.child_by_field_name("name") {
516 Some(n) => name = n,
517 None => break,
518 }
519 }
520 let text = name.utf8_text(source.as_bytes()).unwrap_or("");
521 if CSHARP_TEST_ATTRIBUTES.contains(&text) {
522 return true;
523 }
524 }
525 }
526 false
527}
528
529const TYPESCRIPT_TEST_MODIFIERS: [&str; 6] =
530 ["skip", "only", "todo", "fails", "concurrent", "sequential"];
531const TYPESCRIPT_DESCRIBE_MODIFIERS: [&str; 6] = [
532 "skip",
533 "only",
534 "todo",
535 "concurrent",
536 "sequential",
537 "shuffle",
538];
539
540pub const TYPESCRIPT_TEST_EXTENSIONS: [&str; 8] =
543 ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
544
545pub fn is_typescript_test_file(path: &str) -> bool {
548 TYPESCRIPT_TEST_EXTENSIONS.iter().any(|ext| {
549 path.ends_with(&format!(".test.{ext}")) || path.ends_with(&format!(".spec.{ext}"))
550 })
551}
552
553pub fn extract_typescript_tests(
559 path: &str,
560 source: &str,
561) -> Result<Vec<ExtractedTest>, ExtractError> {
562 let mut parser = tree_sitter::Parser::new();
563 let lang: tree_sitter::Language = if [".ts", ".mts", ".cts"]
564 .iter()
565 .any(|ext| path.ends_with(ext))
566 {
567 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
568 } else {
569 tree_sitter_typescript::LANGUAGE_TSX.into()
570 };
571 parser
572 .set_language(&lang)
573 .expect("valid typescript grammar");
574 let tree = parser
575 .parse(source, None)
576 .ok_or_else(|| ExtractError::Parse {
577 path: path.to_string(),
578 })?;
579 let root = tree.root_node();
580 if root.has_error() {
581 return Err(ExtractError::Parse {
582 path: path.to_string(),
583 });
584 }
585 let base = TYPESCRIPT_TEST_EXTENSIONS
586 .iter()
587 .find_map(|ext| path.strip_suffix(&format!(".{ext}")))
588 .unwrap_or(path);
589 let mut results = Vec::new();
590 walk_typescript_statements(root, source, base, &[], &mut results);
591 Ok(results)
592}
593
594fn typescript_callee_chain<'a>(
597 callee: tree_sitter::Node,
598 source: &'a str,
599) -> Option<(&'a str, Vec<&'a str>)> {
600 let mut props = Vec::new();
601 let mut cur = callee;
602 loop {
603 match cur.kind() {
604 "identifier" => {
605 props.reverse();
606 return Some((cur.utf8_text(source.as_bytes()).ok()?, props));
607 }
608 "member_expression" => {
609 let mut c = cur.walk();
610 if cur.children(&mut c).any(|ch| ch.kind() == "optional_chain") {
611 return None;
612 }
613 let prop = cur.child_by_field_name("property")?;
614 if prop.kind() != "property_identifier" {
615 return None;
616 }
617 props.push(prop.utf8_text(source.as_bytes()).ok()?);
618 cur = cur.child_by_field_name("object")?;
619 }
620 _ => return None,
621 }
622 }
623}
624
625fn typescript_name_arg<'a>(args: tree_sitter::Node, source: &'a str) -> Option<&'a str> {
628 let first = args.named_child(0)?;
629 match first.kind() {
630 "string" => {}
631 "template_string" => {
632 let mut c = first.walk();
633 if first
634 .children(&mut c)
635 .any(|ch| ch.kind() == "template_substitution")
636 {
637 return None;
638 }
639 }
640 _ => return None,
641 }
642 let text = first.utf8_text(source.as_bytes()).ok()?;
643 if text.len() < 2 {
644 return None;
645 }
646 let name = &text[1..text.len() - 1];
647 if !is_valid_test_name(name) {
648 return None;
649 }
650 Some(name)
651}
652
653fn walk_typescript_statements(
654 container: tree_sitter::Node,
655 source: &str,
656 base: &str,
657 describes: &[String],
658 results: &mut Vec<ExtractedTest>,
659) {
660 let mut cursor = container.walk();
661 let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
662 for (idx, node) in children.iter().enumerate() {
663 if node.kind() != "expression_statement" {
664 continue;
665 }
666 let Some(call) = node.named_child(0) else {
667 continue;
668 };
669 if call.kind() != "call_expression" {
670 continue;
671 }
672 let (Some(callee), Some(args)) = (
673 call.child_by_field_name("function"),
674 call.child_by_field_name("arguments"),
675 ) else {
676 continue;
677 };
678 let Some((head, props)) = typescript_callee_chain(callee, source) else {
679 continue;
680 };
681 let Some(name) = typescript_name_arg(args, source) else {
682 continue;
683 };
684 if (head == "test" || head == "it")
685 && props.iter().all(|p| TYPESCRIPT_TEST_MODIFIERS.contains(p))
686 {
687 let markers = collect_preceding_markers(&children, idx, source, "comment");
688 let content = node.utf8_text(source.as_bytes()).unwrap_or("").to_string();
689 results.push(ExtractedTest {
690 id: format!("{base}:{}:{name}", describes.join("/")),
691 name: name.to_string(),
692 markers,
693 content,
694 });
695 } else if head == "describe"
696 && props
697 .iter()
698 .all(|p| TYPESCRIPT_DESCRIBE_MODIFIERS.contains(p))
699 {
700 let Some(cb) = args.named_child(1) else {
701 continue;
702 };
703 if cb.kind() != "arrow_function" && cb.kind() != "function_expression" {
704 continue;
705 }
706 let Some(body) = cb.child_by_field_name("body") else {
707 continue;
708 };
709 if body.kind() != "statement_block" {
710 continue;
711 }
712 let mut new_desc = describes.to_vec();
713 new_desc.push(name.to_string());
714 walk_typescript_statements(body, source, base, &new_desc, results);
715 }
716 }
717}
718
719const JVM_TEST_ANNOTATIONS: [&str; 5] = [
724 "Test",
725 "ParameterizedTest",
726 "RepeatedTest",
727 "TestFactory",
728 "TestTemplate",
729];
730
731const JVM_COMMENT_KINDS: [&str; 2] = ["line_comment", "block_comment"];
732
733fn jvm_content_start(decl: tree_sitter::Node) -> Option<usize> {
737 fn skip(kind: &str) -> bool {
738 matches!(
739 kind,
740 "annotation" | "marker_annotation" | "line_comment" | "block_comment"
741 )
742 }
743 let mut cursor = decl.walk();
744 for child in decl.children(&mut cursor) {
745 if child.kind() == "modifiers" {
746 let mut c2 = child.walk();
747 let first = child.children(&mut c2).find(|n| !skip(n.kind()));
748 if let Some(first) = first {
749 return Some(first.start_byte());
750 }
751 } else if !skip(child.kind()) {
752 return Some(child.start_byte());
753 }
754 }
755 None
756}
757
758fn jvm_name_is_test(name: &str) -> bool {
759 JVM_TEST_ANNOTATIONS.contains(&name)
760}
761
762pub fn extract_java_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
769 let mut parser = tree_sitter::Parser::new();
770 parser
771 .set_language(&tree_sitter_java::LANGUAGE.into())
772 .expect("valid java grammar");
773 let tree = parser
774 .parse(source, None)
775 .ok_or_else(|| ExtractError::Parse {
776 path: path.to_string(),
777 })?;
778 let root = tree.root_node();
779 if root.has_error() {
780 return Err(ExtractError::Parse {
781 path: path.to_string(),
782 });
783 }
784 let base = path.strip_suffix(".java").unwrap_or(path);
785 let mut results = Vec::new();
786 walk_java_container(root, source, base, &[], &mut results);
787 Ok(results)
788}
789
790fn walk_java_container(
791 container: tree_sitter::Node,
792 source: &str,
793 base: &str,
794 types: &[String],
795 results: &mut Vec<ExtractedTest>,
796) {
797 let mut cursor = container.walk();
798 let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
799 for (idx, node) in children.iter().enumerate() {
800 match node.kind() {
801 "class_declaration"
802 | "record_declaration"
803 | "interface_declaration"
804 | "enum_declaration" => {
805 let (Some(name_node), Some(body)) = (
806 node.child_by_field_name("name"),
807 node.child_by_field_name("body"),
808 ) else {
809 continue;
810 };
811 let name = name_node.utf8_text(source.as_bytes()).unwrap_or("");
812 let mut new_types = types.to_vec();
813 new_types.push(name.to_string());
814 if body.kind() == "enum_body" {
815 let mut c = body.walk();
816 let decls: Vec<_> = body
817 .children(&mut c)
818 .filter(|n| n.kind() == "enum_body_declarations")
819 .collect();
820 for d in decls {
821 walk_java_container(d, source, base, &new_types, results);
822 }
823 } else {
824 walk_java_container(body, source, base, &new_types, results);
825 }
826 }
827 "method_declaration" if !types.is_empty() => {
828 if !java_is_test_method(*node, source) {
829 continue;
830 }
831 let Some(name_node) = node.child_by_field_name("name") else {
832 continue;
833 };
834 let Some(start) = jvm_content_start(*node) else {
835 continue;
836 };
837 let name = name_node
838 .utf8_text(source.as_bytes())
839 .unwrap_or("")
840 .to_string();
841 results.push(ExtractedTest {
842 id: format!("{base}:{}:{name}", types.join("/")),
843 name,
844 markers: collect_preceding_markers_with(
845 &children,
846 idx,
847 source,
848 &|n| n.utf8_text(source.as_bytes()).ok(),
849 &JVM_COMMENT_KINDS,
850 &|n| n.utf8_text(source.as_bytes()).is_ok_and(is_doc_comment),
851 ),
852 content: source[start..node.end_byte()].to_string(),
853 });
854 }
855 _ => {}
856 }
857 }
858}
859
860fn java_is_test_method(method: tree_sitter::Node, source: &str) -> bool {
861 let mut cursor = method.walk();
862 let Some(modifiers) = method
863 .children(&mut cursor)
864 .find(|n| n.kind() == "modifiers")
865 else {
866 return false;
867 };
868 let mut c2 = modifiers.walk();
869 for ann in modifiers.children(&mut c2) {
870 if ann.kind() != "marker_annotation" && ann.kind() != "annotation" {
871 continue;
872 }
873 let Some(mut name) = ann.child_by_field_name("name") else {
874 continue;
875 };
876 while name.kind() == "scoped_identifier" {
877 match name.child_by_field_name("name") {
878 Some(n) => name = n,
879 None => break,
880 }
881 }
882 if jvm_name_is_test(name.utf8_text(source.as_bytes()).unwrap_or("")) {
883 return true;
884 }
885 }
886 false
887}
888
889struct KotlinShadow<'a> {
898 original: &'a str,
899 shadow: String,
900 map: Vec<(usize, usize)>,
901}
902
903impl<'a> KotlinShadow<'a> {
904 fn new(original: &'a str) -> Self {
905 let mut shadow = String::with_capacity(original.len());
906 let mut map = vec![(0usize, 0usize)];
907 for (off, ch) in original.char_indices() {
908 if ch.is_ascii() {
909 shadow.push(ch);
910 } else {
911 shadow.push(if ch.is_whitespace() || ch == '\u{feff}' {
912 ' '
913 } else {
914 'x'
915 });
916 map.push((shadow.len(), off + ch.len_utf8()));
917 }
918 }
919 KotlinShadow {
920 original,
921 shadow,
922 map,
923 }
924 }
925
926 fn blank_ranges(&mut self, ranges: &[(usize, usize)]) {
928 let mut bytes = std::mem::take(&mut self.shadow).into_bytes();
929 for &(a, b) in ranges {
930 for byte in &mut bytes[a..b] {
931 if *byte != b'\n' {
932 *byte = b' ';
933 }
934 }
935 }
936 self.shadow = String::from_utf8(bytes).expect("ascii shadow");
937 }
938
939 fn to_original(&self, shadow_offset: usize) -> usize {
940 let i = self.map.partition_point(|&(s, _)| s <= shadow_offset) - 1;
941 let (s, o) = self.map[i];
942 o + (shadow_offset - s)
943 }
944
945 fn slice(&self, start: usize, end: usize) -> Option<&'a str> {
946 self.original
947 .get(self.to_original(start)..self.to_original(end))
948 }
949
950 fn node_text(&self, node: tree_sitter::Node) -> Option<&'a str> {
951 self.slice(node.start_byte(), node.end_byte())
952 }
953}
954
955pub fn extract_kotlin_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
960 kotlin_extract(path, source, true)
961}
962
963const KOTLIN_ROOT_KINDS: [&str; 11] = [
967 "shebang",
968 "file_annotation",
969 "package_header",
970 "import",
971 "class_declaration",
972 "object_declaration",
973 "function_declaration",
974 "property_declaration",
975 "type_alias",
976 "line_comment",
977 "block_comment",
978];
979
980fn kotlin_extract(
983 path: &str,
984 source: &str,
985 retry_annotated_expression: bool,
986) -> Result<Vec<ExtractedTest>, ExtractError> {
987 let parse_err = || ExtractError::Parse {
988 path: path.to_string(),
989 };
990 let mut shadow = KotlinShadow::new(source);
991 let mut parser = tree_sitter::Parser::new();
992 parser
993 .set_language(&tree_sitter_kotlin_ng::LANGUAGE.into())
994 .expect("valid kotlin grammar");
995 let mut tree = parser.parse(&shadow.shadow, None).ok_or_else(parse_err)?;
996 if tree.root_node().has_error() {
997 return Err(parse_err());
998 }
999 if retry_annotated_expression && kotlin_has_annotated_expression(tree.root_node()) {
1006 let ranges = kotlin_annotated_expression_ranges(tree.root_node());
1007 drop(tree);
1008 shadow.blank_ranges(&ranges);
1009 tree = parser.parse(&shadow.shadow, None).ok_or_else(parse_err)?;
1010 if tree.root_node().has_error() {
1011 return Err(parse_err());
1012 }
1013 }
1014 let root = tree.root_node();
1015 let mut cursor = root.walk();
1018 let unexpected = root
1019 .children(&mut cursor)
1020 .any(|n| !KOTLIN_ROOT_KINDS.contains(&n.kind()));
1021 if unexpected {
1022 return Err(parse_err());
1023 }
1024 let base = path.strip_suffix(".kt").unwrap_or(path);
1025 let mut results = Vec::new();
1026 walk_kotlin_container(root, &shadow, base, &[], &mut results).map_err(|()| parse_err())?;
1027 Ok(results)
1028}
1029
1030fn kotlin_has_annotated_expression(root: tree_sitter::Node) -> bool {
1031 let mut cursor = root.walk();
1032 let found = root
1033 .children(&mut cursor)
1034 .any(|n| n.kind() == "annotated_expression");
1035 found
1036}
1037
1038fn kotlin_annotated_expression_ranges(root: tree_sitter::Node) -> Vec<(usize, usize)> {
1047 let mut ranges = Vec::new();
1048 let mut cursor = root.walk();
1049 for expr in root.children(&mut cursor) {
1050 if expr.kind() != "annotated_expression" {
1051 continue;
1052 }
1053 let mut c2 = expr.walk();
1054 let children: Vec<tree_sitter::Node> = expr.children(&mut c2).collect();
1055 let args: Vec<_> = children
1056 .windows(2)
1057 .filter(|w| w[0].kind() == "annotation" && w[1].kind() == "parenthesized_expression")
1058 .map(|w| &w[1])
1059 .collect();
1060 let picked: Vec<_> = if args.is_empty() {
1061 children
1062 .iter()
1063 .filter(|c| c.kind() == "annotation")
1064 .collect()
1065 } else {
1066 args
1067 };
1068 ranges.extend(picked.iter().map(|n| (n.start_byte(), n.end_byte())));
1069 }
1070 ranges
1071}
1072
1073fn kotlin_ident(text: &str) -> String {
1074 text.strip_prefix('`')
1075 .and_then(|t| t.strip_suffix('`'))
1076 .unwrap_or(text)
1077 .to_string()
1078}
1079
1080fn walk_kotlin_container(
1081 container: tree_sitter::Node,
1082 shadow: &KotlinShadow,
1083 base: &str,
1084 types: &[String],
1085 results: &mut Vec<ExtractedTest>,
1086) -> Result<(), ()> {
1087 let mut cursor = container.walk();
1088 let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
1089 for (idx, node) in children.iter().enumerate() {
1090 match node.kind() {
1091 "class_declaration" | "object_declaration" | "companion_object" => {
1092 let name = match node.child_by_field_name("name") {
1093 Some(n) => match shadow.node_text(n) {
1094 Some(t) => kotlin_ident(t),
1095 None => {
1096 debug_assert!(false, "kotlin name range off a char boundary");
1097 return Err(());
1098 }
1099 },
1100 None if node.kind() == "companion_object" => "Companion".to_string(),
1101 None => continue,
1102 };
1103 let mut c = node.walk();
1104 let Some(body) = node
1105 .children(&mut c)
1106 .find(|n| matches!(n.kind(), "class_body" | "enum_class_body"))
1107 else {
1108 continue;
1109 };
1110 let mut new_types = types.to_vec();
1111 new_types.push(name);
1112 walk_kotlin_container(body, shadow, base, &new_types, results)?;
1113 }
1114 "function_declaration" => {
1115 if !kotlin_is_test_function(*node, shadow) {
1116 continue;
1117 }
1118 let Some(name_node) = node.child_by_field_name("name") else {
1119 continue;
1120 };
1121 let Some(name_text) = shadow.node_text(name_node) else {
1122 debug_assert!(false, "kotlin name range off a char boundary");
1123 return Err(());
1124 };
1125 let name = kotlin_ident(name_text);
1126 if !is_valid_test_name(&name) {
1127 continue;
1128 }
1129 let Some(start) = jvm_content_start(*node) else {
1130 continue;
1131 };
1132 let Some(content) = shadow.slice(start, node.end_byte()) else {
1133 debug_assert!(false, "kotlin content range off a char boundary");
1134 return Err(());
1135 };
1136 results.push(ExtractedTest {
1137 id: format!("{base}:{}:{name}", types.join("/")),
1138 name,
1139 markers: collect_preceding_markers_with(
1142 &children,
1143 idx,
1144 &shadow.shadow,
1145 &|n| shadow.node_text(n),
1146 &JVM_COMMENT_KINDS,
1147 &|n| shadow.node_text(n).is_some_and(is_doc_comment),
1148 ),
1149 content: content.to_string(),
1150 });
1151 }
1152 _ => {}
1153 }
1154 }
1155 Ok(())
1156}
1157
1158fn kotlin_is_test_function(func: tree_sitter::Node, shadow: &KotlinShadow) -> bool {
1159 let mut cursor = func.walk();
1160 let Some(modifiers) = func.children(&mut cursor).find(|n| n.kind() == "modifiers") else {
1161 return false;
1162 };
1163 let mut c2 = modifiers.walk();
1164 let found = modifiers
1165 .children(&mut c2)
1166 .filter(|n| n.kind() == "annotation")
1167 .any(|ann| kotlin_annotation_has_test(ann, shadow));
1168 found
1169}
1170
1171fn kotlin_annotation_has_test(node: tree_sitter::Node, shadow: &KotlinShadow) -> bool {
1174 let mut cursor = node.walk();
1175 for child in node.children(&mut cursor) {
1176 match child.kind() {
1177 "user_type" => {
1178 let mut c = child.walk();
1179 let last = child
1180 .children(&mut c)
1181 .filter(|n| matches!(n.kind(), "identifier" | "type_identifier"))
1182 .last();
1183 if let Some(last) = last {
1184 if jvm_name_is_test(shadow.node_text(last).unwrap_or("")) {
1185 return true;
1186 }
1187 }
1188 }
1189 "value_arguments" | "use_site_target" => {}
1190 _ => {
1191 if kotlin_annotation_has_test(child, shadow) {
1192 return true;
1193 }
1194 }
1195 }
1196 }
1197 false
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202 use super::*;
1203
1204 fn find<'a>(tests: &'a [ExtractedTest], name: &str) -> &'a ExtractedTest {
1205 tests
1206 .iter()
1207 .find(|t| t.name == name)
1208 .unwrap_or_else(|| panic!("test named {name} not found in {tests:?}"))
1209 }
1210
1211 #[test]
1213 fn goは直前に隣接するコメントをマーカーとして抽出する() {
1214 let source = r#"package auth
1215
1216// [FR-001]
1217func TestRejectsShortPassword(t *testing.T) {
1218 doSomething()
1219}
1220"#;
1221 let tests = extract_go_tests("internal/auth/login_test.go", source).unwrap();
1222 let t = find(&tests, "TestRejectsShortPassword");
1223 assert_eq!(t.markers, vec!["FR-001".to_string()]);
1224 }
1225
1226 #[test]
1228 fn goは空行を挟んだコメントや欠落をマーカーなしとして無視する() {
1229 let source = r#"package auth
1230
1231// [FR-002]
1232
1233func TestWithBlankLine(t *testing.T) {
1234}
1235
1236func TestNoComment(t *testing.T) {
1237}
1238"#;
1239 let tests = extract_go_tests("internal/auth/login_test.go", source).unwrap();
1240 assert!(find(&tests, "TestWithBlankLine").markers.is_empty());
1241 assert!(find(&tests, "TestNoComment").markers.is_empty());
1242 }
1243
1244 #[test]
1246 fn goのtest識別子はディレクトリと関数名を組み合わせた形式になる() {
1247 let source = "package auth\n\n// [FR-001]\nfunc TestLogin(t *testing.T) {}\n";
1248 let tests = extract_go_tests("internal/auth/login_test.go", source).unwrap();
1249 assert_eq!(find(&tests, "TestLogin").id, "internal/auth:TestLogin");
1250 }
1251
1252 #[test]
1254 fn goのtest識別子はリポジトリ直下ならドットになる() {
1255 let source = "package main\n\n// [FR-001]\nfunc TestRoot(t *testing.T) {}\n";
1256 let tests = extract_go_tests("login_test.go", source).unwrap();
1257 assert_eq!(find(&tests, "TestRoot").id, ".:TestRoot");
1258 }
1259
1260 #[test]
1262 fn rustの関数本体の変更はcontentに反映される() {
1263 let base = r#"// [FR-001]
1264#[test]
1265fn rejects_short_password() {
1266 assert!(true);
1267}
1268"#;
1269 let changed = r#"// [FR-001]
1270#[test]
1271fn rejects_short_password() {
1272 assert!(false);
1273}
1274"#;
1275 let a = find(
1276 &extract_rust_tests("src/auth.rs", base, &[]).unwrap(),
1277 "rejects_short_password",
1278 )
1279 .content
1280 .clone();
1281 let b = find(
1282 &extract_rust_tests("src/auth.rs", changed, &[]).unwrap(),
1283 "rejects_short_password",
1284 )
1285 .content
1286 .clone();
1287 assert_ne!(a, b);
1288 assert!(a.contains("fn rejects_short_password"));
1289 assert!(a.contains("assert!(true)"));
1290 }
1291
1292 #[test]
1294 fn goの関数本体の変更はcontentに反映される() {
1295 let base = "package auth\n\n// [FR-001]\nfunc TestRejectsShortPassword(t *testing.T) {\n\tassertTrue(t, true)\n}\n";
1296 let changed = "package auth\n\n// [FR-001]\nfunc TestRejectsShortPassword(t *testing.T) {\n\tassertTrue(t, false)\n}\n";
1297 let a = find(
1298 &extract_go_tests("internal/auth/login_test.go", base).unwrap(),
1299 "TestRejectsShortPassword",
1300 )
1301 .content
1302 .clone();
1303 let b = find(
1304 &extract_go_tests("internal/auth/login_test.go", changed).unwrap(),
1305 "TestRejectsShortPassword",
1306 )
1307 .content
1308 .clone();
1309 assert_ne!(a, b);
1310 assert!(a.contains("func TestRejectsShortPassword"));
1311 assert!(a.contains("assertTrue(t, true)"));
1312 }
1313
1314 #[test]
1316 fn rustのcontentは外側の属性とマーカーコメントを含まない() {
1317 let source = r#"// [FR-001]
1318#[test]
1319fn rejects_short_password() {
1320 assert!(true);
1321}
1322"#;
1323 let extracted = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1324 let t = find(&extracted, "rejects_short_password");
1325 assert!(!t.content.contains("#[test]"));
1326 assert!(!t.content.contains("[FR-001]"));
1327 assert!(t.content.contains("fn rejects_short_password"));
1328 }
1329
1330 fn rs(source: &str, extra: &[&str]) -> Vec<ExtractedTest> {
1331 let extra: Vec<String> = extra.iter().map(|s| s.to_string()).collect();
1332 extract_rust_tests("src/auth.rs", source, &extra).unwrap()
1333 }
1334
1335 fn has(tests: &[ExtractedTest], name: &str) -> bool {
1336 tests.iter().any(|t| t.name == name)
1337 }
1338
1339 #[test]
1341 fn rustは属性パスの末尾がtestならtestとして抽出する() {
1342 let source = r#"// [FR-001]
1343#[test]
1344fn plain_test() {}
1345
1346// [FR-002]
1347#[tokio::test]
1348async fn async_test() {}
1349
1350// [FR-003]
1351#[async_std::test]
1352async fn async_std_test() {}
1353
1354// [FR-004]
1355#[tokio::test(flavor = "multi_thread")]
1356async fn multi_thread_test() {}
1357
1358// [FR-005]
1359#[ test ]
1360fn spaced_test() {}
1361"#;
1362 let tests = rs(source, &[]);
1363 for name in [
1364 "plain_test",
1365 "async_test",
1366 "async_std_test",
1367 "multi_thread_test",
1368 "spaced_test",
1369 ] {
1370 assert!(has(&tests, name), "{name} should be extracted");
1371 }
1372 assert_eq!(
1373 find(&tests, "async_test").markers,
1374 vec!["FR-002".to_string()]
1375 );
1376 }
1377
1378 #[test]
1380 fn rustのrstestは追加指定がなければ対象外で指定があれば対象になる() {
1381 let source = "// [FR-001]\n#[rstest]\nfn t() {}\n";
1382 assert!(!has(&rs(source, &[]), "t"));
1383 assert!(has(&rs(source, &["rstest"]), "t"));
1384
1385 let scoped = "// [FR-001]\n#[rstest::rstest]\nfn t() {}\n";
1386 assert!(!has(&rs(scoped, &[]), "t"));
1387 assert!(has(&rs(scoped, &["rstest"]), "t"));
1388 }
1389
1390 #[test]
1392 fn rustのtest_caseとcfg_attrのtestは対象外() {
1393 let source = r#"// [FR-001]
1394#[test_case(1)]
1395fn case_test() {}
1396
1397// [FR-002]
1398#[cfg_attr(x, test)]
1399fn cfg_attr_test() {}
1400"#;
1401 let tests = rs(source, &[]);
1402 assert!(!has(&tests, "case_test"));
1403 assert!(!has(&tests, "cfg_attr_test"));
1404
1405 let tests = rs(source, &["test_case"]);
1406 assert!(has(&tests, "case_test"));
1407 assert!(!has(&tests, "cfg_attr_test"));
1408 }
1409
1410 #[test]
1412 fn rustのinner_attributeのtestはtestを作らない() {
1413 let source = "#![test]\n\n// [FR-001]\nfn helper() {}\n";
1414 assert!(rs(source, &[]).is_empty());
1415 }
1416
1417 #[test]
1419 fn rustは最初の対象属性の直前のコメントをマーカーにする() {
1420 let source = "// [FR-001]\n#[rstest]\n#[case(1)]\n#[tokio::test]\nfn t() {}\n";
1421 let tests = rs(source, &["rstest"]);
1422 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
1423 }
1424
1425 #[test]
1427 fn rustは対象属性の設定によりマーカー位置が変わる() {
1428 let source = "#[rstest]\n// [FR-001]\n#[tokio::test]\nfn t() {}\n";
1429 let without = rs(source, &[]);
1430 assert_eq!(find(&without, "t").markers, vec!["FR-001".to_string()]);
1431 let with = rs(source, &["rstest"]);
1432 assert!(find(&with, "t").markers.is_empty());
1433 }
1434
1435 #[test]
1437 fn rustは属性間や属性末尾のコメントがあってもtestとして検出しマーカーを取る() {
1438 let between = "// [FR-001]\n#[test]\n// note\n#[ignore]\nfn between() {}\n";
1439 let tests = rs(between, &[]);
1440 assert_eq!(find(&tests, "between").markers, vec!["FR-001".to_string()]);
1441
1442 let trailing = "// [FR-001]\n#[test] // trailing\n#[ignore]\nfn trailing() {}\n";
1443 let tests = rs(trailing, &[]);
1444 assert_eq!(find(&tests, "trailing").markers, vec!["FR-001".to_string()]);
1445
1446 let block = "// [FR-001]\n#[test]\n/* block */\n#[ignore]\nfn block() {}\n";
1447 assert_eq!(
1448 find(&rs(block, &[]), "block").markers,
1449 vec!["FR-001".to_string()]
1450 );
1451 }
1452
1453 #[test]
1455 fn tokioのtest属性はcontentに含まれない() {
1456 let source = "// [FR-001]\n#[tokio::test]\nasync fn t() { assert!(true); }\n";
1457 let tests = rs(source, &[]);
1458 let t = find(&tests, "t");
1459 assert!(!t.content.contains("tokio::test"));
1460 assert!(!t.content.contains("#["));
1461 assert!(t.content.contains("async fn t"));
1462 }
1463
1464 #[test]
1466 fn rustはtest属性の直前に隣接するコメントをマーカーとする() {
1467 let source = r#"// [FR-001]
1468#[test]
1469fn adjacent() {}
1470
1471// [FR-002]
1472
1473#[test]
1474fn blank_line_between() {}
1475
1476#[test]
1477fn no_comment() {}
1478"#;
1479 let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1480 assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
1481 assert!(find(&tests, "blank_line_between").markers.is_empty());
1482 assert!(find(&tests, "no_comment").markers.is_empty());
1483 }
1484
1485 #[test]
1487 fn rustのtest識別子はトップレベルなら空モジュールパスになる() {
1488 let source = "// [FR-001]\n#[test]\nfn roundtrip() {}\n";
1489 let tests = extract_rust_tests("src/lock.rs", source, &[]).unwrap();
1490 assert_eq!(find(&tests, "roundtrip").id, "src/lock::roundtrip");
1491 }
1492
1493 #[test]
1495 fn rustのtest識別子はinline_modを外側から内側の順に連結する() {
1496 let source = r#"mod tests {
1497 // [FR-001]
1498 #[test]
1499 fn roundtrip() {}
1500}
1501"#;
1502 let tests = extract_rust_tests("src/lock.rs", source, &[]).unwrap();
1503 assert_eq!(find(&tests, "roundtrip").id, "src/lock:tests:roundtrip");
1504 }
1505
1506 #[test]
1508 fn rustのtest識別子はネストしたinline_modをスラッシュで連結する() {
1509 let source = r#"mod outer {
1510 mod inner {
1511 // [FR-001]
1512 #[test]
1513 fn t() {}
1514 }
1515}
1516"#;
1517 let tests = extract_rust_tests("src/x.rs", source, &[]).unwrap();
1518 assert_eq!(find(&tests, "t").id, "src/x:outer/inner:t");
1519 }
1520
1521 #[test]
1523 fn rustは属性が間に挟まるとtestの直前のコメントをマーカーにしない() {
1524 let source = r#"// [FR-001]
1525#[cfg(feature = "x")]
1526#[test]
1527fn not_adjacent() {}
1528"#;
1529 let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1530 assert!(find(&tests, "not_adjacent").markers.is_empty());
1531 }
1532
1533 #[test]
1535 fn rustは他の属性の後ろでもtestの直前のコメントはマーカーになる() {
1536 let source = r#"#[cfg(feature = "x")]
1537// [FR-001]
1538#[test]
1539fn adjacent_after_other_attr() {}
1540"#;
1541 let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1542 assert_eq!(
1543 find(&tests, "adjacent_after_other_attr").markers,
1544 vec!["FR-001".to_string()]
1545 );
1546 }
1547
1548 #[test]
1550 fn rustはtestとfnの間にコメントがあってもtestとして検出しマーカーを採用する() {
1551 let source = r#"// [FR-001]
1552#[test]
1553// 補足
1554fn t() {}
1555"#;
1556 let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1557 let found = find(&tests, "t");
1558 assert_eq!(found.markers, vec!["FR-001".to_string()]);
1559 assert!(!found.content.contains("補足"));
1560 }
1561
1562 fn cs(source: &str) -> Vec<ExtractedTest> {
1565 extract_csharp_tests("tests/AuthTests.cs", source).unwrap()
1566 }
1567
1568 fn cs_wrap(method: &str) -> String {
1569 format!("public class AuthTests\n{{\n{method}\n}}\n")
1570 }
1571
1572 #[test]
1574 fn csharpはif_endif内のtestメソッドも検出する() {
1575 let source = "public class A {\n#if DEBUG\n // [FR-001]\n [Fact]\n public void T() { }\n#endif\n}\n";
1576 let tests = extract_csharp_tests("tests/A.cs", source).unwrap();
1577 assert_eq!(tests.len(), 1);
1578 assert_eq!(tests[0].id, "tests/A:A:T");
1579 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
1580 }
1581
1582 #[test]
1584 fn csharpは7種類の属性をそれぞれtestとして検出する() {
1585 for attr in [
1586 "Fact",
1587 "Theory",
1588 "Test",
1589 "TestCase",
1590 "TestCaseSource",
1591 "TestMethod",
1592 "DataTestMethod",
1593 ] {
1594 let source = cs_wrap(&format!(" [{attr}]\n public void T() {{ }}"));
1595 let tests = cs(&source);
1596 assert_eq!(tests.len(), 1, "attribute {attr} should be detected");
1597 assert_eq!(tests[0].name, "T");
1598 }
1599 }
1600
1601 #[test]
1603 fn csharpはattribute接尾辞付きや無関係な属性のみのメソッドを検出しない() {
1604 let source = cs_wrap(
1605 " [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() { }",
1606 );
1607 assert!(cs(&source).is_empty());
1608 }
1609
1610 #[test]
1612 fn csharpは引数付きの属性も検出する() {
1613 let source = cs_wrap(" [Fact(Skip = \"x\")]\n public void T() { }");
1614 assert_eq!(cs(&source).len(), 1);
1615 }
1616
1617 #[test]
1619 fn csharpは名前空間修飾された属性も検出する() {
1620 let source = cs_wrap(
1621 " [Xunit.Fact]\n public void A() { }\n\n [global::Xunit.Fact]\n public void B() { }",
1622 );
1623 let tests = cs(&source);
1624 assert_eq!(tests.len(), 2);
1625 find(&tests, "A");
1626 find(&tests, "B");
1627 }
1628
1629 #[test]
1631 fn csharpはtarget指定付きの属性も検出する() {
1632 let source = cs_wrap(" [method: Fact]\n public void T() { }");
1633 assert_eq!(cs(&source).len(), 1);
1634 }
1635
1636 #[test]
1638 fn csharpは同一属性リスト内の他の属性と並んだ属性も検出する() {
1639 let source = cs_wrap(" [Trait(\"a\", \"b\"), Fact]\n public void T() { }");
1640 assert_eq!(cs(&source).len(), 1);
1641 }
1642
1643 #[test]
1645 fn csharpは複数の属性リストの順序に関わらず検出する() {
1646 let source = cs_wrap(
1647 " [Theory]\n [InlineData(1)]\n public void A(int x) { }\n\n [InlineData(1)]\n [Theory]\n public void B(int x) { }",
1648 );
1649 let tests = cs(&source);
1650 assert_eq!(tests.len(), 2);
1651 find(&tests, "A");
1652 find(&tests, "B");
1653 }
1654
1655 #[test]
1657 fn csharpは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
1658 let source = cs_wrap(
1659 " // [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() { }",
1660 );
1661 let tests = cs(&source);
1662 assert_eq!(find(&tests, "Adjacent").markers, vec!["FR-001".to_string()]);
1663 assert!(find(&tests, "Blank").markers.is_empty());
1664 assert!(find(&tests, "None").markers.is_empty());
1665 }
1666
1667 #[test]
1669 fn csharpはドキュメントコメントとブロックコメントもマーカーにできる() {
1670 let source = cs_wrap(
1671 " /// [FR-001]\n [Fact]\n public void Doc() { }\n\n /* [FR-002] */\n [Fact]\n public void Block() { }",
1672 );
1673 let tests = cs(&source);
1674 assert_eq!(find(&tests, "Doc").markers, vec!["FR-001".to_string()]);
1675 assert_eq!(find(&tests, "Block").markers, vec!["FR-002".to_string()]);
1676 }
1677
1678 #[test]
1680 fn csharpは属性と署名の間のコメントをマーカーにせずcontentにも含めない() {
1681 let source = cs_wrap(
1682 " [Fact]\n // [FR-001]\n public void T() { }\n\n [Fact]\n [Trait(\"a\", \"b\")]\n // 補足\n public void U() { }",
1683 );
1684 let tests = cs(&source);
1685 let t = find(&tests, "T");
1686 assert!(t.markers.is_empty());
1687 assert!(!t.content.contains("FR-001"));
1688 let u = find(&tests, "U");
1689 assert!(!u.content.contains("補足"));
1690 }
1691
1692 #[test]
1694 fn csharpは属性リストの間のコメントをマーカーにせずcontentにも含めない() {
1695 let source = cs_wrap(
1696 " [Theory]\n // [FR-001]\n [InlineData(1)]\n public void T(int x) { }",
1697 );
1698 let tests = cs(&source);
1699 let t = find(&tests, "T");
1700 assert!(t.markers.is_empty());
1701 assert!(!t.content.contains("FR-001"));
1702 }
1703
1704 #[test]
1706 fn csharpのtest識別子はトップレベルclassならファイルパスと型名とメソッド名になる() {
1707 let source =
1708 "public class AuthTests\n{\n [Fact]\n public void RejectsShort() { }\n}\n";
1709 let tests = cs(source);
1710 assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:RejectsShort");
1711 }
1712
1713 #[test]
1715 fn csharpのtest識別子はネストした型を外側から内側へスラッシュで連結する() {
1716 let source = "public class Outer\n{\n public class Inner\n {\n [Test]\n public void T3() { }\n }\n}\n";
1717 let tests = cs(source);
1718 assert_eq!(tests[0].id, "tests/AuthTests:Outer/Inner:T3");
1719 }
1720
1721 #[test]
1723 fn csharpのtest識別子はstruct_record_interfaceの中でも型名を辿る() {
1724 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";
1725 let tests = cs(source);
1726 assert_eq!(find(&tests, "A").id, "tests/AuthTests:S:A");
1727 assert_eq!(find(&tests, "B").id, "tests/AuthTests:R:B");
1728 assert_eq!(find(&tests, "C").id, "tests/AuthTests:I:C");
1729 }
1730
1731 #[test]
1733 fn csharpのtest識別子はblock_namespaceを含まない() {
1734 let source = "namespace Foo.Bar\n{\n public class AuthTests\n {\n [Fact]\n public void T() { }\n }\n}\n";
1735 let tests = cs(source);
1736 assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:T");
1737 }
1738
1739 #[test]
1741 fn csharpのtest識別子はfile_scoped_namespaceを含まない() {
1742 let source =
1743 "namespace Foo;\n\npublic class AuthTests\n{\n [Fact]\n public void T() { }\n}\n";
1744 let tests = cs(source);
1745 assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:T");
1746 }
1747
1748 #[test]
1750 fn csharpのcontentは属性を含まず修飾子から始まる() {
1751 let source = cs_wrap(
1752 " // [FR-001]\n [Fact]\n [Trait(\"a\", \"b\")]\n public void T() { Assert.True(true); }",
1753 );
1754 let tests = cs(&source);
1755 let c = &tests[0].content;
1756 assert!(c.starts_with("public void T()"), "content = {c:?}");
1757 assert!(c.ends_with('}'));
1758 assert!(!c.contains("Fact"));
1759 assert!(!c.contains("Trait"));
1760 assert!(!c.contains("FR-001"));
1761 }
1762
1763 #[test]
1765 fn csharpの本体変更はcontentに反映される() {
1766 let a = cs(&cs_wrap(
1767 " [Fact]\n public void T() { Assert.True(true); }",
1768 ));
1769 let b = cs(&cs_wrap(
1770 " [Fact]\n public void T() { Assert.True(false); }",
1771 ));
1772 assert_ne!(a[0].content, b[0].content);
1773 }
1774
1775 #[test]
1777 fn csharpの属性だけの変更はcontentを変えない() {
1778 let a = cs(&cs_wrap(
1779 " [Fact]\n public void T() { Assert.True(true); }",
1780 ));
1781 let b = cs(&cs_wrap(
1782 " [Fact(Skip = \"x\")]\n [Trait(\"a\", \"b\")]\n public void T() { Assert.True(true); }",
1783 ));
1784 assert_eq!(a[0].content, b[0].content);
1785 }
1786
1787 #[test]
1789 fn csharpは構文エラーのソースでエラーを返す() {
1790 let source = "public class A {\n [Fact]\n public void T( {\n}\n";
1791 assert!(extract_csharp_tests("tests/A.cs", source).is_err());
1792 }
1793
1794 fn ts(source: &str) -> Vec<ExtractedTest> {
1797 extract_typescript_tests("src/a.test.ts", source).unwrap()
1798 }
1799
1800 #[test]
1802 fn typescriptはtestとitの呼び出しを検出する() {
1803 let tests = ts("test(\"a\", () => {});\nit(\"b\", () => {});\n");
1804 assert_eq!(tests.len(), 2);
1805 find(&tests, "a");
1806 find(&tests, "b");
1807 }
1808
1809 #[test]
1811 fn typescriptは許可された修飾子付きの呼び出しを検出する() {
1812 let source = "it.skip(\"a\", () => {});\ntest.only(\"b\", () => {});\ntest.concurrent.only(\"c\", () => {});\ntest.todo(\"d\", () => {});\n";
1813 let tests = ts(source);
1814 assert_eq!(tests.len(), 4);
1815 for n in ["a", "b", "c", "d"] {
1816 find(&tests, n);
1817 }
1818 }
1819
1820 #[test]
1822 fn typescriptはdescribe_skipの中のtestも検出する() {
1823 let tests = ts("describe.skip(\"d\", () => {\n it(\"a\", () => {});\n});\n");
1824 assert_eq!(tests.len(), 1);
1825 assert_eq!(tests[0].id, "src/a.test:d:a");
1826 }
1827
1828 #[test]
1830 fn typescriptはfunction式のコールバックも検出する() {
1831 let source = "describe(\"d\", function () {\n it(\"a\", function () {});\n});\n";
1832 let tests = ts(source);
1833 assert_eq!(tests.len(), 1);
1834 assert_eq!(tests[0].id, "src/a.test:d:a");
1835 }
1836
1837 #[test]
1839 fn typescriptはtsxファイルのjsxを含むソースも解析できる() {
1840 let source = "it(\"renders\", () => {\n const x = <div className=\"a\">hi</div>;\n expect(x).toBeTruthy();\n});\n";
1841 let tests = extract_typescript_tests("src/a.test.tsx", source).unwrap();
1842 assert_eq!(tests.len(), 1);
1843 assert_eq!(tests[0].id, "src/a.test::renders");
1844 }
1845
1846 #[test]
1848 fn typescriptは許可外の修飾子や呼び出し結果の呼び出しを検出しない() {
1849 let source = r#"test.each([1, 2])("each %i", (n) => {});
1850test.skipIf(c)("skipif", () => {});
1851test.extend({ a: 1 });
1852test.describe("desc", () => {});
1853test.runIf(c)("runif", () => {});
1854it?.skip("optional", () => {});
1855"#;
1856 assert!(ts(source).is_empty());
1857 }
1858
1859 #[test]
1861 fn typescriptはexport_defaultやskipif_describeやforループ内のitを検出しない() {
1862 let source = r#"export default describe("d", () => { it("y", () => {}); })
1863describe.skipIf(c)("d", () => { it("x", () => {}); });
1864for (const n of [1]) {
1865 it("loop", () => {});
1866}
1867"#;
1868 assert!(ts(source).is_empty());
1869 }
1870
1871 #[test]
1873 fn typescriptは許可された修飾子を検出し走査する() {
1874 let source = r#"test.fails("f", () => {});
1875it.sequential("s", () => {});
1876describe.shuffle("d", () => {
1877 it("inner", () => {});
1878});
1879"#;
1880 let tests = ts(source);
1881 assert_eq!(tests.len(), 3);
1882 find(&tests, "f");
1883 find(&tests, "s");
1884 assert_eq!(find(&tests, "inner").id, "src/a.test:d:inner");
1885 }
1886
1887 #[test]
1889 fn typescriptはdescribe_eachの中のtestを検出しない() {
1890 let source = "describe.each([1])(\"d %i\", (n) => {\n it(\"a\", () => {});\n});\n";
1891 assert!(ts(source).is_empty());
1892 }
1893
1894 #[test]
1896 fn typescriptはawaitなど式文直下でない呼び出しを検出しない() {
1897 let source = "await it(\"a\", () => {});\nvoid it(\"b\", () => {});\n";
1898 assert!(ts(source).is_empty());
1899 }
1900
1901 #[test]
1903 fn typescriptは第1引数が文字列リテラルでない呼び出しを検出しない() {
1904 let source = "const name = \"x\";\nit(name, () => {});\nit(\"a\" + \"b\", () => {});\n";
1905 assert!(ts(source).is_empty());
1906 }
1907
1908 #[test]
1910 fn typescriptはhelper関数やtest本体やif文の中のitを検出しない() {
1911 let source = r#"function helper() {
1912 it("in helper", () => {});
1913}
1914test("outer", () => {
1915 it("in test", () => {});
1916});
1917if (cond) {
1918 it("in if", () => {});
1919}
1920"#;
1921 let tests = ts(source);
1922 assert_eq!(tests.len(), 1);
1923 assert_eq!(tests[0].name, "outer");
1924 }
1925
1926 #[test]
1928 fn typescriptは置換なしのテンプレート文字列を対象にし置換ありを無視する() {
1929 let source = "it(`plain`, () => {});\nit(`with ${x}`, () => {});\n";
1930 let tests = ts(source);
1931 assert_eq!(tests.len(), 1);
1932 assert_eq!(tests[0].name, "plain");
1933 }
1934
1935 #[test]
1937 fn typescriptは空文字名と改行を含む名前を無視する() {
1938 let source = "it(\"\", () => {});\nit(`line1\nline2`, () => {});\nit(\"ok\", () => {});\n";
1939 let tests = ts(source);
1940 assert_eq!(tests.len(), 1);
1941 assert_eq!(tests[0].name, "ok");
1942 }
1943
1944 #[test]
1946 fn typescriptは行区切り文字を含む名前を無視する() {
1947 let source = "it(\"a\u{2028}b\", () => {});\nit(\"ok\", () => {});\n";
1948 let tests = ts(source);
1949 assert_eq!(tests.len(), 1);
1950 assert_eq!(tests[0].name, "ok");
1951 }
1952
1953 #[test]
1955 fn typescriptはエスケープを解釈せずソース表記のまま名前にする() {
1956 let source = "it('it\\'s', () => {});\n";
1957 let tests = ts(source);
1958 assert_eq!(tests.len(), 1);
1959 assert_eq!(tests[0].name, "it\\'s");
1960 assert_eq!(tests[0].id, "src/a.test::it\\'s");
1961 }
1962
1963 #[test]
1965 fn typescriptのtest識別子はdescribeの外なら空のdescribeパスになる() {
1966 let tests = ts("it(\"top\", () => {});\n");
1967 assert_eq!(tests[0].id, "src/a.test::top");
1968 }
1969
1970 #[test]
1972 fn typescriptのtest識別子はdescribeを外側から内側へスラッシュで連結する() {
1973 let source = "describe(\"auth\", () => {\n it(\"one\", () => {});\n describe(\"inner\", () => {\n it(\"two\", () => {});\n });\n});\n";
1974 let tests = ts(source);
1975 assert_eq!(find(&tests, "one").id, "src/a.test:auth:one");
1976 assert_eq!(find(&tests, "two").id, "src/a.test:auth/inner:two");
1977 }
1978
1979 #[test]
1981 fn typescriptのtest識別子は対象拡張子をすべて除去しtestやspecは残す() {
1982 let src = "it(\"t\", () => {});\n";
1983 for (path, id) in [
1984 ("src/a.test.ts", "src/a.test::t"),
1985 ("src/a.spec.tsx", "src/a.spec::t"),
1986 ("src/a.test.mjs", "src/a.test::t"),
1987 ("src/a.spec.cts", "src/a.spec::t"),
1988 ("src/a.test.jsx", "src/a.test::t"),
1989 ("src/a.test.js", "src/a.test::t"),
1990 ("src/a.spec.cjs", "src/a.spec::t"),
1991 ("src/a.test.mts", "src/a.test::t"),
1992 ] {
1993 let tests = extract_typescript_tests(path, src).unwrap();
1994 assert_eq!(tests[0].id, id, "{path}");
1995 }
1996 }
1997
1998 #[test]
2000 fn typescriptのtest識別子は対象外の拡張子を除去しない() {
2001 let tests =
2002 extract_typescript_tests("src/a.test.ts.bak", "it(\"t\", () => {});\n").unwrap();
2003 assert_eq!(tests[0].id, "src/a.test.ts.bak::t");
2004 }
2005
2006 #[test]
2008 fn typescriptはjs系拡張子のファイルからtestを検出する() {
2009 for path in [
2010 "src/a.test.js",
2011 "src/a.spec.mjs",
2012 "src/a.test.cjs",
2013 "src/a.spec.mts",
2014 "src/a.test.cts",
2015 ] {
2016 let tests = extract_typescript_tests(path, "it(\"t\", () => {});\n").unwrap();
2017 assert_eq!(tests.len(), 1, "{path}");
2018 }
2019 }
2020
2021 #[test]
2023 fn typescriptはts系拡張子でtsx文法が拒否する角括弧キャストを解析できる() {
2024 let source = "it(\"t\", () => {\n const n = <number>x;\n});\n";
2025 for path in ["src/a.test.ts", "src/a.test.mts", "src/a.spec.cts"] {
2026 let tests = extract_typescript_tests(path, source).unwrap();
2027 assert_eq!(tests.len(), 1, "{path}");
2028 }
2029 }
2030
2031 #[test]
2033 fn typescriptはjsxとjsファイルのjsxを含むソースも解析できる() {
2034 let source = "it(\"renders\", () => {\n render(<App />);\n});\n";
2035 for path in ["src/a.test.jsx", "src/a.test.js"] {
2036 let tests = extract_typescript_tests(path, source).unwrap();
2037 assert_eq!(tests.len(), 1, "{path}");
2038 assert_eq!(tests[0].id, "src/a.test::renders", "{path}");
2039 }
2040 }
2041
2042 #[test]
2044 fn typescriptは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
2045 let source = "// [FR-001]\nit(\"adjacent\", () => {});\n\n// [FR-002]\n\nit(\"blank\", () => {});\n\nit(\"none\", () => {});\n";
2046 let tests = ts(source);
2047 assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
2048 assert!(find(&tests, "blank").markers.is_empty());
2049 assert!(find(&tests, "none").markers.is_empty());
2050 }
2051
2052 #[test]
2054 fn typescriptはdescribe内のtestでも直前のコメントをマーカーにする() {
2055 let source = "describe(\"d\", () => {\n // [FR-001]\n it(\"a\", () => {});\n});\n";
2056 let tests = ts(source);
2057 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
2058 }
2059
2060 #[test]
2062 fn typescriptのcontentは呼び出し文全体でマーカーコメントを含まない() {
2063 let source = "// [FR-001]\nit(\"a\", () => {\n expect(1).toBe(1);\n});\n";
2064 let tests = ts(source);
2065 let c = &tests[0].content;
2066 assert!(c.starts_with("it(\"a\""), "content = {c:?}");
2067 assert!(c.ends_with(");"), "content = {c:?}");
2068 assert!(!c.contains("FR-001"));
2069 }
2070
2071 #[test]
2073 fn typescriptの本体変更はcontentに反映される() {
2074 let a = ts("it(\"a\", () => { expect(1).toBe(1); });\n");
2075 let b = ts("it(\"a\", () => { expect(1).toBe(2); });\n");
2076 assert_ne!(a[0].content, b[0].content);
2077 }
2078
2079 #[test]
2081 fn typescriptは構文エラーのソースでエラーを返す() {
2082 let source = "it(\"a\", () => {\n";
2083 assert!(extract_typescript_tests("src/a.test.ts", source).is_err());
2084 }
2085
2086 fn jv(source: &str) -> Vec<ExtractedTest> {
2089 extract_java_tests("tests/AuthTests.java", source).unwrap()
2090 }
2091
2092 fn jv_wrap(body: &str) -> String {
2093 format!("public class AuthTests {{\n{body}\n}}\n")
2094 }
2095
2096 #[test]
2098 fn javaは5種類の注釈をそれぞれtestとして検出する() {
2099 for ann in [
2100 "Test",
2101 "ParameterizedTest",
2102 "RepeatedTest",
2103 "TestFactory",
2104 "TestTemplate",
2105 ] {
2106 let source = jv_wrap(&format!(" @{ann}\n void t() {{ }}"));
2107 let tests = jv(&source);
2108 assert_eq!(tests.len(), 1, "annotation {ann} should be detected");
2109 assert_eq!(tests[0].name, "t");
2110 }
2111 }
2112
2113 #[test]
2115 fn javaは修飾名の注釈と引数付きの注釈も検出する() {
2116 let source = jv_wrap(
2117 " @org.junit.jupiter.api.Test\n void a() { }\n\n @Test(timeout = 1)\n void b() { }",
2118 );
2119 let tests = jv(&source);
2120 assert_eq!(tests.len(), 2);
2121 find(&tests, "a");
2122 find(&tests, "b");
2123 }
2124
2125 #[test]
2127 fn javaは無関係な注釈のみのメソッドを検出しない() {
2128 let source = jv_wrap(
2129 " @Disabled\n void a() { }\n\n void b() { }\n\n @Tag(\"x\")\n void c() { }",
2130 );
2131 assert!(jv(&source).is_empty());
2132 }
2133
2134 #[test]
2136 fn javaは修飾子の後ろに書かれた注釈も検出する() {
2137 let source = jv_wrap(" public @Test void t() { }");
2138 let tests = jv(&source);
2139 assert_eq!(tests.len(), 1);
2140 assert!(tests[0].content.contains("@Test"), "{:?}", tests[0].content);
2141 }
2142
2143 #[test]
2145 fn javaは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
2146 let source = jv_wrap(
2147 " // [FR-001]\n @Test\n void adjacent() { }\n\n // [FR-002]\n\n @Test\n void blank() { }\n\n @Test\n void none() { }",
2148 );
2149 let tests = jv(&source);
2150 assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
2151 assert!(find(&tests, "blank").markers.is_empty());
2152 assert!(find(&tests, "none").markers.is_empty());
2153 }
2154
2155 #[test]
2157 fn javaはブロックコメントをマーカーにできる() {
2158 let source = jv_wrap(" /* [FR-002] */\n @Test\n void t() { }");
2159 assert_eq!(jv(&source)[0].markers, vec!["FR-002".to_string()]);
2160 }
2161
2162 #[test]
2164 fn javaはjavadocコメントをマーカーにしない() {
2165 let source = jv_wrap(" /** [FR-001] */\n @Test\n void t() { }");
2166 assert!(jv(&source)[0].markers.is_empty());
2167 }
2168
2169 #[test]
2171 fn javaは注釈と署名の間のコメントをマーカーにせずcontentにも含めない() {
2172 let source = jv_wrap(" @Test\n // [FR-001]\n // 補足\n void t() { }");
2173 let tests = jv(&source);
2174 assert!(tests[0].markers.is_empty());
2175 assert!(!tests[0].content.contains("FR-001"));
2176 assert!(!tests[0].content.contains("補足"));
2177 }
2178
2179 #[test]
2181 fn javaのtest識別子はclassならファイルパスと型名とメソッド名になる() {
2182 let tests = jv(&jv_wrap(" @Test\n void rejectsShort() { }"));
2183 assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:rejectsShort");
2184 }
2185
2186 #[test]
2188 fn javaのtest識別子はネストした型を外側から内側へスラッシュで連結する() {
2189 let source = "public class Outer {\n @Nested\n class Inner {\n @Test\n void t() { }\n }\n}\n";
2190 let tests = jv(source);
2191 assert_eq!(tests[0].id, "tests/AuthTests:Outer/Inner:t");
2192 }
2193
2194 #[test]
2196 fn javaのtest識別子はrecord_interface_enumの中でも型名を辿る() {
2197 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";
2198 let tests = jv(source);
2199 assert_eq!(find(&tests, "a").id, "tests/AuthTests:R:a");
2200 assert_eq!(find(&tests, "b").id, "tests/AuthTests:I:b");
2201 assert_eq!(find(&tests, "c").id, "tests/AuthTests:E:c");
2202 }
2203
2204 #[test]
2206 fn javaのtest識別子はpackageを含まない() {
2207 let source =
2208 "package com.example;\n\npublic class AuthTests {\n @Test\n void t() { }\n}\n";
2209 assert_eq!(jv(source)[0].id, "tests/AuthTests:AuthTests:t");
2210 }
2211
2212 #[test]
2214 fn javaはトップレベルメソッドを対象外にする() {
2215 let source = "@Test\nvoid t() { }\n";
2216 let tests = extract_java_tests("tests/AuthTests.java", source).unwrap();
2217 assert!(tests.is_empty());
2218 let source = "class A {\n @Test\n void u() { }\n}\n";
2220 assert_eq!(jv(source).len(), 1);
2221 }
2222
2223 #[test]
2225 fn javaは匿名クラスとenum定数本体の中を対象外にする() {
2226 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";
2227 let tests = jv(source);
2228 let names: Vec<&str> = tests.iter().map(|t| t.name.as_str()).collect();
2229 assert_eq!(names, vec!["real", "outside"], "{names:?}");
2230 }
2231
2232 #[test]
2234 fn javaのcontentは注釈を含まず修飾子から始まる() {
2235 let source = jv_wrap(
2236 " // [FR-001]\n @Test\n @Tag(\"a\")\n public void t() { assertTrue(true); }",
2237 );
2238 let c = &jv(&source)[0].content;
2239 assert!(c.starts_with("public void t()"), "content = {c:?}");
2240 assert!(c.ends_with('}'));
2241 assert!(!c.contains("@Test"));
2242 assert!(!c.contains("@Tag"));
2243 assert!(!c.contains("FR-001"));
2244 }
2245
2246 #[test]
2248 fn javaのcontentは型パラメータ付きでも修飾子から始まる() {
2249 let source = jv_wrap(" @Test\n static <T> void t() { }");
2250 let c = &jv(&source)[0].content;
2251 assert!(c.starts_with("static <T> void t()"), "content = {c:?}");
2252 }
2253
2254 #[test]
2256 fn javaのcontentは修飾子なしなら型パラメータから始まる() {
2257 let source = jv_wrap(" @Test\n <T> void t() { }");
2258 let c = &jv(&source)[0].content;
2259 assert!(c.starts_with("<T> void t()"), "content = {c:?}");
2260 }
2261
2262 #[test]
2264 fn javaの本体変更はcontentに反映される() {
2265 let a = jv(&jv_wrap(" @Test\n void t() { assertTrue(true); }"));
2266 let b = jv(&jv_wrap(" @Test\n void t() { assertTrue(false); }"));
2267 assert_ne!(a[0].content, b[0].content);
2268 }
2269
2270 #[test]
2272 fn javaの注釈だけの変更はcontentを変えない() {
2273 let a = jv(&jv_wrap(" @Test\n void t() { assertTrue(true); }"));
2274 let b = jv(&jv_wrap(
2275 " @Test(timeout = 1)\n @Tag(\"a\")\n void t() { assertTrue(true); }",
2276 ));
2277 assert_eq!(a[0].content, b[0].content);
2278 }
2279
2280 #[test]
2282 fn javaは構文エラーのソースでエラーを返す() {
2283 let source = "public class A {\n @Test\n void t( {\n}\n";
2284 assert!(extract_java_tests("tests/A.java", source).is_err());
2285 }
2286
2287 fn kt(source: &str) -> Vec<ExtractedTest> {
2290 extract_kotlin_tests("tests/AuthTest.kt", source).unwrap()
2291 }
2292
2293 fn kt_wrap(body: &str) -> String {
2294 format!("class AuthTest {{\n{body}\n}}\n")
2295 }
2296
2297 #[test]
2299 fn kotlinは5種類の注釈をそれぞれtestとして検出する() {
2300 for ann in [
2301 "Test",
2302 "ParameterizedTest",
2303 "RepeatedTest",
2304 "TestFactory",
2305 "TestTemplate",
2306 ] {
2307 let source = kt_wrap(&format!(" @{ann}\n fun t() {{ }}"));
2308 let tests = kt(&source);
2309 assert_eq!(tests.len(), 1, "annotation {ann} should be detected");
2310 assert_eq!(tests[0].name, "t");
2311 }
2312 }
2313
2314 #[test]
2316 fn kotlinは修飾名と複数注釈リストとuse_site_target付きの注釈も検出する() {
2317 let source = kt_wrap(
2318 " @org.junit.jupiter.api.Test\n fun a() { }\n\n @[Test Tag(\"a\")]\n fun b() { }\n\n @get:Test\n fun c() { }",
2319 );
2320 let tests = kt(&source);
2321 assert_eq!(tests.len(), 3, "{tests:?}");
2322 find(&tests, "a");
2323 find(&tests, "b");
2324 find(&tests, "c");
2325 }
2326
2327 #[test]
2329 fn kotlinは無関係な注釈のみの関数を検出しない() {
2330 let source = kt_wrap(" @Disabled\n fun a() { }\n\n fun b() { }");
2331 assert!(kt(&source).is_empty());
2332 }
2333
2334 #[test]
2336 fn kotlinは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
2337 let source = kt_wrap(
2338 " // [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() { }",
2339 );
2340 let tests = kt(&source);
2341 assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
2342 assert!(find(&tests, "blank").markers.is_empty());
2343 assert_eq!(find(&tests, "block").markers, vec!["FR-003".to_string()]);
2344 }
2345
2346 #[test]
2348 fn kotlinはkdocコメントをマーカーにしない() {
2349 let source = kt_wrap(" /** [Foo] */\n @Test\n fun t() { }");
2350 assert!(kt(&source)[0].markers.is_empty());
2351 }
2352
2353 #[test]
2355 fn kotlinは注釈と署名の間のコメントをマーカーにせずcontentにも含めない() {
2356 let source = kt_wrap(
2357 " @Test // [FR-001]\n fun a() { }\n\n @Test\n // [FR-002]\n fun b() { }",
2358 );
2359 let tests = kt(&source);
2360 for name in ["a", "b"] {
2361 let t = find(&tests, name);
2362 assert!(t.markers.is_empty(), "{name}: {:?}", t.markers);
2363 assert!(!t.content.contains("FR-00"), "{name}: {:?}", t.content);
2364 }
2365 }
2366
2367 #[test]
2369 fn kotlinのバッククォート名はバッククォートを除いた名前になる() {
2370 let source = kt_wrap(" @Test\n fun `一覧 取得 は 200`() { }");
2371 let tests = kt(&source);
2372 assert_eq!(tests[0].name, "一覧 取得 は 200");
2373 assert_eq!(tests[0].id, "tests/AuthTest:AuthTest:一覧 取得 は 200");
2374 }
2375
2376 #[test]
2378 fn kotlinのtest識別子はネストした型を外側から内側へスラッシュで連結する() {
2379 let source = "class AuthTest {\n @Nested\n inner class Inner {\n @Test\n fun t() { }\n }\n}\n";
2380 assert_eq!(kt(source)[0].id, "tests/AuthTest:AuthTest/Inner:t");
2381 }
2382
2383 #[test]
2385 fn kotlinのtest識別子はobjectとcompanion_objectを辿る() {
2386 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";
2387 let tests = kt(source);
2390 assert_eq!(find(&tests, "a").id, "tests/AuthTest:O:a");
2391 assert_eq!(find(&tests, "b").id, "tests/AuthTest:C/Companion:b");
2392 assert_eq!(find(&tests, "c").id, "tests/AuthTest:C/Factory:c");
2393 }
2394
2395 #[test]
2397 fn kotlinのトップレベル関数は型部分が空の識別子になる() {
2398 let source = "@Test\nfun top() { }\n";
2399 let tests = kt(source);
2400 assert_eq!(tests.len(), 1);
2401 assert_eq!(tests[0].id, "tests/AuthTest::top");
2402 }
2403
2404 #[test]
2406 fn kotlinのtest識別子はpackageを含まない() {
2407 let source = "package com.example\n\nclass AuthTest {\n @Test\n fun t() { }\n}\n";
2408 assert_eq!(kt(source)[0].id, "tests/AuthTest:AuthTest:t");
2409 }
2410
2411 #[test]
2413 fn kotlinはコンストラクタ注釈付きの主コンストラクタを持つクラスでもtestを取れる() {
2414 let source = "@SpringBootTest\nclass X @Autowired constructor(private val a: Int) : Base(a) {\n @Test\n fun `日本語名`() { }\n}\n";
2415 let tests = kt(source);
2416 assert_eq!(tests.len(), 1, "{tests:?}");
2417 assert_eq!(tests[0].id, "tests/AuthTest:X:日本語名");
2418 }
2419
2420 #[test]
2422 fn kotlinは関数本体内のローカル関数や匿名objectやenum定数本体やgetterを対象外にする() {
2423 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";
2424 let tests = kt(source);
2425 let names: Vec<&str> = tests.iter().map(|t| t.name.as_str()).collect();
2426 assert_eq!(names, vec!["real", "outside"], "{names:?}");
2427 }
2428
2429 #[test]
2431 fn kotlinは漢字の関数名と型名のtestを抽出し識別子とcontentが元の文字を保つ() {
2432 let source = "class AuthTest {\n @Nested\n inner class 一覧取得 {\n @Test\n fun 取得() { println(\"漢字\") }\n }\n}\n";
2433 let tests = kt(source);
2434 assert_eq!(tests.len(), 1, "{tests:?}");
2435 assert_eq!(tests[0].id, "tests/AuthTest:AuthTest/一覧取得:取得");
2436 assert_eq!(tests[0].name, "取得");
2437 assert!(tests[0].content.contains("取得"));
2438 assert!(tests[0].content.contains("漢字"));
2439 assert!(!tests[0].content.contains('x'), "{:?}", tests[0].content);
2440 }
2441
2442 #[test]
2444 fn kotlinはカタカナとハングルの関数名も抽出する() {
2445 let source = kt_wrap(" @Test\n fun テスト() { }\n\n @Test\n fun 테스트() { }");
2446 let tests = kt(&source);
2447 assert_eq!(tests.len(), 2, "{tests:?}");
2448 find(&tests, "テスト");
2449 find(&tests, "테스트");
2450 }
2451
2452 #[test]
2454 fn kotlinは非asciiの文字リテラルを含むファイルも解析できる() {
2455 let source =
2456 kt_wrap(" @Test\n fun t() {\n val c = 'あ'\n val d = '漢'\n }");
2457 let tests = kt(&source);
2458 assert_eq!(tests.len(), 1);
2459 assert!(tests[0].content.contains("'あ'"));
2460 }
2461
2462 #[test]
2464 fn kotlinはbom付きファイルも解析できる() {
2465 let source = format!("\u{feff}{}", kt_wrap(" @Test\n fun t() { }"));
2466 let tests = kt(&source);
2467 assert_eq!(tests.len(), 1);
2468 assert_eq!(tests[0].name, "t");
2469 }
2470
2471 #[test]
2473 fn kotlinのcontentは注釈を含まず修飾子から始まる() {
2474 let source = kt_wrap(
2475 " // [FR-001]\n @Test\n internal suspend fun t() { assertTrue(true) }",
2476 );
2477 let c = &kt(&source)[0].content;
2478 assert!(c.starts_with("internal suspend fun t()"), "content = {c:?}");
2479 assert!(c.ends_with('}'));
2480 assert!(!c.contains("@Test"));
2481 assert!(!c.contains("FR-001"));
2482 }
2483
2484 #[test]
2486 fn kotlinのcontentは注釈のみならfunから始まる() {
2487 let source = kt_wrap(" @Test\n @Tag(\"a\")\n fun t() { }");
2488 let c = &kt(&source)[0].content;
2489 assert!(c.starts_with("fun t()"), "content = {c:?}");
2490 }
2491
2492 #[test]
2494 fn kotlinの修飾子の後ろに書かれた注釈はcontentに含まれる() {
2495 let source = kt_wrap(" internal @Test fun t() { }");
2496 let tests = kt(&source);
2497 assert_eq!(tests.len(), 1);
2498 assert!(
2499 tests[0].content.starts_with("internal"),
2500 "{:?}",
2501 tests[0].content
2502 );
2503 assert!(tests[0].content.contains("@Test"), "{:?}", tests[0].content);
2504 }
2505
2506 #[test]
2508 fn kotlinの式本体は終端までcontentに含まれる() {
2509 let source = kt_wrap(" @Test\n fun t() = assertTrue(computeSomething())");
2510 let c = &kt(&source)[0].content;
2511 assert!(c.ends_with("computeSomething())"), "content = {c:?}");
2512 }
2513
2514 #[test]
2516 fn kotlinの本体変更はcontentに反映される() {
2517 let a = kt(&kt_wrap(" @Test\n fun t() { assertTrue(true) }"));
2518 let b = kt(&kt_wrap(" @Test\n fun t() { assertTrue(false) }"));
2519 assert_ne!(a[0].content, b[0].content);
2520 }
2521
2522 #[test]
2524 fn kotlinの注釈だけの変更はcontentを変えない() {
2525 let a = kt(&kt_wrap(" @Test\n fun t() { assertTrue(true) }"));
2526 let b = kt(&kt_wrap(
2527 " @Test\n @Tag(\"a\")\n fun t() { assertTrue(true) }",
2528 ));
2529 assert_eq!(a[0].content, b[0].content);
2530 }
2531
2532 #[test]
2534 fn kotlinは構文エラーのソースでエラーを返す() {
2535 let source = "class A { fun x() {} }\n";
2536 assert!(extract_kotlin_tests("tests/A.kt", source).is_err());
2537 }
2538
2539 #[test]
2541 fn kotlinのマーカーは元のソースから取り非asciiの角括弧は偽マーカーにならない() {
2542 let source = kt_wrap(
2543 " // [正常系]\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() { }",
2544 );
2545 let tests = kt(&source);
2546 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2547 assert!(find(&tests, "u").markers.is_empty());
2548 assert!(find(&tests, "v").markers.is_empty());
2549 }
2550
2551 #[test]
2553 fn kotlinはdocコメントを挟んでも手前のマーカーが有効になる() {
2554 let source = kt_wrap(" // [FR-001]\n /** KDoc */\n @Test\n fun b() { }");
2555 assert_eq!(kt(&source)[0].markers, vec!["FR-001".to_string()]);
2556 }
2557
2558 #[test]
2560 fn javaはdocコメントを挟んでも手前のマーカーが有効になる() {
2561 let source = jv_wrap(" // [FR-001]\n /** Javadoc */\n @Test\n void b() { }");
2562 assert_eq!(jv(&source)[0].markers, vec!["FR-001".to_string()]);
2563 }
2564
2565 #[test]
2567 fn javaは注釈同士の間のコメントをマーカーにせずcontentにも含めない() {
2568 let source =
2569 jv_wrap(" @Test\n // [FR-001]\n @Tag(\"a\")\n // 補足\n void t() { }");
2570 let t = &jv(&source)[0];
2571 assert!(t.markers.is_empty());
2572 assert!(!t.content.contains("FR-001"));
2573 assert!(!t.content.contains("補足"));
2574 }
2575
2576 #[test]
2578 fn kotlinは注釈の間と直後のコメントをcontentに含めない() {
2579 let source = kt_wrap(
2580 " @Test\n // 間のメモ\n @Tag(\"a\")\n // 直後のメモ\n fun t() { }",
2581 );
2582 let t = &kt(&source)[0];
2583 assert!(!t.content.contains("メモ"), "{:?}", t.content);
2584 assert!(t.content.starts_with("fun t()"), "{:?}", t.content);
2585 }
2586
2587 #[test]
2589 fn kotlinは注釈同士の間のコメントをマーカーにしない() {
2590 let source = kt_wrap(" @Test\n // [FR-001]\n @Tag(\"a\")\n fun t() { }");
2591 assert!(kt(&source)[0].markers.is_empty());
2592 }
2593
2594 #[test]
2596 fn kotlinはdata_classとinterfaceの直下のtestも辿る() {
2597 let source = "data class D(val a: Int) {\n @Test\n fun a() { }\n}\n\ninterface I {\n @Test\n fun b() { }\n}\n";
2598 let tests = kt(source);
2599 assert_eq!(find(&tests, "a").id, "tests/AuthTest:D:a");
2600 assert_eq!(find(&tests, "b").id, "tests/AuthTest:I:b");
2601 }
2602
2603 #[test]
2605 fn kotlinは制御文字や空を含む名前のtestを無視する() {
2606 let source =
2607 kt_wrap(" @Test\n fun `tab\there`() { }\n\n @Test\n fun `ok name`() { }");
2608 let tests = kt(&source);
2609 assert_eq!(tests.len(), 1, "{tests:?}");
2610 assert_eq!(tests[0].name, "ok name");
2611 }
2612
2613 #[test]
2615 fn kotlinは注釈2つで基底型なしのクラス内の注釈付きinner_classでもtestを取れる() {
2616 let source = "@A\n@C\nclass P {\n @Nested\n inner class X {\n // [FR-001]\n @Test\n fun t() { }\n }\n}\n";
2617 let tests = kt(source);
2618 assert_eq!(tests.len(), 1, "{tests:?}");
2619 assert_eq!(tests[0].id, "tests/AuthTest:P/X:t");
2620 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
2621 }
2622
2623 #[test]
2625 fn kotlinはnestedを含む複数注釈クラスでも非asciiの名前を元のまま取る() {
2626 let source = "@A\n@C\nclass P {\n @Nested\n inner class 一覧取得 {\n @Test\n fun `取得 する`() { }\n }\n}\n";
2627 let tests = kt(source);
2628 assert_eq!(tests[0].id, "tests/AuthTest:P/一覧取得:取得 する");
2629 }
2630
2631 #[test]
2633 fn kotlinは宣言以外のトップレベル構造やキーワードを含む文字列でもok空になる() {
2634 for source in [
2635 "package a\n// helper for class X\nval x = 1\n",
2636 "package a\nval fixture = object : Runnable {\n override fun run() {}\n}\n",
2637 "package a\nval f = fun() { }\n",
2638 "package a\nconst val NAME = \"class\"\n",
2639 ] {
2640 let tests = kt(source);
2641 assert!(tests.is_empty(), "{source:?}: {tests:?}");
2642 }
2643 }
2644
2645 #[test]
2647 fn kotlinは文法の不具合で宣言が認識されなかった場合に構文エラーを返す() {
2648 let source = "@A\n@C\nclass P {\n @Nested\n inner class X {\n }\n}\n";
2649 assert!(kotlin_extract("tests/P.kt", source, true).is_ok());
2650 assert!(kotlin_extract("tests/P.kt", source, false).is_err());
2651 }
2652
2653 #[test]
2655 fn kotlinはshebangやimportやtypealiasなど宣言以外の通常の構造でok空になる() {
2656 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";
2657 assert!(kt(source).is_empty());
2658 }
2659
2660 #[test]
2664 fn rustはdocコメントを挟んでもマーカーを取る() {
2665 let tests = rs("// [FR-001]\n/// doc\n#[test]\nfn t() {}\n", &[]);
2666 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2667 }
2668
2669 #[test]
2671 fn rustの外側docコメントはマーカーにならない() {
2672 let tests = rs("/// [FR-001]\n#[test]\nfn t() {}\n", &[]);
2673 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2674 }
2675
2676 #[test]
2678 fn rustの内側docコメントはマーカーにならない() {
2679 let tests = rs("mod m {\n//! [FR-001]\n#[test]\nfn t() {}\n}\n", &[]);
2680 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2681 }
2682
2683 #[test]
2685 fn rustはブロックdocコメントを挟んでもマーカーを取る() {
2686 let tests = rs("// [FR-001]\n/** doc */\n#[test]\nfn t() {}\n", &[]);
2687 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2688 }
2689
2690 #[test]
2692 fn rustのスラッシュ4つのコメントは通常コメントだが余分な記号が本文に残りマーカーにならない() {
2693 let tests = rs("//// [FR-001]\n#[test]\nfn t() {}\n", &[]);
2694 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2695 }
2696
2697 #[test]
2699 fn rustはdocコメントの後に空行があるとマーカーを取らない() {
2700 let tests = rs("// [FR-001]\n/// doc\n\n#[test]\nfn t() {}\n", &[]);
2701 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2702 }
2703
2704 #[test]
2706 fn rustはcrlfでもdocコメントを挟んでマーカーを取る() {
2707 let tests = rs("// [FR-001]\r\n/// doc\r\n#[test]\r\nfn t() {}\r\n", &[]);
2708 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2709 }
2710
2711 #[test]
2713 fn goは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2714 let source =
2715 "package a\n\nfunc TestA(t *testing.T) {} // [FR-001]\nfunc TestB(t *testing.T) {}\n";
2716 let tests = extract_go_tests("a/x_test.go", source).unwrap();
2717 assert!(find(&tests, "TestB").markers.is_empty());
2718 }
2719
2720 #[test]
2722 fn rustは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2723 let tests = rs("fn x() {} // [FR-001]\n#[test]\nfn t() {}\n", &[]);
2724 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2725 }
2726
2727 #[test]
2729 fn csharpは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2730 let source =
2731 cs_wrap(" public void X() {} // [FR-001]\n [Fact]\n public void T() {}");
2732 let tests = cs(&source);
2733 assert!(find(&tests, "T").markers.is_empty());
2734 }
2735
2736 #[test]
2738 fn typescriptは前の文の末尾行コメントを次のtestのマーカーにしない() {
2739 let tests = ts("it(\"a\", () => {}); // [FR-001]\nit(\"b\", () => {});\n");
2740 assert!(find(&tests, "b").markers.is_empty());
2741 }
2742
2743 #[test]
2745 fn javaは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2746 let source = jv_wrap(" void x() {} // [FR-001]\n @Test\n void t() {}");
2747 let tests = jv(&source);
2748 assert!(find(&tests, "t").markers.is_empty());
2749 }
2750
2751 #[test]
2753 fn kotlinは非ascii識別子の後の末尾行コメントを次のtestのマーカーにしない() {
2754 let source = kt_wrap(" fun `日本語`() {} // [FR-001]\n @Test\n fun t() {}");
2755 let tests = kt(&source);
2756 assert!(find(&tests, "t").markers.is_empty());
2757 }
2758
2759 #[test]
2761 fn rustはインデント付きの行頭コメントをマーカーにする() {
2762 let tests = rs(
2763 "mod m {\n // [FR-001]\n #[test]\n fn t() {}\n}\n",
2764 &[],
2765 );
2766 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2767 }
2768
2769 #[test]
2771 fn typescriptは同じ行に複数のコメントがあると2つ目以降は行頭でないので打ち切る() {
2772 let tests = ts("/* [FR-001] */ // note\nit(\"b\", () => {});\n");
2773 assert!(find(&tests, "b").markers.is_empty());
2774 }
2775
2776 #[test]
2778 fn rustはbomの直後の1行目マーカーを取る() {
2779 let tests = rs("\u{feff}// [FR-001]\n#[test]\nfn t() {}\n", &[]);
2780 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2781 }
2782
2783 #[test]
2785 fn goはbomの直後の1行目マーカーを取る() {
2786 let source = "\u{feff}// [FR-001]\nfunc TestA(t *testing.T) {}\n";
2787 let tests = extract_go_tests("a/x_test.go", source).unwrap();
2788 assert_eq!(find(&tests, "TestA").markers, vec!["FR-001".to_string()]);
2789 }
2790
2791 #[test]
2793 fn rustの内側ブロックdocコメントはマーカーにならない() {
2794 let tests = rs("mod m {\n/*! [FR-001] */\n#[test]\nfn t() {}\n}\n", &[]);
2795 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2796 }
2797
2798 #[test]
2800 fn rustのスラッシュ星3つのコメントは通常コメントだが余分な記号が本文に残りマーカーにならない() {
2801 let tests = rs("/*** [FR-001] */\n#[test]\nfn t() {}\n", &[]);
2802 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2803 }
2804
2805 #[test]
2807 fn rustの外側ブロックdocコメントはマーカーにならない() {
2808 let tests = rs("/** [FR-001] */\n#[test]\nfn t() {}\n", &[]);
2809 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2810 }
2811
2812 fn marker_cases() -> Vec<(&'static str, Vec<&'static str>)> {
2816 vec![
2817 ("// [FR-001]", vec!["FR-001"]),
2818 ("// [FR-001] ", vec!["FR-001"]),
2819 ("// [FR-001] [FR-002]", vec!["FR-001", "FR-002"]),
2820 ("// [FR-001]\n// [FR-002]", vec!["FR-001", "FR-002"]),
2821 ("//// [FR-001]", vec![]),
2822 ("/*** [FR-001] */", vec![]),
2823 ("// [FR-001] 説明", vec![]),
2824 ("// 説明 [FR-001]", vec![]),
2825 ("// 説明 [0] 説明", vec![]),
2826 ("// [FR-001] [正常系]", vec![]),
2827 ("// [FR-001][FR-002]", vec![]),
2828 ("//", vec![]),
2829 ("/* [FR-001] */", vec!["FR-001"]),
2830 ("/*\n * [FR-001]\n */", vec!["FR-001"]),
2831 (
2832 "/*\n * [FR-001]\n * [FR-002]\n */",
2833 vec!["FR-001", "FR-002"],
2834 ),
2835 ("/* [FR-001] [FR-002] */", vec!["FR-001", "FR-002"]),
2836 ("/*\n * [FR-001] 説明\n */", vec![]),
2837 ("/* 説明 [0] */", vec![]),
2838 ]
2839 }
2840
2841 fn assert_marker_cases(markers_of: impl Fn(&str) -> Vec<String>) {
2842 for (comment, expected) in marker_cases() {
2843 assert_eq!(markers_of(comment), expected, "comment: {comment:?}");
2844 }
2845 }
2846
2847 #[test]
2849 fn goはコメントが単一トークンだけのときマーカーにする() {
2850 assert_marker_cases(|c| {
2851 let source = format!("{c}\nfunc TestA(t *testing.T) {{}}\n");
2852 find(&extract_go_tests("a/x_test.go", &source).unwrap(), "TestA")
2853 .markers
2854 .clone()
2855 });
2856 }
2857
2858 #[test]
2860 fn rustはコメントが単一トークンだけのときマーカーにする() {
2861 assert_marker_cases(|c| {
2862 rs(&format!("{c}\n#[test]\nfn t() {{}}\n"), &[])[0]
2863 .markers
2864 .clone()
2865 });
2866 }
2867
2868 #[test]
2870 fn csharpはコメントが単一トークンだけのときマーカーにする() {
2871 assert_marker_cases(|c| {
2872 cs(&cs_wrap(&format!("{c}\n[Fact]\npublic void T() {{ }}")))[0]
2873 .markers
2874 .clone()
2875 });
2876 }
2877
2878 #[test]
2880 fn javaはコメントが単一トークンだけのときマーカーにする() {
2881 assert_marker_cases(|c| {
2882 jv(&jv_wrap(&format!("{c}\n@Test\nvoid t() {{ }}")))[0]
2883 .markers
2884 .clone()
2885 });
2886 }
2887
2888 #[test]
2890 fn kotlinはコメントが単一トークンだけのときマーカーにする() {
2891 assert_marker_cases(|c| {
2892 kt(&kt_wrap(&format!("{c}\n@Test\nfun t() {{ }}")))[0]
2893 .markers
2894 .clone()
2895 });
2896 }
2897
2898 #[test]
2900 fn typescriptはコメントが単一トークンだけのときマーカーにする() {
2901 assert_marker_cases(|c| {
2902 ts(&format!("{c}\nit(\"t\", () => {{}});\n"))[0]
2903 .markers
2904 .clone()
2905 });
2906 }
2907
2908 #[test]
2910 fn csharpの三連スラッシュコメントも単一トークンだけならマーカーにする() {
2911 let tests = cs(&cs_wrap("/// [FR-001]\n[Fact]\npublic void T() { }"));
2912 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
2913 let tests = cs(&cs_wrap("/// [FR-001] 説明\n[Fact]\npublic void T() { }"));
2914 assert!(tests[0].markers.is_empty());
2915 }
2916
2917 const KOTLIN_KDOC_FIXTURE: &str = include_str!("../fixtures/kotlin_kdoc_annotation.kt");
2920 const KOTLIN_SUPPORT_FIXTURE: &str = include_str!("../fixtures/kotlin_annotation_support.kt");
2921
2922 #[test]
2924 fn kotlinは直前の宣言とコメントの後の位置引数付き注釈でも解析できる() {
2925 let tests = kt("fun g() {}\n// a\n@S(\"L\")\nfun f() {}\n");
2926 assert!(tests.is_empty(), "{tests:?}");
2927 }
2928
2929 #[test]
2931 fn kotlinは非asciiのkdocと位置引数付き注釈の組み合わせでも解析できる() {
2932 let tests = kt(KOTLIN_KDOC_FIXTURE);
2933 assert!(tests.is_empty(), "{tests:?}");
2934 }
2935
2936 #[test]
2938 fn kotlinは位置引数付き注釈を持つサポートコードのファイルを解析できる() {
2939 let tests = kt(KOTLIN_SUPPORT_FIXTURE);
2940 assert!(tests.is_empty(), "{tests:?}");
2941 }
2942
2943 #[test]
2945 fn kotlinは位置引数付き注釈の後ろにtestが続いてもtestを取る() {
2946 let tests = kt("fun g(){}\n// a\n@DisplayName(\"x\")\n@Test\nfun f(){}\n");
2947 assert_eq!(tests.len(), 1, "{tests:?}");
2948 assert_eq!(tests[0].name, "f");
2949 }
2950
2951 #[test]
2953 fn kotlinは位置引数付き注釈の後ろのtestのマーカーも取る() {
2954 let tests = kt("fun g(){}\n// a\n// [FR-001]\n@DisplayName(\"x\")\n@Test\nfun f(){}\n");
2955 assert_eq!(tests.len(), 1, "{tests:?}");
2956 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
2957 }
2958
2959 #[test]
2961 fn kotlinは位置引数付き注釈のある本当の構文エラーを引き続きエラーにする() {
2962 let source = "fun g() {}\n// a\n@S(\"L\")\nfun f( {\n";
2963 assert!(extract_kotlin_tests("tests/A.kt", source).is_err());
2964 }
2965
2966 #[test]
2968 fn kotlinは位置引数付き注釈が複数あってもtestを取る() {
2969 let tests = kt("fun g(){}\n// a\n@A(\"x\")\n@B(\"y\")\n@Test\nfun f(){}\n");
2970 assert_eq!(tests.len(), 1, "{tests:?}");
2971 }
2972}