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
719pub fn is_dart_test_file(path: &str) -> bool {
723 path.ends_with("_test.dart")
724}
725
726fn is_dart_doc_comment(text: &str) -> bool {
730 (text.starts_with("///") && !text.starts_with("////"))
731 || (text.starts_with("/**") && !text.starts_with("/***") && !text.starts_with("/**/"))
732}
733
734pub fn extract_dart_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
740 let mut parser = tree_sitter::Parser::new();
741 parser
742 .set_language(&tree_sitter_dart::LANGUAGE.into())
743 .expect("valid dart grammar");
744 let tree = parser
745 .parse(source, None)
746 .ok_or_else(|| ExtractError::Parse {
747 path: path.to_string(),
748 })?;
749 let root = tree.root_node();
750 if root.has_error() {
751 return Err(ExtractError::Parse {
752 path: path.to_string(),
753 });
754 }
755 let base = path.strip_suffix(".dart").unwrap_or(path);
756 let mut results = Vec::new();
757 let mut cursor = root.walk();
758 for decl in root.children(&mut cursor) {
759 if decl.kind() != "function_declaration" {
760 continue;
761 }
762 let is_main = decl
763 .child_by_field_name("signature")
764 .and_then(|s| s.child_by_field_name("name"))
765 .and_then(|n| n.utf8_text(source.as_bytes()).ok())
766 == Some("main");
767 if !is_main {
768 continue;
769 }
770 let Some(block) = decl
771 .child_by_field_name("body")
772 .and_then(|b| b.named_child(0))
773 .filter(|b| b.kind() == "block")
774 else {
775 continue;
776 };
777 walk_dart_statements(block, source, base, &[], &mut results);
778 }
779 Ok(results)
780}
781
782fn dart_positional_args(args: tree_sitter::Node) -> Vec<tree_sitter::Node> {
785 let mut cursor = args.walk();
786 args.named_children(&mut cursor)
787 .filter(|n| n.kind() != "named_argument" && n.kind() != "comment")
788 .collect()
789}
790
791fn dart_string_name(lit: tree_sitter::Node, source: &str) -> Option<String> {
796 if lit.kind() != "string_literal" {
797 return None;
798 }
799 let mut name = String::new();
800 let mut cursor = lit.walk();
801 let mut parts = 0;
802 for part in lit.children(&mut cursor) {
803 let prefix = match part.kind() {
804 "string_literal_single_quotes" | "string_literal_double_quotes" => 1,
805 "raw_string_literal_single_quotes" | "raw_string_literal_double_quotes" => 2,
806 "comment" => continue,
807 _ => return None,
808 };
809 let mut c = part.walk();
810 if part
811 .children(&mut c)
812 .any(|ch| ch.kind() == "template_substitution")
813 {
814 return None;
815 }
816 let text = part.utf8_text(source.as_bytes()).ok()?;
817 if text.len() < prefix + 1 {
818 return None;
819 }
820 name.push_str(text.get(prefix..text.len() - 1)?);
821 parts += 1;
822 }
823 if parts == 0 || !is_valid_test_name(&name) {
824 return None;
825 }
826 Some(name)
827}
828
829fn walk_dart_statements(
830 block: tree_sitter::Node,
831 source: &str,
832 base: &str,
833 groups: &[String],
834 results: &mut Vec<ExtractedTest>,
835) {
836 let mut cursor = block.walk();
837 let children: Vec<tree_sitter::Node> = block.children(&mut cursor).collect();
838 for (idx, node) in children.iter().enumerate() {
839 if node.kind() != "expression_statement" {
840 continue;
841 }
842 let Some(call) = node.named_child(0) else {
843 continue;
844 };
845 if call.kind() != "call_expression" {
846 continue;
847 }
848 let (Some(callee), Some(args)) = (
849 call.child_by_field_name("function"),
850 call.child_by_field_name("arguments"),
851 ) else {
852 continue;
853 };
854 if callee.kind() != "identifier" {
855 continue;
856 }
857 let head = callee.utf8_text(source.as_bytes()).unwrap_or("");
858 if !matches!(head, "test" | "testWidgets" | "group") {
859 continue;
860 }
861 let positional = dart_positional_args(args);
862 let Some(name) = positional
863 .first()
864 .and_then(|a| dart_string_name(*a, source))
865 else {
866 continue;
867 };
868 if head == "group" {
869 let Some(cb) = positional
870 .get(1)
871 .filter(|cb| cb.kind() == "function_expression")
872 else {
873 continue;
874 };
875 let Some(inner) = cb
876 .child_by_field_name("body")
877 .filter(|b| b.kind() == "function_expression_body")
878 .and_then(|b| b.named_child(0))
879 .filter(|b| b.kind() == "block")
880 else {
881 continue;
882 };
883 let mut new_groups = groups.to_vec();
884 new_groups.push(name);
885 walk_dart_statements(inner, source, base, &new_groups, results);
886 } else {
887 let markers = collect_preceding_markers_with(
888 &children,
889 idx,
890 source,
891 &|n| n.utf8_text(source.as_bytes()).ok(),
892 &["comment"],
893 &|n| {
894 n.utf8_text(source.as_bytes())
895 .is_ok_and(is_dart_doc_comment)
896 },
897 );
898 let content = node.utf8_text(source.as_bytes()).unwrap_or("").to_string();
899 results.push(ExtractedTest {
900 id: format!("{base}:{}:{name}", groups.join("/")),
901 name,
902 markers,
903 content,
904 });
905 }
906 }
907}
908
909const JVM_TEST_ANNOTATIONS: [&str; 5] = [
914 "Test",
915 "ParameterizedTest",
916 "RepeatedTest",
917 "TestFactory",
918 "TestTemplate",
919];
920
921const JVM_COMMENT_KINDS: [&str; 2] = ["line_comment", "block_comment"];
922
923fn jvm_content_start(decl: tree_sitter::Node) -> Option<usize> {
927 fn skip(kind: &str) -> bool {
928 matches!(
929 kind,
930 "annotation" | "marker_annotation" | "line_comment" | "block_comment"
931 )
932 }
933 let mut cursor = decl.walk();
934 for child in decl.children(&mut cursor) {
935 if child.kind() == "modifiers" {
936 let mut c2 = child.walk();
937 let first = child.children(&mut c2).find(|n| !skip(n.kind()));
938 if let Some(first) = first {
939 return Some(first.start_byte());
940 }
941 } else if !skip(child.kind()) {
942 return Some(child.start_byte());
943 }
944 }
945 None
946}
947
948fn jvm_name_is_test(name: &str) -> bool {
949 JVM_TEST_ANNOTATIONS.contains(&name)
950}
951
952pub fn extract_java_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
959 let mut parser = tree_sitter::Parser::new();
960 parser
961 .set_language(&tree_sitter_java::LANGUAGE.into())
962 .expect("valid java grammar");
963 let tree = parser
964 .parse(source, None)
965 .ok_or_else(|| ExtractError::Parse {
966 path: path.to_string(),
967 })?;
968 let root = tree.root_node();
969 if root.has_error() {
970 return Err(ExtractError::Parse {
971 path: path.to_string(),
972 });
973 }
974 let base = path.strip_suffix(".java").unwrap_or(path);
975 let mut results = Vec::new();
976 walk_java_container(root, source, base, &[], &mut results);
977 Ok(results)
978}
979
980fn walk_java_container(
981 container: tree_sitter::Node,
982 source: &str,
983 base: &str,
984 types: &[String],
985 results: &mut Vec<ExtractedTest>,
986) {
987 let mut cursor = container.walk();
988 let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
989 for (idx, node) in children.iter().enumerate() {
990 match node.kind() {
991 "class_declaration"
992 | "record_declaration"
993 | "interface_declaration"
994 | "enum_declaration" => {
995 let (Some(name_node), Some(body)) = (
996 node.child_by_field_name("name"),
997 node.child_by_field_name("body"),
998 ) else {
999 continue;
1000 };
1001 let name = name_node.utf8_text(source.as_bytes()).unwrap_or("");
1002 let mut new_types = types.to_vec();
1003 new_types.push(name.to_string());
1004 if body.kind() == "enum_body" {
1005 let mut c = body.walk();
1006 let decls: Vec<_> = body
1007 .children(&mut c)
1008 .filter(|n| n.kind() == "enum_body_declarations")
1009 .collect();
1010 for d in decls {
1011 walk_java_container(d, source, base, &new_types, results);
1012 }
1013 } else {
1014 walk_java_container(body, source, base, &new_types, results);
1015 }
1016 }
1017 "method_declaration" if !types.is_empty() => {
1018 if !java_is_test_method(*node, source) {
1019 continue;
1020 }
1021 let Some(name_node) = node.child_by_field_name("name") else {
1022 continue;
1023 };
1024 let Some(start) = jvm_content_start(*node) else {
1025 continue;
1026 };
1027 let name = name_node
1028 .utf8_text(source.as_bytes())
1029 .unwrap_or("")
1030 .to_string();
1031 results.push(ExtractedTest {
1032 id: format!("{base}:{}:{name}", types.join("/")),
1033 name,
1034 markers: collect_preceding_markers_with(
1035 &children,
1036 idx,
1037 source,
1038 &|n| n.utf8_text(source.as_bytes()).ok(),
1039 &JVM_COMMENT_KINDS,
1040 &|n| n.utf8_text(source.as_bytes()).is_ok_and(is_doc_comment),
1041 ),
1042 content: source[start..node.end_byte()].to_string(),
1043 });
1044 }
1045 _ => {}
1046 }
1047 }
1048}
1049
1050fn java_is_test_method(method: tree_sitter::Node, source: &str) -> bool {
1051 let mut cursor = method.walk();
1052 let Some(modifiers) = method
1053 .children(&mut cursor)
1054 .find(|n| n.kind() == "modifiers")
1055 else {
1056 return false;
1057 };
1058 let mut c2 = modifiers.walk();
1059 for ann in modifiers.children(&mut c2) {
1060 if ann.kind() != "marker_annotation" && ann.kind() != "annotation" {
1061 continue;
1062 }
1063 let Some(mut name) = ann.child_by_field_name("name") else {
1064 continue;
1065 };
1066 while name.kind() == "scoped_identifier" {
1067 match name.child_by_field_name("name") {
1068 Some(n) => name = n,
1069 None => break,
1070 }
1071 }
1072 if jvm_name_is_test(name.utf8_text(source.as_bytes()).unwrap_or("")) {
1073 return true;
1074 }
1075 }
1076 false
1077}
1078
1079struct KotlinShadow<'a> {
1088 original: &'a str,
1089 shadow: String,
1090 map: Vec<(usize, usize)>,
1091}
1092
1093impl<'a> KotlinShadow<'a> {
1094 fn new(original: &'a str) -> Self {
1095 let mut shadow = String::with_capacity(original.len());
1096 let mut map = vec![(0usize, 0usize)];
1097 for (off, ch) in original.char_indices() {
1098 if ch.is_ascii() {
1099 shadow.push(ch);
1100 } else {
1101 shadow.push(if ch.is_whitespace() || ch == '\u{feff}' {
1102 ' '
1103 } else {
1104 'x'
1105 });
1106 map.push((shadow.len(), off + ch.len_utf8()));
1107 }
1108 }
1109 KotlinShadow {
1110 original,
1111 shadow,
1112 map,
1113 }
1114 }
1115
1116 fn blank_ranges(&mut self, ranges: &[(usize, usize)]) {
1118 let mut bytes = std::mem::take(&mut self.shadow).into_bytes();
1119 for &(a, b) in ranges {
1120 for byte in &mut bytes[a..b] {
1121 if *byte != b'\n' {
1122 *byte = b' ';
1123 }
1124 }
1125 }
1126 self.shadow = String::from_utf8(bytes).expect("ascii shadow");
1127 }
1128
1129 fn to_original(&self, shadow_offset: usize) -> usize {
1130 let i = self.map.partition_point(|&(s, _)| s <= shadow_offset) - 1;
1131 let (s, o) = self.map[i];
1132 o + (shadow_offset - s)
1133 }
1134
1135 fn slice(&self, start: usize, end: usize) -> Option<&'a str> {
1136 self.original
1137 .get(self.to_original(start)..self.to_original(end))
1138 }
1139
1140 fn node_text(&self, node: tree_sitter::Node) -> Option<&'a str> {
1141 self.slice(node.start_byte(), node.end_byte())
1142 }
1143}
1144
1145pub fn extract_kotlin_tests(path: &str, source: &str) -> Result<Vec<ExtractedTest>, ExtractError> {
1150 kotlin_extract(path, source, true)
1151}
1152
1153const KOTLIN_ROOT_KINDS: [&str; 11] = [
1157 "shebang",
1158 "file_annotation",
1159 "package_header",
1160 "import",
1161 "class_declaration",
1162 "object_declaration",
1163 "function_declaration",
1164 "property_declaration",
1165 "type_alias",
1166 "line_comment",
1167 "block_comment",
1168];
1169
1170fn kotlin_extract(
1173 path: &str,
1174 source: &str,
1175 retry_annotated_expression: bool,
1176) -> Result<Vec<ExtractedTest>, ExtractError> {
1177 let parse_err = || ExtractError::Parse {
1178 path: path.to_string(),
1179 };
1180 let mut shadow = KotlinShadow::new(source);
1181 let mut parser = tree_sitter::Parser::new();
1182 parser
1183 .set_language(&tree_sitter_kotlin_ng::LANGUAGE.into())
1184 .expect("valid kotlin grammar");
1185 let mut tree = parser.parse(&shadow.shadow, None).ok_or_else(parse_err)?;
1186 if tree.root_node().has_error() {
1187 return Err(parse_err());
1188 }
1189 if retry_annotated_expression && kotlin_has_annotated_expression(tree.root_node()) {
1196 let ranges = kotlin_annotated_expression_ranges(tree.root_node());
1197 drop(tree);
1198 shadow.blank_ranges(&ranges);
1199 tree = parser.parse(&shadow.shadow, None).ok_or_else(parse_err)?;
1200 if tree.root_node().has_error() {
1201 return Err(parse_err());
1202 }
1203 }
1204 let root = tree.root_node();
1205 let mut cursor = root.walk();
1208 let unexpected = root
1209 .children(&mut cursor)
1210 .any(|n| !KOTLIN_ROOT_KINDS.contains(&n.kind()));
1211 if unexpected {
1212 return Err(parse_err());
1213 }
1214 let base = path.strip_suffix(".kt").unwrap_or(path);
1215 let mut results = Vec::new();
1216 walk_kotlin_container(root, &shadow, base, &[], &mut results).map_err(|()| parse_err())?;
1217 Ok(results)
1218}
1219
1220fn kotlin_has_annotated_expression(root: tree_sitter::Node) -> bool {
1221 let mut cursor = root.walk();
1222 let found = root
1223 .children(&mut cursor)
1224 .any(|n| n.kind() == "annotated_expression");
1225 found
1226}
1227
1228fn kotlin_annotated_expression_ranges(root: tree_sitter::Node) -> Vec<(usize, usize)> {
1237 let mut ranges = Vec::new();
1238 let mut cursor = root.walk();
1239 for expr in root.children(&mut cursor) {
1240 if expr.kind() != "annotated_expression" {
1241 continue;
1242 }
1243 let mut c2 = expr.walk();
1244 let children: Vec<tree_sitter::Node> = expr.children(&mut c2).collect();
1245 let args: Vec<_> = children
1246 .windows(2)
1247 .filter(|w| w[0].kind() == "annotation" && w[1].kind() == "parenthesized_expression")
1248 .map(|w| &w[1])
1249 .collect();
1250 let picked: Vec<_> = if args.is_empty() {
1251 children
1252 .iter()
1253 .filter(|c| c.kind() == "annotation")
1254 .collect()
1255 } else {
1256 args
1257 };
1258 ranges.extend(picked.iter().map(|n| (n.start_byte(), n.end_byte())));
1259 }
1260 ranges
1261}
1262
1263fn kotlin_ident(text: &str) -> String {
1264 text.strip_prefix('`')
1265 .and_then(|t| t.strip_suffix('`'))
1266 .unwrap_or(text)
1267 .to_string()
1268}
1269
1270fn walk_kotlin_container(
1271 container: tree_sitter::Node,
1272 shadow: &KotlinShadow,
1273 base: &str,
1274 types: &[String],
1275 results: &mut Vec<ExtractedTest>,
1276) -> Result<(), ()> {
1277 let mut cursor = container.walk();
1278 let children: Vec<tree_sitter::Node> = container.children(&mut cursor).collect();
1279 for (idx, node) in children.iter().enumerate() {
1280 match node.kind() {
1281 "class_declaration" | "object_declaration" | "companion_object" => {
1282 let name = match node.child_by_field_name("name") {
1283 Some(n) => match shadow.node_text(n) {
1284 Some(t) => kotlin_ident(t),
1285 None => {
1286 debug_assert!(false, "kotlin name range off a char boundary");
1287 return Err(());
1288 }
1289 },
1290 None if node.kind() == "companion_object" => "Companion".to_string(),
1291 None => continue,
1292 };
1293 let mut c = node.walk();
1294 let Some(body) = node
1295 .children(&mut c)
1296 .find(|n| matches!(n.kind(), "class_body" | "enum_class_body"))
1297 else {
1298 continue;
1299 };
1300 let mut new_types = types.to_vec();
1301 new_types.push(name);
1302 walk_kotlin_container(body, shadow, base, &new_types, results)?;
1303 }
1304 "function_declaration" => {
1305 if !kotlin_is_test_function(*node, shadow) {
1306 continue;
1307 }
1308 let Some(name_node) = node.child_by_field_name("name") else {
1309 continue;
1310 };
1311 let Some(name_text) = shadow.node_text(name_node) else {
1312 debug_assert!(false, "kotlin name range off a char boundary");
1313 return Err(());
1314 };
1315 let name = kotlin_ident(name_text);
1316 if !is_valid_test_name(&name) {
1317 continue;
1318 }
1319 let Some(start) = jvm_content_start(*node) else {
1320 continue;
1321 };
1322 let Some(content) = shadow.slice(start, node.end_byte()) else {
1323 debug_assert!(false, "kotlin content range off a char boundary");
1324 return Err(());
1325 };
1326 results.push(ExtractedTest {
1327 id: format!("{base}:{}:{name}", types.join("/")),
1328 name,
1329 markers: collect_preceding_markers_with(
1332 &children,
1333 idx,
1334 &shadow.shadow,
1335 &|n| shadow.node_text(n),
1336 &JVM_COMMENT_KINDS,
1337 &|n| shadow.node_text(n).is_some_and(is_doc_comment),
1338 ),
1339 content: content.to_string(),
1340 });
1341 }
1342 _ => {}
1343 }
1344 }
1345 Ok(())
1346}
1347
1348fn kotlin_is_test_function(func: tree_sitter::Node, shadow: &KotlinShadow) -> bool {
1349 let mut cursor = func.walk();
1350 let Some(modifiers) = func.children(&mut cursor).find(|n| n.kind() == "modifiers") else {
1351 return false;
1352 };
1353 let mut c2 = modifiers.walk();
1354 let found = modifiers
1355 .children(&mut c2)
1356 .filter(|n| n.kind() == "annotation")
1357 .any(|ann| kotlin_annotation_has_test(ann, shadow));
1358 found
1359}
1360
1361fn kotlin_annotation_has_test(node: tree_sitter::Node, shadow: &KotlinShadow) -> bool {
1364 let mut cursor = node.walk();
1365 for child in node.children(&mut cursor) {
1366 match child.kind() {
1367 "user_type" => {
1368 let mut c = child.walk();
1369 let last = child
1370 .children(&mut c)
1371 .filter(|n| matches!(n.kind(), "identifier" | "type_identifier"))
1372 .last();
1373 if let Some(last) = last {
1374 if jvm_name_is_test(shadow.node_text(last).unwrap_or("")) {
1375 return true;
1376 }
1377 }
1378 }
1379 "value_arguments" | "use_site_target" => {}
1380 _ => {
1381 if kotlin_annotation_has_test(child, shadow) {
1382 return true;
1383 }
1384 }
1385 }
1386 }
1387 false
1388}
1389
1390#[cfg(test)]
1391mod tests {
1392 use super::*;
1393
1394 fn find<'a>(tests: &'a [ExtractedTest], name: &str) -> &'a ExtractedTest {
1395 tests
1396 .iter()
1397 .find(|t| t.name == name)
1398 .unwrap_or_else(|| panic!("test named {name} not found in {tests:?}"))
1399 }
1400
1401 #[test]
1403 fn goは直前に隣接するコメントをマーカーとして抽出する() {
1404 let source = r#"package auth
1405
1406// [FR-001]
1407func TestRejectsShortPassword(t *testing.T) {
1408 doSomething()
1409}
1410"#;
1411 let tests = extract_go_tests("internal/auth/login_test.go", source).unwrap();
1412 let t = find(&tests, "TestRejectsShortPassword");
1413 assert_eq!(t.markers, vec!["FR-001".to_string()]);
1414 }
1415
1416 #[test]
1418 fn goは空行を挟んだコメントや欠落をマーカーなしとして無視する() {
1419 let source = r#"package auth
1420
1421// [FR-002]
1422
1423func TestWithBlankLine(t *testing.T) {
1424}
1425
1426func TestNoComment(t *testing.T) {
1427}
1428"#;
1429 let tests = extract_go_tests("internal/auth/login_test.go", source).unwrap();
1430 assert!(find(&tests, "TestWithBlankLine").markers.is_empty());
1431 assert!(find(&tests, "TestNoComment").markers.is_empty());
1432 }
1433
1434 #[test]
1436 fn goのtest識別子はディレクトリと関数名を組み合わせた形式になる() {
1437 let source = "package auth\n\n// [FR-001]\nfunc TestLogin(t *testing.T) {}\n";
1438 let tests = extract_go_tests("internal/auth/login_test.go", source).unwrap();
1439 assert_eq!(find(&tests, "TestLogin").id, "internal/auth:TestLogin");
1440 }
1441
1442 #[test]
1444 fn goのtest識別子はリポジトリ直下ならドットになる() {
1445 let source = "package main\n\n// [FR-001]\nfunc TestRoot(t *testing.T) {}\n";
1446 let tests = extract_go_tests("login_test.go", source).unwrap();
1447 assert_eq!(find(&tests, "TestRoot").id, ".:TestRoot");
1448 }
1449
1450 #[test]
1452 fn rustの関数本体の変更はcontentに反映される() {
1453 let base = r#"// [FR-001]
1454#[test]
1455fn rejects_short_password() {
1456 assert!(true);
1457}
1458"#;
1459 let changed = r#"// [FR-001]
1460#[test]
1461fn rejects_short_password() {
1462 assert!(false);
1463}
1464"#;
1465 let a = find(
1466 &extract_rust_tests("src/auth.rs", base, &[]).unwrap(),
1467 "rejects_short_password",
1468 )
1469 .content
1470 .clone();
1471 let b = find(
1472 &extract_rust_tests("src/auth.rs", changed, &[]).unwrap(),
1473 "rejects_short_password",
1474 )
1475 .content
1476 .clone();
1477 assert_ne!(a, b);
1478 assert!(a.contains("fn rejects_short_password"));
1479 assert!(a.contains("assert!(true)"));
1480 }
1481
1482 #[test]
1484 fn goの関数本体の変更はcontentに反映される() {
1485 let base = "package auth\n\n// [FR-001]\nfunc TestRejectsShortPassword(t *testing.T) {\n\tassertTrue(t, true)\n}\n";
1486 let changed = "package auth\n\n// [FR-001]\nfunc TestRejectsShortPassword(t *testing.T) {\n\tassertTrue(t, false)\n}\n";
1487 let a = find(
1488 &extract_go_tests("internal/auth/login_test.go", base).unwrap(),
1489 "TestRejectsShortPassword",
1490 )
1491 .content
1492 .clone();
1493 let b = find(
1494 &extract_go_tests("internal/auth/login_test.go", changed).unwrap(),
1495 "TestRejectsShortPassword",
1496 )
1497 .content
1498 .clone();
1499 assert_ne!(a, b);
1500 assert!(a.contains("func TestRejectsShortPassword"));
1501 assert!(a.contains("assertTrue(t, true)"));
1502 }
1503
1504 #[test]
1506 fn rustのcontentは外側の属性とマーカーコメントを含まない() {
1507 let source = r#"// [FR-001]
1508#[test]
1509fn rejects_short_password() {
1510 assert!(true);
1511}
1512"#;
1513 let extracted = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1514 let t = find(&extracted, "rejects_short_password");
1515 assert!(!t.content.contains("#[test]"));
1516 assert!(!t.content.contains("[FR-001]"));
1517 assert!(t.content.contains("fn rejects_short_password"));
1518 }
1519
1520 fn rs(source: &str, extra: &[&str]) -> Vec<ExtractedTest> {
1521 let extra: Vec<String> = extra.iter().map(|s| s.to_string()).collect();
1522 extract_rust_tests("src/auth.rs", source, &extra).unwrap()
1523 }
1524
1525 fn has(tests: &[ExtractedTest], name: &str) -> bool {
1526 tests.iter().any(|t| t.name == name)
1527 }
1528
1529 #[test]
1531 fn rustは属性パスの末尾がtestならtestとして抽出する() {
1532 let source = r#"// [FR-001]
1533#[test]
1534fn plain_test() {}
1535
1536// [FR-002]
1537#[tokio::test]
1538async fn async_test() {}
1539
1540// [FR-003]
1541#[async_std::test]
1542async fn async_std_test() {}
1543
1544// [FR-004]
1545#[tokio::test(flavor = "multi_thread")]
1546async fn multi_thread_test() {}
1547
1548// [FR-005]
1549#[ test ]
1550fn spaced_test() {}
1551"#;
1552 let tests = rs(source, &[]);
1553 for name in [
1554 "plain_test",
1555 "async_test",
1556 "async_std_test",
1557 "multi_thread_test",
1558 "spaced_test",
1559 ] {
1560 assert!(has(&tests, name), "{name} should be extracted");
1561 }
1562 assert_eq!(
1563 find(&tests, "async_test").markers,
1564 vec!["FR-002".to_string()]
1565 );
1566 }
1567
1568 #[test]
1570 fn rustのrstestは追加指定がなければ対象外で指定があれば対象になる() {
1571 let source = "// [FR-001]\n#[rstest]\nfn t() {}\n";
1572 assert!(!has(&rs(source, &[]), "t"));
1573 assert!(has(&rs(source, &["rstest"]), "t"));
1574
1575 let scoped = "// [FR-001]\n#[rstest::rstest]\nfn t() {}\n";
1576 assert!(!has(&rs(scoped, &[]), "t"));
1577 assert!(has(&rs(scoped, &["rstest"]), "t"));
1578 }
1579
1580 #[test]
1582 fn rustのtest_caseとcfg_attrのtestは対象外() {
1583 let source = r#"// [FR-001]
1584#[test_case(1)]
1585fn case_test() {}
1586
1587// [FR-002]
1588#[cfg_attr(x, test)]
1589fn cfg_attr_test() {}
1590"#;
1591 let tests = rs(source, &[]);
1592 assert!(!has(&tests, "case_test"));
1593 assert!(!has(&tests, "cfg_attr_test"));
1594
1595 let tests = rs(source, &["test_case"]);
1596 assert!(has(&tests, "case_test"));
1597 assert!(!has(&tests, "cfg_attr_test"));
1598 }
1599
1600 #[test]
1602 fn rustのinner_attributeのtestはtestを作らない() {
1603 let source = "#![test]\n\n// [FR-001]\nfn helper() {}\n";
1604 assert!(rs(source, &[]).is_empty());
1605 }
1606
1607 #[test]
1609 fn rustは最初の対象属性の直前のコメントをマーカーにする() {
1610 let source = "// [FR-001]\n#[rstest]\n#[case(1)]\n#[tokio::test]\nfn t() {}\n";
1611 let tests = rs(source, &["rstest"]);
1612 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
1613 }
1614
1615 #[test]
1617 fn rustは対象属性の設定によりマーカー位置が変わる() {
1618 let source = "#[rstest]\n// [FR-001]\n#[tokio::test]\nfn t() {}\n";
1619 let without = rs(source, &[]);
1620 assert_eq!(find(&without, "t").markers, vec!["FR-001".to_string()]);
1621 let with = rs(source, &["rstest"]);
1622 assert!(find(&with, "t").markers.is_empty());
1623 }
1624
1625 #[test]
1627 fn rustは属性間や属性末尾のコメントがあってもtestとして検出しマーカーを取る() {
1628 let between = "// [FR-001]\n#[test]\n// note\n#[ignore]\nfn between() {}\n";
1629 let tests = rs(between, &[]);
1630 assert_eq!(find(&tests, "between").markers, vec!["FR-001".to_string()]);
1631
1632 let trailing = "// [FR-001]\n#[test] // trailing\n#[ignore]\nfn trailing() {}\n";
1633 let tests = rs(trailing, &[]);
1634 assert_eq!(find(&tests, "trailing").markers, vec!["FR-001".to_string()]);
1635
1636 let block = "// [FR-001]\n#[test]\n/* block */\n#[ignore]\nfn block() {}\n";
1637 assert_eq!(
1638 find(&rs(block, &[]), "block").markers,
1639 vec!["FR-001".to_string()]
1640 );
1641 }
1642
1643 #[test]
1645 fn tokioのtest属性はcontentに含まれない() {
1646 let source = "// [FR-001]\n#[tokio::test]\nasync fn t() { assert!(true); }\n";
1647 let tests = rs(source, &[]);
1648 let t = find(&tests, "t");
1649 assert!(!t.content.contains("tokio::test"));
1650 assert!(!t.content.contains("#["));
1651 assert!(t.content.contains("async fn t"));
1652 }
1653
1654 #[test]
1656 fn rustはtest属性の直前に隣接するコメントをマーカーとする() {
1657 let source = r#"// [FR-001]
1658#[test]
1659fn adjacent() {}
1660
1661// [FR-002]
1662
1663#[test]
1664fn blank_line_between() {}
1665
1666#[test]
1667fn no_comment() {}
1668"#;
1669 let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1670 assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
1671 assert!(find(&tests, "blank_line_between").markers.is_empty());
1672 assert!(find(&tests, "no_comment").markers.is_empty());
1673 }
1674
1675 #[test]
1677 fn rustのtest識別子はトップレベルなら空モジュールパスになる() {
1678 let source = "// [FR-001]\n#[test]\nfn roundtrip() {}\n";
1679 let tests = extract_rust_tests("src/lock.rs", source, &[]).unwrap();
1680 assert_eq!(find(&tests, "roundtrip").id, "src/lock::roundtrip");
1681 }
1682
1683 #[test]
1685 fn rustのtest識別子はinline_modを外側から内側の順に連結する() {
1686 let source = r#"mod tests {
1687 // [FR-001]
1688 #[test]
1689 fn roundtrip() {}
1690}
1691"#;
1692 let tests = extract_rust_tests("src/lock.rs", source, &[]).unwrap();
1693 assert_eq!(find(&tests, "roundtrip").id, "src/lock:tests:roundtrip");
1694 }
1695
1696 #[test]
1698 fn rustのtest識別子はネストしたinline_modをスラッシュで連結する() {
1699 let source = r#"mod outer {
1700 mod inner {
1701 // [FR-001]
1702 #[test]
1703 fn t() {}
1704 }
1705}
1706"#;
1707 let tests = extract_rust_tests("src/x.rs", source, &[]).unwrap();
1708 assert_eq!(find(&tests, "t").id, "src/x:outer/inner:t");
1709 }
1710
1711 #[test]
1713 fn rustは属性が間に挟まるとtestの直前のコメントをマーカーにしない() {
1714 let source = r#"// [FR-001]
1715#[cfg(feature = "x")]
1716#[test]
1717fn not_adjacent() {}
1718"#;
1719 let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1720 assert!(find(&tests, "not_adjacent").markers.is_empty());
1721 }
1722
1723 #[test]
1725 fn rustは他の属性の後ろでもtestの直前のコメントはマーカーになる() {
1726 let source = r#"#[cfg(feature = "x")]
1727// [FR-001]
1728#[test]
1729fn adjacent_after_other_attr() {}
1730"#;
1731 let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1732 assert_eq!(
1733 find(&tests, "adjacent_after_other_attr").markers,
1734 vec!["FR-001".to_string()]
1735 );
1736 }
1737
1738 #[test]
1740 fn rustはtestとfnの間にコメントがあってもtestとして検出しマーカーを採用する() {
1741 let source = r#"// [FR-001]
1742#[test]
1743// 補足
1744fn t() {}
1745"#;
1746 let tests = extract_rust_tests("src/auth.rs", source, &[]).unwrap();
1747 let found = find(&tests, "t");
1748 assert_eq!(found.markers, vec!["FR-001".to_string()]);
1749 assert!(!found.content.contains("補足"));
1750 }
1751
1752 fn cs(source: &str) -> Vec<ExtractedTest> {
1755 extract_csharp_tests("tests/AuthTests.cs", source).unwrap()
1756 }
1757
1758 fn cs_wrap(method: &str) -> String {
1759 format!("public class AuthTests\n{{\n{method}\n}}\n")
1760 }
1761
1762 #[test]
1764 fn csharpはif_endif内のtestメソッドも検出する() {
1765 let source = "public class A {\n#if DEBUG\n // [FR-001]\n [Fact]\n public void T() { }\n#endif\n}\n";
1766 let tests = extract_csharp_tests("tests/A.cs", source).unwrap();
1767 assert_eq!(tests.len(), 1);
1768 assert_eq!(tests[0].id, "tests/A:A:T");
1769 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
1770 }
1771
1772 #[test]
1774 fn csharpは7種類の属性をそれぞれtestとして検出する() {
1775 for attr in [
1776 "Fact",
1777 "Theory",
1778 "Test",
1779 "TestCase",
1780 "TestCaseSource",
1781 "TestMethod",
1782 "DataTestMethod",
1783 ] {
1784 let source = cs_wrap(&format!(" [{attr}]\n public void T() {{ }}"));
1785 let tests = cs(&source);
1786 assert_eq!(tests.len(), 1, "attribute {attr} should be detected");
1787 assert_eq!(tests[0].name, "T");
1788 }
1789 }
1790
1791 #[test]
1793 fn csharpはattribute接尾辞付きや無関係な属性のみのメソッドを検出しない() {
1794 let source = cs_wrap(
1795 " [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() { }",
1796 );
1797 assert!(cs(&source).is_empty());
1798 }
1799
1800 #[test]
1802 fn csharpは引数付きの属性も検出する() {
1803 let source = cs_wrap(" [Fact(Skip = \"x\")]\n public void T() { }");
1804 assert_eq!(cs(&source).len(), 1);
1805 }
1806
1807 #[test]
1809 fn csharpは名前空間修飾された属性も検出する() {
1810 let source = cs_wrap(
1811 " [Xunit.Fact]\n public void A() { }\n\n [global::Xunit.Fact]\n public void B() { }",
1812 );
1813 let tests = cs(&source);
1814 assert_eq!(tests.len(), 2);
1815 find(&tests, "A");
1816 find(&tests, "B");
1817 }
1818
1819 #[test]
1821 fn csharpはtarget指定付きの属性も検出する() {
1822 let source = cs_wrap(" [method: Fact]\n public void T() { }");
1823 assert_eq!(cs(&source).len(), 1);
1824 }
1825
1826 #[test]
1828 fn csharpは同一属性リスト内の他の属性と並んだ属性も検出する() {
1829 let source = cs_wrap(" [Trait(\"a\", \"b\"), Fact]\n public void T() { }");
1830 assert_eq!(cs(&source).len(), 1);
1831 }
1832
1833 #[test]
1835 fn csharpは複数の属性リストの順序に関わらず検出する() {
1836 let source = cs_wrap(
1837 " [Theory]\n [InlineData(1)]\n public void A(int x) { }\n\n [InlineData(1)]\n [Theory]\n public void B(int x) { }",
1838 );
1839 let tests = cs(&source);
1840 assert_eq!(tests.len(), 2);
1841 find(&tests, "A");
1842 find(&tests, "B");
1843 }
1844
1845 #[test]
1847 fn csharpは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
1848 let source = cs_wrap(
1849 " // [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() { }",
1850 );
1851 let tests = cs(&source);
1852 assert_eq!(find(&tests, "Adjacent").markers, vec!["FR-001".to_string()]);
1853 assert!(find(&tests, "Blank").markers.is_empty());
1854 assert!(find(&tests, "None").markers.is_empty());
1855 }
1856
1857 #[test]
1859 fn csharpはドキュメントコメントとブロックコメントもマーカーにできる() {
1860 let source = cs_wrap(
1861 " /// [FR-001]\n [Fact]\n public void Doc() { }\n\n /* [FR-002] */\n [Fact]\n public void Block() { }",
1862 );
1863 let tests = cs(&source);
1864 assert_eq!(find(&tests, "Doc").markers, vec!["FR-001".to_string()]);
1865 assert_eq!(find(&tests, "Block").markers, vec!["FR-002".to_string()]);
1866 }
1867
1868 #[test]
1870 fn csharpは属性と署名の間のコメントをマーカーにせずcontentにも含めない() {
1871 let source = cs_wrap(
1872 " [Fact]\n // [FR-001]\n public void T() { }\n\n [Fact]\n [Trait(\"a\", \"b\")]\n // 補足\n public void U() { }",
1873 );
1874 let tests = cs(&source);
1875 let t = find(&tests, "T");
1876 assert!(t.markers.is_empty());
1877 assert!(!t.content.contains("FR-001"));
1878 let u = find(&tests, "U");
1879 assert!(!u.content.contains("補足"));
1880 }
1881
1882 #[test]
1884 fn csharpは属性リストの間のコメントをマーカーにせずcontentにも含めない() {
1885 let source = cs_wrap(
1886 " [Theory]\n // [FR-001]\n [InlineData(1)]\n public void T(int x) { }",
1887 );
1888 let tests = cs(&source);
1889 let t = find(&tests, "T");
1890 assert!(t.markers.is_empty());
1891 assert!(!t.content.contains("FR-001"));
1892 }
1893
1894 #[test]
1896 fn csharpのtest識別子はトップレベルclassならファイルパスと型名とメソッド名になる() {
1897 let source =
1898 "public class AuthTests\n{\n [Fact]\n public void RejectsShort() { }\n}\n";
1899 let tests = cs(source);
1900 assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:RejectsShort");
1901 }
1902
1903 #[test]
1905 fn csharpのtest識別子はネストした型を外側から内側へスラッシュで連結する() {
1906 let source = "public class Outer\n{\n public class Inner\n {\n [Test]\n public void T3() { }\n }\n}\n";
1907 let tests = cs(source);
1908 assert_eq!(tests[0].id, "tests/AuthTests:Outer/Inner:T3");
1909 }
1910
1911 #[test]
1913 fn csharpのtest識別子はstruct_record_interfaceの中でも型名を辿る() {
1914 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";
1915 let tests = cs(source);
1916 assert_eq!(find(&tests, "A").id, "tests/AuthTests:S:A");
1917 assert_eq!(find(&tests, "B").id, "tests/AuthTests:R:B");
1918 assert_eq!(find(&tests, "C").id, "tests/AuthTests:I:C");
1919 }
1920
1921 #[test]
1923 fn csharpのtest識別子はblock_namespaceを含まない() {
1924 let source = "namespace Foo.Bar\n{\n public class AuthTests\n {\n [Fact]\n public void T() { }\n }\n}\n";
1925 let tests = cs(source);
1926 assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:T");
1927 }
1928
1929 #[test]
1931 fn csharpのtest識別子はfile_scoped_namespaceを含まない() {
1932 let source =
1933 "namespace Foo;\n\npublic class AuthTests\n{\n [Fact]\n public void T() { }\n}\n";
1934 let tests = cs(source);
1935 assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:T");
1936 }
1937
1938 #[test]
1940 fn csharpのcontentは属性を含まず修飾子から始まる() {
1941 let source = cs_wrap(
1942 " // [FR-001]\n [Fact]\n [Trait(\"a\", \"b\")]\n public void T() { Assert.True(true); }",
1943 );
1944 let tests = cs(&source);
1945 let c = &tests[0].content;
1946 assert!(c.starts_with("public void T()"), "content = {c:?}");
1947 assert!(c.ends_with('}'));
1948 assert!(!c.contains("Fact"));
1949 assert!(!c.contains("Trait"));
1950 assert!(!c.contains("FR-001"));
1951 }
1952
1953 #[test]
1955 fn csharpの本体変更はcontentに反映される() {
1956 let a = cs(&cs_wrap(
1957 " [Fact]\n public void T() { Assert.True(true); }",
1958 ));
1959 let b = cs(&cs_wrap(
1960 " [Fact]\n public void T() { Assert.True(false); }",
1961 ));
1962 assert_ne!(a[0].content, b[0].content);
1963 }
1964
1965 #[test]
1967 fn csharpの属性だけの変更はcontentを変えない() {
1968 let a = cs(&cs_wrap(
1969 " [Fact]\n public void T() { Assert.True(true); }",
1970 ));
1971 let b = cs(&cs_wrap(
1972 " [Fact(Skip = \"x\")]\n [Trait(\"a\", \"b\")]\n public void T() { Assert.True(true); }",
1973 ));
1974 assert_eq!(a[0].content, b[0].content);
1975 }
1976
1977 #[test]
1979 fn csharpは構文エラーのソースでエラーを返す() {
1980 let source = "public class A {\n [Fact]\n public void T( {\n}\n";
1981 assert!(extract_csharp_tests("tests/A.cs", source).is_err());
1982 }
1983
1984 fn ts(source: &str) -> Vec<ExtractedTest> {
1987 extract_typescript_tests("src/a.test.ts", source).unwrap()
1988 }
1989
1990 #[test]
1992 fn typescriptはtestとitの呼び出しを検出する() {
1993 let tests = ts("test(\"a\", () => {});\nit(\"b\", () => {});\n");
1994 assert_eq!(tests.len(), 2);
1995 find(&tests, "a");
1996 find(&tests, "b");
1997 }
1998
1999 #[test]
2001 fn typescriptは許可された修飾子付きの呼び出しを検出する() {
2002 let source = "it.skip(\"a\", () => {});\ntest.only(\"b\", () => {});\ntest.concurrent.only(\"c\", () => {});\ntest.todo(\"d\", () => {});\n";
2003 let tests = ts(source);
2004 assert_eq!(tests.len(), 4);
2005 for n in ["a", "b", "c", "d"] {
2006 find(&tests, n);
2007 }
2008 }
2009
2010 #[test]
2012 fn typescriptはdescribe_skipの中のtestも検出する() {
2013 let tests = ts("describe.skip(\"d\", () => {\n it(\"a\", () => {});\n});\n");
2014 assert_eq!(tests.len(), 1);
2015 assert_eq!(tests[0].id, "src/a.test:d:a");
2016 }
2017
2018 #[test]
2020 fn typescriptはfunction式のコールバックも検出する() {
2021 let source = "describe(\"d\", function () {\n it(\"a\", function () {});\n});\n";
2022 let tests = ts(source);
2023 assert_eq!(tests.len(), 1);
2024 assert_eq!(tests[0].id, "src/a.test:d:a");
2025 }
2026
2027 #[test]
2029 fn typescriptはtsxファイルのjsxを含むソースも解析できる() {
2030 let source = "it(\"renders\", () => {\n const x = <div className=\"a\">hi</div>;\n expect(x).toBeTruthy();\n});\n";
2031 let tests = extract_typescript_tests("src/a.test.tsx", source).unwrap();
2032 assert_eq!(tests.len(), 1);
2033 assert_eq!(tests[0].id, "src/a.test::renders");
2034 }
2035
2036 #[test]
2038 fn typescriptは許可外の修飾子や呼び出し結果の呼び出しを検出しない() {
2039 let source = r#"test.each([1, 2])("each %i", (n) => {});
2040test.skipIf(c)("skipif", () => {});
2041test.extend({ a: 1 });
2042test.describe("desc", () => {});
2043test.runIf(c)("runif", () => {});
2044it?.skip("optional", () => {});
2045"#;
2046 assert!(ts(source).is_empty());
2047 }
2048
2049 #[test]
2051 fn typescriptはexport_defaultやskipif_describeやforループ内のitを検出しない() {
2052 let source = r#"export default describe("d", () => { it("y", () => {}); })
2053describe.skipIf(c)("d", () => { it("x", () => {}); });
2054for (const n of [1]) {
2055 it("loop", () => {});
2056}
2057"#;
2058 assert!(ts(source).is_empty());
2059 }
2060
2061 #[test]
2063 fn typescriptは許可された修飾子を検出し走査する() {
2064 let source = r#"test.fails("f", () => {});
2065it.sequential("s", () => {});
2066describe.shuffle("d", () => {
2067 it("inner", () => {});
2068});
2069"#;
2070 let tests = ts(source);
2071 assert_eq!(tests.len(), 3);
2072 find(&tests, "f");
2073 find(&tests, "s");
2074 assert_eq!(find(&tests, "inner").id, "src/a.test:d:inner");
2075 }
2076
2077 #[test]
2079 fn typescriptはdescribe_eachの中のtestを検出しない() {
2080 let source = "describe.each([1])(\"d %i\", (n) => {\n it(\"a\", () => {});\n});\n";
2081 assert!(ts(source).is_empty());
2082 }
2083
2084 #[test]
2086 fn typescriptはawaitなど式文直下でない呼び出しを検出しない() {
2087 let source = "await it(\"a\", () => {});\nvoid it(\"b\", () => {});\n";
2088 assert!(ts(source).is_empty());
2089 }
2090
2091 #[test]
2093 fn typescriptは第1引数が文字列リテラルでない呼び出しを検出しない() {
2094 let source = "const name = \"x\";\nit(name, () => {});\nit(\"a\" + \"b\", () => {});\n";
2095 assert!(ts(source).is_empty());
2096 }
2097
2098 #[test]
2100 fn typescriptはhelper関数やtest本体やif文の中のitを検出しない() {
2101 let source = r#"function helper() {
2102 it("in helper", () => {});
2103}
2104test("outer", () => {
2105 it("in test", () => {});
2106});
2107if (cond) {
2108 it("in if", () => {});
2109}
2110"#;
2111 let tests = ts(source);
2112 assert_eq!(tests.len(), 1);
2113 assert_eq!(tests[0].name, "outer");
2114 }
2115
2116 #[test]
2118 fn typescriptは置換なしのテンプレート文字列を対象にし置換ありを無視する() {
2119 let source = "it(`plain`, () => {});\nit(`with ${x}`, () => {});\n";
2120 let tests = ts(source);
2121 assert_eq!(tests.len(), 1);
2122 assert_eq!(tests[0].name, "plain");
2123 }
2124
2125 #[test]
2127 fn typescriptは空文字名と改行を含む名前を無視する() {
2128 let source = "it(\"\", () => {});\nit(`line1\nline2`, () => {});\nit(\"ok\", () => {});\n";
2129 let tests = ts(source);
2130 assert_eq!(tests.len(), 1);
2131 assert_eq!(tests[0].name, "ok");
2132 }
2133
2134 #[test]
2136 fn typescriptは行区切り文字を含む名前を無視する() {
2137 let source = "it(\"a\u{2028}b\", () => {});\nit(\"ok\", () => {});\n";
2138 let tests = ts(source);
2139 assert_eq!(tests.len(), 1);
2140 assert_eq!(tests[0].name, "ok");
2141 }
2142
2143 #[test]
2145 fn typescriptはエスケープを解釈せずソース表記のまま名前にする() {
2146 let source = "it('it\\'s', () => {});\n";
2147 let tests = ts(source);
2148 assert_eq!(tests.len(), 1);
2149 assert_eq!(tests[0].name, "it\\'s");
2150 assert_eq!(tests[0].id, "src/a.test::it\\'s");
2151 }
2152
2153 #[test]
2155 fn typescriptのtest識別子はdescribeの外なら空のdescribeパスになる() {
2156 let tests = ts("it(\"top\", () => {});\n");
2157 assert_eq!(tests[0].id, "src/a.test::top");
2158 }
2159
2160 #[test]
2162 fn typescriptのtest識別子はdescribeを外側から内側へスラッシュで連結する() {
2163 let source = "describe(\"auth\", () => {\n it(\"one\", () => {});\n describe(\"inner\", () => {\n it(\"two\", () => {});\n });\n});\n";
2164 let tests = ts(source);
2165 assert_eq!(find(&tests, "one").id, "src/a.test:auth:one");
2166 assert_eq!(find(&tests, "two").id, "src/a.test:auth/inner:two");
2167 }
2168
2169 #[test]
2171 fn typescriptのtest識別子は対象拡張子をすべて除去しtestやspecは残す() {
2172 let src = "it(\"t\", () => {});\n";
2173 for (path, id) in [
2174 ("src/a.test.ts", "src/a.test::t"),
2175 ("src/a.spec.tsx", "src/a.spec::t"),
2176 ("src/a.test.mjs", "src/a.test::t"),
2177 ("src/a.spec.cts", "src/a.spec::t"),
2178 ("src/a.test.jsx", "src/a.test::t"),
2179 ("src/a.test.js", "src/a.test::t"),
2180 ("src/a.spec.cjs", "src/a.spec::t"),
2181 ("src/a.test.mts", "src/a.test::t"),
2182 ] {
2183 let tests = extract_typescript_tests(path, src).unwrap();
2184 assert_eq!(tests[0].id, id, "{path}");
2185 }
2186 }
2187
2188 #[test]
2190 fn typescriptのtest識別子は対象外の拡張子を除去しない() {
2191 let tests =
2192 extract_typescript_tests("src/a.test.ts.bak", "it(\"t\", () => {});\n").unwrap();
2193 assert_eq!(tests[0].id, "src/a.test.ts.bak::t");
2194 }
2195
2196 #[test]
2198 fn typescriptはjs系拡張子のファイルからtestを検出する() {
2199 for path in [
2200 "src/a.test.js",
2201 "src/a.spec.mjs",
2202 "src/a.test.cjs",
2203 "src/a.spec.mts",
2204 "src/a.test.cts",
2205 ] {
2206 let tests = extract_typescript_tests(path, "it(\"t\", () => {});\n").unwrap();
2207 assert_eq!(tests.len(), 1, "{path}");
2208 }
2209 }
2210
2211 #[test]
2213 fn typescriptはts系拡張子でtsx文法が拒否する角括弧キャストを解析できる() {
2214 let source = "it(\"t\", () => {\n const n = <number>x;\n});\n";
2215 for path in ["src/a.test.ts", "src/a.test.mts", "src/a.spec.cts"] {
2216 let tests = extract_typescript_tests(path, source).unwrap();
2217 assert_eq!(tests.len(), 1, "{path}");
2218 }
2219 }
2220
2221 #[test]
2223 fn typescriptはjsxとjsファイルのjsxを含むソースも解析できる() {
2224 let source = "it(\"renders\", () => {\n render(<App />);\n});\n";
2225 for path in ["src/a.test.jsx", "src/a.test.js"] {
2226 let tests = extract_typescript_tests(path, source).unwrap();
2227 assert_eq!(tests.len(), 1, "{path}");
2228 assert_eq!(tests[0].id, "src/a.test::renders", "{path}");
2229 }
2230 }
2231
2232 #[test]
2234 fn typescriptは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
2235 let source = "// [FR-001]\nit(\"adjacent\", () => {});\n\n// [FR-002]\n\nit(\"blank\", () => {});\n\nit(\"none\", () => {});\n";
2236 let tests = ts(source);
2237 assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
2238 assert!(find(&tests, "blank").markers.is_empty());
2239 assert!(find(&tests, "none").markers.is_empty());
2240 }
2241
2242 #[test]
2244 fn typescriptはdescribe内のtestでも直前のコメントをマーカーにする() {
2245 let source = "describe(\"d\", () => {\n // [FR-001]\n it(\"a\", () => {});\n});\n";
2246 let tests = ts(source);
2247 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
2248 }
2249
2250 #[test]
2252 fn typescriptのcontentは呼び出し文全体でマーカーコメントを含まない() {
2253 let source = "// [FR-001]\nit(\"a\", () => {\n expect(1).toBe(1);\n});\n";
2254 let tests = ts(source);
2255 let c = &tests[0].content;
2256 assert!(c.starts_with("it(\"a\""), "content = {c:?}");
2257 assert!(c.ends_with(");"), "content = {c:?}");
2258 assert!(!c.contains("FR-001"));
2259 }
2260
2261 #[test]
2263 fn typescriptの本体変更はcontentに反映される() {
2264 let a = ts("it(\"a\", () => { expect(1).toBe(1); });\n");
2265 let b = ts("it(\"a\", () => { expect(1).toBe(2); });\n");
2266 assert_ne!(a[0].content, b[0].content);
2267 }
2268
2269 #[test]
2271 fn typescriptは構文エラーのソースでエラーを返す() {
2272 let source = "it(\"a\", () => {\n";
2273 assert!(extract_typescript_tests("src/a.test.ts", source).is_err());
2274 }
2275
2276 fn jv(source: &str) -> Vec<ExtractedTest> {
2279 extract_java_tests("tests/AuthTests.java", source).unwrap()
2280 }
2281
2282 fn jv_wrap(body: &str) -> String {
2283 format!("public class AuthTests {{\n{body}\n}}\n")
2284 }
2285
2286 #[test]
2288 fn javaは5種類の注釈をそれぞれtestとして検出する() {
2289 for ann in [
2290 "Test",
2291 "ParameterizedTest",
2292 "RepeatedTest",
2293 "TestFactory",
2294 "TestTemplate",
2295 ] {
2296 let source = jv_wrap(&format!(" @{ann}\n void t() {{ }}"));
2297 let tests = jv(&source);
2298 assert_eq!(tests.len(), 1, "annotation {ann} should be detected");
2299 assert_eq!(tests[0].name, "t");
2300 }
2301 }
2302
2303 #[test]
2305 fn javaは修飾名の注釈と引数付きの注釈も検出する() {
2306 let source = jv_wrap(
2307 " @org.junit.jupiter.api.Test\n void a() { }\n\n @Test(timeout = 1)\n void b() { }",
2308 );
2309 let tests = jv(&source);
2310 assert_eq!(tests.len(), 2);
2311 find(&tests, "a");
2312 find(&tests, "b");
2313 }
2314
2315 #[test]
2317 fn javaは無関係な注釈のみのメソッドを検出しない() {
2318 let source = jv_wrap(
2319 " @Disabled\n void a() { }\n\n void b() { }\n\n @Tag(\"x\")\n void c() { }",
2320 );
2321 assert!(jv(&source).is_empty());
2322 }
2323
2324 #[test]
2326 fn javaは修飾子の後ろに書かれた注釈も検出する() {
2327 let source = jv_wrap(" public @Test void t() { }");
2328 let tests = jv(&source);
2329 assert_eq!(tests.len(), 1);
2330 assert!(tests[0].content.contains("@Test"), "{:?}", tests[0].content);
2331 }
2332
2333 #[test]
2335 fn javaは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
2336 let source = jv_wrap(
2337 " // [FR-001]\n @Test\n void adjacent() { }\n\n // [FR-002]\n\n @Test\n void blank() { }\n\n @Test\n void none() { }",
2338 );
2339 let tests = jv(&source);
2340 assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
2341 assert!(find(&tests, "blank").markers.is_empty());
2342 assert!(find(&tests, "none").markers.is_empty());
2343 }
2344
2345 #[test]
2347 fn javaはブロックコメントをマーカーにできる() {
2348 let source = jv_wrap(" /* [FR-002] */\n @Test\n void t() { }");
2349 assert_eq!(jv(&source)[0].markers, vec!["FR-002".to_string()]);
2350 }
2351
2352 #[test]
2354 fn javaはjavadocコメントをマーカーにしない() {
2355 let source = jv_wrap(" /** [FR-001] */\n @Test\n void t() { }");
2356 assert!(jv(&source)[0].markers.is_empty());
2357 }
2358
2359 #[test]
2361 fn javaは注釈と署名の間のコメントをマーカーにせずcontentにも含めない() {
2362 let source = jv_wrap(" @Test\n // [FR-001]\n // 補足\n void t() { }");
2363 let tests = jv(&source);
2364 assert!(tests[0].markers.is_empty());
2365 assert!(!tests[0].content.contains("FR-001"));
2366 assert!(!tests[0].content.contains("補足"));
2367 }
2368
2369 #[test]
2371 fn javaのtest識別子はclassならファイルパスと型名とメソッド名になる() {
2372 let tests = jv(&jv_wrap(" @Test\n void rejectsShort() { }"));
2373 assert_eq!(tests[0].id, "tests/AuthTests:AuthTests:rejectsShort");
2374 }
2375
2376 #[test]
2378 fn javaのtest識別子はネストした型を外側から内側へスラッシュで連結する() {
2379 let source = "public class Outer {\n @Nested\n class Inner {\n @Test\n void t() { }\n }\n}\n";
2380 let tests = jv(source);
2381 assert_eq!(tests[0].id, "tests/AuthTests:Outer/Inner:t");
2382 }
2383
2384 #[test]
2386 fn javaのtest識別子はrecord_interface_enumの中でも型名を辿る() {
2387 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";
2388 let tests = jv(source);
2389 assert_eq!(find(&tests, "a").id, "tests/AuthTests:R:a");
2390 assert_eq!(find(&tests, "b").id, "tests/AuthTests:I:b");
2391 assert_eq!(find(&tests, "c").id, "tests/AuthTests:E:c");
2392 }
2393
2394 #[test]
2396 fn javaのtest識別子はpackageを含まない() {
2397 let source =
2398 "package com.example;\n\npublic class AuthTests {\n @Test\n void t() { }\n}\n";
2399 assert_eq!(jv(source)[0].id, "tests/AuthTests:AuthTests:t");
2400 }
2401
2402 #[test]
2404 fn javaはトップレベルメソッドを対象外にする() {
2405 let source = "@Test\nvoid t() { }\n";
2406 let tests = extract_java_tests("tests/AuthTests.java", source).unwrap();
2407 assert!(tests.is_empty());
2408 let source = "class A {\n @Test\n void u() { }\n}\n";
2410 assert_eq!(jv(source).len(), 1);
2411 }
2412
2413 #[test]
2415 fn javaは匿名クラスとenum定数本体の中を対象外にする() {
2416 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";
2417 let tests = jv(source);
2418 let names: Vec<&str> = tests.iter().map(|t| t.name.as_str()).collect();
2419 assert_eq!(names, vec!["real", "outside"], "{names:?}");
2420 }
2421
2422 #[test]
2424 fn javaのcontentは注釈を含まず修飾子から始まる() {
2425 let source = jv_wrap(
2426 " // [FR-001]\n @Test\n @Tag(\"a\")\n public void t() { assertTrue(true); }",
2427 );
2428 let c = &jv(&source)[0].content;
2429 assert!(c.starts_with("public void t()"), "content = {c:?}");
2430 assert!(c.ends_with('}'));
2431 assert!(!c.contains("@Test"));
2432 assert!(!c.contains("@Tag"));
2433 assert!(!c.contains("FR-001"));
2434 }
2435
2436 #[test]
2438 fn javaのcontentは型パラメータ付きでも修飾子から始まる() {
2439 let source = jv_wrap(" @Test\n static <T> void t() { }");
2440 let c = &jv(&source)[0].content;
2441 assert!(c.starts_with("static <T> void t()"), "content = {c:?}");
2442 }
2443
2444 #[test]
2446 fn javaのcontentは修飾子なしなら型パラメータから始まる() {
2447 let source = jv_wrap(" @Test\n <T> void t() { }");
2448 let c = &jv(&source)[0].content;
2449 assert!(c.starts_with("<T> void t()"), "content = {c:?}");
2450 }
2451
2452 #[test]
2454 fn javaの本体変更はcontentに反映される() {
2455 let a = jv(&jv_wrap(" @Test\n void t() { assertTrue(true); }"));
2456 let b = jv(&jv_wrap(" @Test\n void t() { assertTrue(false); }"));
2457 assert_ne!(a[0].content, b[0].content);
2458 }
2459
2460 #[test]
2462 fn javaの注釈だけの変更はcontentを変えない() {
2463 let a = jv(&jv_wrap(" @Test\n void t() { assertTrue(true); }"));
2464 let b = jv(&jv_wrap(
2465 " @Test(timeout = 1)\n @Tag(\"a\")\n void t() { assertTrue(true); }",
2466 ));
2467 assert_eq!(a[0].content, b[0].content);
2468 }
2469
2470 #[test]
2472 fn javaは構文エラーのソースでエラーを返す() {
2473 let source = "public class A {\n @Test\n void t( {\n}\n";
2474 assert!(extract_java_tests("tests/A.java", source).is_err());
2475 }
2476
2477 fn kt(source: &str) -> Vec<ExtractedTest> {
2480 extract_kotlin_tests("tests/AuthTest.kt", source).unwrap()
2481 }
2482
2483 fn kt_wrap(body: &str) -> String {
2484 format!("class AuthTest {{\n{body}\n}}\n")
2485 }
2486
2487 #[test]
2489 fn kotlinは5種類の注釈をそれぞれtestとして検出する() {
2490 for ann in [
2491 "Test",
2492 "ParameterizedTest",
2493 "RepeatedTest",
2494 "TestFactory",
2495 "TestTemplate",
2496 ] {
2497 let source = kt_wrap(&format!(" @{ann}\n fun t() {{ }}"));
2498 let tests = kt(&source);
2499 assert_eq!(tests.len(), 1, "annotation {ann} should be detected");
2500 assert_eq!(tests[0].name, "t");
2501 }
2502 }
2503
2504 #[test]
2506 fn kotlinは修飾名と複数注釈リストとuse_site_target付きの注釈も検出する() {
2507 let source = kt_wrap(
2508 " @org.junit.jupiter.api.Test\n fun a() { }\n\n @[Test Tag(\"a\")]\n fun b() { }\n\n @get:Test\n fun c() { }",
2509 );
2510 let tests = kt(&source);
2511 assert_eq!(tests.len(), 3, "{tests:?}");
2512 find(&tests, "a");
2513 find(&tests, "b");
2514 find(&tests, "c");
2515 }
2516
2517 #[test]
2519 fn kotlinは無関係な注釈のみの関数を検出しない() {
2520 let source = kt_wrap(" @Disabled\n fun a() { }\n\n fun b() { }");
2521 assert!(kt(&source).is_empty());
2522 }
2523
2524 #[test]
2526 fn kotlinは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
2527 let source = kt_wrap(
2528 " // [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() { }",
2529 );
2530 let tests = kt(&source);
2531 assert_eq!(find(&tests, "adjacent").markers, vec!["FR-001".to_string()]);
2532 assert!(find(&tests, "blank").markers.is_empty());
2533 assert_eq!(find(&tests, "block").markers, vec!["FR-003".to_string()]);
2534 }
2535
2536 #[test]
2538 fn kotlinはkdocコメントをマーカーにしない() {
2539 let source = kt_wrap(" /** [Foo] */\n @Test\n fun t() { }");
2540 assert!(kt(&source)[0].markers.is_empty());
2541 }
2542
2543 #[test]
2545 fn kotlinは注釈と署名の間のコメントをマーカーにせずcontentにも含めない() {
2546 let source = kt_wrap(
2547 " @Test // [FR-001]\n fun a() { }\n\n @Test\n // [FR-002]\n fun b() { }",
2548 );
2549 let tests = kt(&source);
2550 for name in ["a", "b"] {
2551 let t = find(&tests, name);
2552 assert!(t.markers.is_empty(), "{name}: {:?}", t.markers);
2553 assert!(!t.content.contains("FR-00"), "{name}: {:?}", t.content);
2554 }
2555 }
2556
2557 #[test]
2559 fn kotlinのバッククォート名はバッククォートを除いた名前になる() {
2560 let source = kt_wrap(" @Test\n fun `一覧 取得 は 200`() { }");
2561 let tests = kt(&source);
2562 assert_eq!(tests[0].name, "一覧 取得 は 200");
2563 assert_eq!(tests[0].id, "tests/AuthTest:AuthTest:一覧 取得 は 200");
2564 }
2565
2566 #[test]
2568 fn kotlinのtest識別子はネストした型を外側から内側へスラッシュで連結する() {
2569 let source = "class AuthTest {\n @Nested\n inner class Inner {\n @Test\n fun t() { }\n }\n}\n";
2570 assert_eq!(kt(source)[0].id, "tests/AuthTest:AuthTest/Inner:t");
2571 }
2572
2573 #[test]
2575 fn kotlinのtest識別子はobjectとcompanion_objectを辿る() {
2576 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";
2577 let tests = kt(source);
2580 assert_eq!(find(&tests, "a").id, "tests/AuthTest:O:a");
2581 assert_eq!(find(&tests, "b").id, "tests/AuthTest:C/Companion:b");
2582 assert_eq!(find(&tests, "c").id, "tests/AuthTest:C/Factory:c");
2583 }
2584
2585 #[test]
2587 fn kotlinのトップレベル関数は型部分が空の識別子になる() {
2588 let source = "@Test\nfun top() { }\n";
2589 let tests = kt(source);
2590 assert_eq!(tests.len(), 1);
2591 assert_eq!(tests[0].id, "tests/AuthTest::top");
2592 }
2593
2594 #[test]
2596 fn kotlinのtest識別子はpackageを含まない() {
2597 let source = "package com.example\n\nclass AuthTest {\n @Test\n fun t() { }\n}\n";
2598 assert_eq!(kt(source)[0].id, "tests/AuthTest:AuthTest:t");
2599 }
2600
2601 #[test]
2603 fn kotlinはコンストラクタ注釈付きの主コンストラクタを持つクラスでもtestを取れる() {
2604 let source = "@SpringBootTest\nclass X @Autowired constructor(private val a: Int) : Base(a) {\n @Test\n fun `日本語名`() { }\n}\n";
2605 let tests = kt(source);
2606 assert_eq!(tests.len(), 1, "{tests:?}");
2607 assert_eq!(tests[0].id, "tests/AuthTest:X:日本語名");
2608 }
2609
2610 #[test]
2612 fn kotlinは関数本体内のローカル関数や匿名objectやenum定数本体やgetterを対象外にする() {
2613 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";
2614 let tests = kt(source);
2615 let names: Vec<&str> = tests.iter().map(|t| t.name.as_str()).collect();
2616 assert_eq!(names, vec!["real", "outside"], "{names:?}");
2617 }
2618
2619 #[test]
2621 fn kotlinは漢字の関数名と型名のtestを抽出し識別子とcontentが元の文字を保つ() {
2622 let source = "class AuthTest {\n @Nested\n inner class 一覧取得 {\n @Test\n fun 取得() { println(\"漢字\") }\n }\n}\n";
2623 let tests = kt(source);
2624 assert_eq!(tests.len(), 1, "{tests:?}");
2625 assert_eq!(tests[0].id, "tests/AuthTest:AuthTest/一覧取得:取得");
2626 assert_eq!(tests[0].name, "取得");
2627 assert!(tests[0].content.contains("取得"));
2628 assert!(tests[0].content.contains("漢字"));
2629 assert!(!tests[0].content.contains('x'), "{:?}", tests[0].content);
2630 }
2631
2632 #[test]
2634 fn kotlinはカタカナとハングルの関数名も抽出する() {
2635 let source = kt_wrap(" @Test\n fun テスト() { }\n\n @Test\n fun 테스트() { }");
2636 let tests = kt(&source);
2637 assert_eq!(tests.len(), 2, "{tests:?}");
2638 find(&tests, "テスト");
2639 find(&tests, "테스트");
2640 }
2641
2642 #[test]
2644 fn kotlinは非asciiの文字リテラルを含むファイルも解析できる() {
2645 let source =
2646 kt_wrap(" @Test\n fun t() {\n val c = 'あ'\n val d = '漢'\n }");
2647 let tests = kt(&source);
2648 assert_eq!(tests.len(), 1);
2649 assert!(tests[0].content.contains("'あ'"));
2650 }
2651
2652 #[test]
2654 fn kotlinはbom付きファイルも解析できる() {
2655 let source = format!("\u{feff}{}", kt_wrap(" @Test\n fun t() { }"));
2656 let tests = kt(&source);
2657 assert_eq!(tests.len(), 1);
2658 assert_eq!(tests[0].name, "t");
2659 }
2660
2661 #[test]
2663 fn kotlinのcontentは注釈を含まず修飾子から始まる() {
2664 let source = kt_wrap(
2665 " // [FR-001]\n @Test\n internal suspend fun t() { assertTrue(true) }",
2666 );
2667 let c = &kt(&source)[0].content;
2668 assert!(c.starts_with("internal suspend fun t()"), "content = {c:?}");
2669 assert!(c.ends_with('}'));
2670 assert!(!c.contains("@Test"));
2671 assert!(!c.contains("FR-001"));
2672 }
2673
2674 #[test]
2676 fn kotlinのcontentは注釈のみならfunから始まる() {
2677 let source = kt_wrap(" @Test\n @Tag(\"a\")\n fun t() { }");
2678 let c = &kt(&source)[0].content;
2679 assert!(c.starts_with("fun t()"), "content = {c:?}");
2680 }
2681
2682 #[test]
2684 fn kotlinの修飾子の後ろに書かれた注釈はcontentに含まれる() {
2685 let source = kt_wrap(" internal @Test fun t() { }");
2686 let tests = kt(&source);
2687 assert_eq!(tests.len(), 1);
2688 assert!(
2689 tests[0].content.starts_with("internal"),
2690 "{:?}",
2691 tests[0].content
2692 );
2693 assert!(tests[0].content.contains("@Test"), "{:?}", tests[0].content);
2694 }
2695
2696 #[test]
2698 fn kotlinの式本体は終端までcontentに含まれる() {
2699 let source = kt_wrap(" @Test\n fun t() = assertTrue(computeSomething())");
2700 let c = &kt(&source)[0].content;
2701 assert!(c.ends_with("computeSomething())"), "content = {c:?}");
2702 }
2703
2704 #[test]
2706 fn kotlinの本体変更はcontentに反映される() {
2707 let a = kt(&kt_wrap(" @Test\n fun t() { assertTrue(true) }"));
2708 let b = kt(&kt_wrap(" @Test\n fun t() { assertTrue(false) }"));
2709 assert_ne!(a[0].content, b[0].content);
2710 }
2711
2712 #[test]
2714 fn kotlinの注釈だけの変更はcontentを変えない() {
2715 let a = kt(&kt_wrap(" @Test\n fun t() { assertTrue(true) }"));
2716 let b = kt(&kt_wrap(
2717 " @Test\n @Tag(\"a\")\n fun t() { assertTrue(true) }",
2718 ));
2719 assert_eq!(a[0].content, b[0].content);
2720 }
2721
2722 #[test]
2724 fn kotlinは構文エラーのソースでエラーを返す() {
2725 let source = "class A { fun x() {} }\n";
2726 assert!(extract_kotlin_tests("tests/A.kt", source).is_err());
2727 }
2728
2729 #[test]
2731 fn kotlinのマーカーは元のソースから取り非asciiの角括弧は偽マーカーにならない() {
2732 let source = kt_wrap(
2733 " // [正常系]\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() { }",
2734 );
2735 let tests = kt(&source);
2736 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2737 assert!(find(&tests, "u").markers.is_empty());
2738 assert!(find(&tests, "v").markers.is_empty());
2739 }
2740
2741 #[test]
2743 fn kotlinはdocコメントを挟んでも手前のマーカーが有効になる() {
2744 let source = kt_wrap(" // [FR-001]\n /** KDoc */\n @Test\n fun b() { }");
2745 assert_eq!(kt(&source)[0].markers, vec!["FR-001".to_string()]);
2746 }
2747
2748 #[test]
2750 fn javaはdocコメントを挟んでも手前のマーカーが有効になる() {
2751 let source = jv_wrap(" // [FR-001]\n /** Javadoc */\n @Test\n void b() { }");
2752 assert_eq!(jv(&source)[0].markers, vec!["FR-001".to_string()]);
2753 }
2754
2755 #[test]
2757 fn javaは注釈同士の間のコメントをマーカーにせずcontentにも含めない() {
2758 let source =
2759 jv_wrap(" @Test\n // [FR-001]\n @Tag(\"a\")\n // 補足\n void t() { }");
2760 let t = &jv(&source)[0];
2761 assert!(t.markers.is_empty());
2762 assert!(!t.content.contains("FR-001"));
2763 assert!(!t.content.contains("補足"));
2764 }
2765
2766 #[test]
2768 fn kotlinは注釈の間と直後のコメントをcontentに含めない() {
2769 let source = kt_wrap(
2770 " @Test\n // 間のメモ\n @Tag(\"a\")\n // 直後のメモ\n fun t() { }",
2771 );
2772 let t = &kt(&source)[0];
2773 assert!(!t.content.contains("メモ"), "{:?}", t.content);
2774 assert!(t.content.starts_with("fun t()"), "{:?}", t.content);
2775 }
2776
2777 #[test]
2779 fn kotlinは注釈同士の間のコメントをマーカーにしない() {
2780 let source = kt_wrap(" @Test\n // [FR-001]\n @Tag(\"a\")\n fun t() { }");
2781 assert!(kt(&source)[0].markers.is_empty());
2782 }
2783
2784 #[test]
2786 fn kotlinはdata_classとinterfaceの直下のtestも辿る() {
2787 let source = "data class D(val a: Int) {\n @Test\n fun a() { }\n}\n\ninterface I {\n @Test\n fun b() { }\n}\n";
2788 let tests = kt(source);
2789 assert_eq!(find(&tests, "a").id, "tests/AuthTest:D:a");
2790 assert_eq!(find(&tests, "b").id, "tests/AuthTest:I:b");
2791 }
2792
2793 #[test]
2795 fn kotlinは制御文字や空を含む名前のtestを無視する() {
2796 let source =
2797 kt_wrap(" @Test\n fun `tab\there`() { }\n\n @Test\n fun `ok name`() { }");
2798 let tests = kt(&source);
2799 assert_eq!(tests.len(), 1, "{tests:?}");
2800 assert_eq!(tests[0].name, "ok name");
2801 }
2802
2803 #[test]
2805 fn kotlinは注釈2つで基底型なしのクラス内の注釈付きinner_classでもtestを取れる() {
2806 let source = "@A\n@C\nclass P {\n @Nested\n inner class X {\n // [FR-001]\n @Test\n fun t() { }\n }\n}\n";
2807 let tests = kt(source);
2808 assert_eq!(tests.len(), 1, "{tests:?}");
2809 assert_eq!(tests[0].id, "tests/AuthTest:P/X:t");
2810 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
2811 }
2812
2813 #[test]
2815 fn kotlinはnestedを含む複数注釈クラスでも非asciiの名前を元のまま取る() {
2816 let source = "@A\n@C\nclass P {\n @Nested\n inner class 一覧取得 {\n @Test\n fun `取得 する`() { }\n }\n}\n";
2817 let tests = kt(source);
2818 assert_eq!(tests[0].id, "tests/AuthTest:P/一覧取得:取得 する");
2819 }
2820
2821 #[test]
2823 fn kotlinは宣言以外のトップレベル構造やキーワードを含む文字列でもok空になる() {
2824 for source in [
2825 "package a\n// helper for class X\nval x = 1\n",
2826 "package a\nval fixture = object : Runnable {\n override fun run() {}\n}\n",
2827 "package a\nval f = fun() { }\n",
2828 "package a\nconst val NAME = \"class\"\n",
2829 ] {
2830 let tests = kt(source);
2831 assert!(tests.is_empty(), "{source:?}: {tests:?}");
2832 }
2833 }
2834
2835 #[test]
2837 fn kotlinは文法の不具合で宣言が認識されなかった場合に構文エラーを返す() {
2838 let source = "@A\n@C\nclass P {\n @Nested\n inner class X {\n }\n}\n";
2839 assert!(kotlin_extract("tests/P.kt", source, true).is_ok());
2840 assert!(kotlin_extract("tests/P.kt", source, false).is_err());
2841 }
2842
2843 #[test]
2845 fn kotlinはshebangやimportやtypealiasなど宣言以外の通常の構造でok空になる() {
2846 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";
2847 assert!(kt(source).is_empty());
2848 }
2849
2850 #[test]
2854 fn rustはdocコメントを挟んでもマーカーを取る() {
2855 let tests = rs("// [FR-001]\n/// doc\n#[test]\nfn t() {}\n", &[]);
2856 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2857 }
2858
2859 #[test]
2861 fn rustの外側docコメントはマーカーにならない() {
2862 let tests = rs("/// [FR-001]\n#[test]\nfn t() {}\n", &[]);
2863 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2864 }
2865
2866 #[test]
2868 fn rustの内側docコメントはマーカーにならない() {
2869 let tests = rs("mod m {\n//! [FR-001]\n#[test]\nfn t() {}\n}\n", &[]);
2870 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2871 }
2872
2873 #[test]
2875 fn rustはブロックdocコメントを挟んでもマーカーを取る() {
2876 let tests = rs("// [FR-001]\n/** doc */\n#[test]\nfn t() {}\n", &[]);
2877 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2878 }
2879
2880 #[test]
2882 fn rustのスラッシュ4つのコメントは通常コメントだが余分な記号が本文に残りマーカーにならない() {
2883 let tests = rs("//// [FR-001]\n#[test]\nfn t() {}\n", &[]);
2884 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2885 }
2886
2887 #[test]
2889 fn rustはdocコメントの後に空行があるとマーカーを取らない() {
2890 let tests = rs("// [FR-001]\n/// doc\n\n#[test]\nfn t() {}\n", &[]);
2891 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2892 }
2893
2894 #[test]
2896 fn rustはcrlfでもdocコメントを挟んでマーカーを取る() {
2897 let tests = rs("// [FR-001]\r\n/// doc\r\n#[test]\r\nfn t() {}\r\n", &[]);
2898 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2899 }
2900
2901 #[test]
2903 fn goは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2904 let source =
2905 "package a\n\nfunc TestA(t *testing.T) {} // [FR-001]\nfunc TestB(t *testing.T) {}\n";
2906 let tests = extract_go_tests("a/x_test.go", source).unwrap();
2907 assert!(find(&tests, "TestB").markers.is_empty());
2908 }
2909
2910 #[test]
2912 fn rustは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2913 let tests = rs("fn x() {} // [FR-001]\n#[test]\nfn t() {}\n", &[]);
2914 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2915 }
2916
2917 #[test]
2919 fn csharpは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2920 let source =
2921 cs_wrap(" public void X() {} // [FR-001]\n [Fact]\n public void T() {}");
2922 let tests = cs(&source);
2923 assert!(find(&tests, "T").markers.is_empty());
2924 }
2925
2926 #[test]
2928 fn typescriptは前の文の末尾行コメントを次のtestのマーカーにしない() {
2929 let tests = ts("it(\"a\", () => {}); // [FR-001]\nit(\"b\", () => {});\n");
2930 assert!(find(&tests, "b").markers.is_empty());
2931 }
2932
2933 #[test]
2935 fn javaは前の宣言の末尾行コメントを次のtestのマーカーにしない() {
2936 let source = jv_wrap(" void x() {} // [FR-001]\n @Test\n void t() {}");
2937 let tests = jv(&source);
2938 assert!(find(&tests, "t").markers.is_empty());
2939 }
2940
2941 #[test]
2943 fn kotlinは非ascii識別子の後の末尾行コメントを次のtestのマーカーにしない() {
2944 let source = kt_wrap(" fun `日本語`() {} // [FR-001]\n @Test\n fun t() {}");
2945 let tests = kt(&source);
2946 assert!(find(&tests, "t").markers.is_empty());
2947 }
2948
2949 #[test]
2951 fn rustはインデント付きの行頭コメントをマーカーにする() {
2952 let tests = rs(
2953 "mod m {\n // [FR-001]\n #[test]\n fn t() {}\n}\n",
2954 &[],
2955 );
2956 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2957 }
2958
2959 #[test]
2961 fn typescriptは同じ行に複数のコメントがあると2つ目以降は行頭でないので打ち切る() {
2962 let tests = ts("/* [FR-001] */ // note\nit(\"b\", () => {});\n");
2963 assert!(find(&tests, "b").markers.is_empty());
2964 }
2965
2966 #[test]
2968 fn rustはbomの直後の1行目マーカーを取る() {
2969 let tests = rs("\u{feff}// [FR-001]\n#[test]\nfn t() {}\n", &[]);
2970 assert_eq!(find(&tests, "t").markers, vec!["FR-001".to_string()]);
2971 }
2972
2973 #[test]
2975 fn goはbomの直後の1行目マーカーを取る() {
2976 let source = "\u{feff}// [FR-001]\nfunc TestA(t *testing.T) {}\n";
2977 let tests = extract_go_tests("a/x_test.go", source).unwrap();
2978 assert_eq!(find(&tests, "TestA").markers, vec!["FR-001".to_string()]);
2979 }
2980
2981 #[test]
2983 fn rustの内側ブロックdocコメントはマーカーにならない() {
2984 let tests = rs("mod m {\n/*! [FR-001] */\n#[test]\nfn t() {}\n}\n", &[]);
2985 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2986 }
2987
2988 #[test]
2990 fn rustのスラッシュ星3つのコメントは通常コメントだが余分な記号が本文に残りマーカーにならない() {
2991 let tests = rs("/*** [FR-001] */\n#[test]\nfn t() {}\n", &[]);
2992 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
2993 }
2994
2995 #[test]
2997 fn rustの外側ブロックdocコメントはマーカーにならない() {
2998 let tests = rs("/** [FR-001] */\n#[test]\nfn t() {}\n", &[]);
2999 assert_eq!(find(&tests, "t").markers, Vec::<String>::new());
3000 }
3001
3002 fn marker_cases() -> Vec<(&'static str, Vec<&'static str>)> {
3006 vec![
3007 ("// [FR-001]", vec!["FR-001"]),
3008 ("// [FR-001] ", vec!["FR-001"]),
3009 ("// [FR-001] [FR-002]", vec!["FR-001", "FR-002"]),
3010 ("// [FR-001]\n// [FR-002]", vec!["FR-001", "FR-002"]),
3011 ("//// [FR-001]", vec![]),
3012 ("/*** [FR-001] */", vec![]),
3013 ("// [FR-001] 説明", vec![]),
3014 ("// 説明 [FR-001]", vec![]),
3015 ("// 説明 [0] 説明", vec![]),
3016 ("// [FR-001] [正常系]", vec![]),
3017 ("// [FR-001][FR-002]", vec![]),
3018 ("//", vec![]),
3019 ("/* [FR-001] */", vec!["FR-001"]),
3020 ("/*\n * [FR-001]\n */", vec!["FR-001"]),
3021 (
3022 "/*\n * [FR-001]\n * [FR-002]\n */",
3023 vec!["FR-001", "FR-002"],
3024 ),
3025 ("/* [FR-001] [FR-002] */", vec!["FR-001", "FR-002"]),
3026 ("/*\n * [FR-001] 説明\n */", vec![]),
3027 ("/* 説明 [0] */", vec![]),
3028 ]
3029 }
3030
3031 fn assert_marker_cases(markers_of: impl Fn(&str) -> Vec<String>) {
3032 for (comment, expected) in marker_cases() {
3033 assert_eq!(markers_of(comment), expected, "comment: {comment:?}");
3034 }
3035 }
3036
3037 #[test]
3039 fn goはコメントが単一トークンだけのときマーカーにする() {
3040 assert_marker_cases(|c| {
3041 let source = format!("{c}\nfunc TestA(t *testing.T) {{}}\n");
3042 find(&extract_go_tests("a/x_test.go", &source).unwrap(), "TestA")
3043 .markers
3044 .clone()
3045 });
3046 }
3047
3048 #[test]
3050 fn rustはコメントが単一トークンだけのときマーカーにする() {
3051 assert_marker_cases(|c| {
3052 rs(&format!("{c}\n#[test]\nfn t() {{}}\n"), &[])[0]
3053 .markers
3054 .clone()
3055 });
3056 }
3057
3058 #[test]
3060 fn csharpはコメントが単一トークンだけのときマーカーにする() {
3061 assert_marker_cases(|c| {
3062 cs(&cs_wrap(&format!("{c}\n[Fact]\npublic void T() {{ }}")))[0]
3063 .markers
3064 .clone()
3065 });
3066 }
3067
3068 #[test]
3070 fn javaはコメントが単一トークンだけのときマーカーにする() {
3071 assert_marker_cases(|c| {
3072 jv(&jv_wrap(&format!("{c}\n@Test\nvoid t() {{ }}")))[0]
3073 .markers
3074 .clone()
3075 });
3076 }
3077
3078 #[test]
3080 fn kotlinはコメントが単一トークンだけのときマーカーにする() {
3081 assert_marker_cases(|c| {
3082 kt(&kt_wrap(&format!("{c}\n@Test\nfun t() {{ }}")))[0]
3083 .markers
3084 .clone()
3085 });
3086 }
3087
3088 #[test]
3090 fn typescriptはコメントが単一トークンだけのときマーカーにする() {
3091 assert_marker_cases(|c| {
3092 ts(&format!("{c}\nit(\"t\", () => {{}});\n"))[0]
3093 .markers
3094 .clone()
3095 });
3096 }
3097
3098 #[test]
3100 fn dartはコメントが単一トークンだけのときマーカーにする() {
3101 assert_marker_cases(|c| {
3102 dt(&format!("void main() {{\n{c}\ntest('t', () {{}});\n}}\n"))[0]
3103 .markers
3104 .clone()
3105 });
3106 }
3107 #[test]
3109 fn csharpの三連スラッシュコメントも単一トークンだけならマーカーにする() {
3110 let tests = cs(&cs_wrap("/// [FR-001]\n[Fact]\npublic void T() { }"));
3111 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
3112 let tests = cs(&cs_wrap("/// [FR-001] 説明\n[Fact]\npublic void T() { }"));
3113 assert!(tests[0].markers.is_empty());
3114 }
3115
3116 fn dt(source: &str) -> Vec<ExtractedTest> {
3119 extract_dart_tests("mobile/test/a_test.dart", source).unwrap()
3120 }
3121
3122 #[test]
3124 fn dartはtest_dartで終わるパスだけをテストファイルとして扱う() {
3125 assert!(is_dart_test_file("test/a_test.dart"));
3126 assert!(is_dart_test_file("a_test.dart"));
3127 assert!(!is_dart_test_file("test/a.dart"));
3128 assert!(!is_dart_test_file("test/a_test.dart.bak"));
3129 assert!(!is_dart_test_file("test/a_test.rs"));
3130 assert!(!is_dart_test_file("test/atest.dart"));
3131 }
3132
3133 #[test]
3135 fn dartはmain直下のtestとtestwidgetsを検出する() {
3136 let tests =
3137 dt("void main() {\n test('a', () {});\n testWidgets('b', (tester) async {});\n}\n");
3138 assert_eq!(tests.len(), 2);
3139 find(&tests, "a");
3140 find(&tests, "b");
3141 }
3142
3143 #[test]
3145 fn dartはgroupの入れ子を辿る() {
3146 let tests = dt("void main() {\n group('outer', () {\n group('inner', () {\n test('t', () {});\n });\n test('u', () {});\n });\n}\n");
3147 assert_eq!(tests.len(), 2);
3148 assert_eq!(find(&tests, "t").id, "mobile/test/a_test:outer/inner:t");
3149 assert_eq!(find(&tests, "u").id, "mobile/test/a_test:outer:u");
3150 }
3151
3152 #[test]
3154 fn dartのtest識別子はgroupの外なら空のgroupパスになる() {
3155 let tests = dt("void main() { test('t', () {}); }\n");
3156 assert_eq!(tests[0].id, "mobile/test/a_test::t");
3157 }
3158
3159 #[test]
3161 fn dartはfuture_void_main_asyncでも検出する() {
3162 let tests = dt("Future<void> main() async {\n test('a', () {});\n}\n");
3163 assert_eq!(tests.len(), 1);
3164 let tests = dt("main() { test('a', () {}); }\n");
3165 assert_eq!(tests.len(), 1);
3166 }
3167
3168 #[test]
3170 fn dartはasyncコールバックとアロー本体のtestも検出する() {
3171 let tests = dt("void main() {\n test('a', () async {});\n test('b', () => expect(1, 1));\n group('g', () async {\n test('c', () {});\n });\n}\n");
3172 assert_eq!(tests.len(), 3);
3173 assert_eq!(find(&tests, "c").id, "mobile/test/a_test:g:c");
3174 }
3175
3176 #[test]
3178 fn dartのgroupはアロー本体だと中へ降りない() {
3179 let tests = dt("void main() {\n group('g', () => test('t', () {}));\n}\n");
3180 assert!(tests.is_empty(), "{tests:?}");
3181 }
3182
3183 #[test]
3185 fn dartは名前付き引数付きでも1件のtestとして扱う() {
3186 let tests = dt("void main() {\n test('a', () {}, skip: true, timeout: Timeout.none);\n group('g', () {\n test('b', () {});\n }, skip: 'x');\n}\n");
3187 assert_eq!(tests.len(), 2);
3188 assert_eq!(find(&tests, "b").id, "mobile/test/a_test:g:b");
3189 }
3190
3191 #[test]
3193 fn dartはraw文字列を引用符の内側のソース表記のまま名前にする() {
3194 let tests = dt("void main() {\n test(r'a\\n$b', () {});\n test(r\"c\\d\", () {});\n}\n");
3195 assert_eq!(tests.len(), 2);
3196 find(&tests, "a\\n$b");
3197 find(&tests, "c\\d");
3198 }
3199
3200 #[test]
3202 fn dartは隣接文字列連結を各部分の内側で連結して名前にする() {
3203 let tests = dt("void main() {\n test('abc '\n \"def \"\n r'g\\h', () {});\n}\n");
3204 assert_eq!(tests.len(), 1);
3205 assert_eq!(tests[0].name, "abc def g\\h");
3206 }
3207
3208 #[test]
3210 fn dartはエスケープを解釈せずソース表記のまま名前にする() {
3211 let tests = dt("void main() {\n test('it\\'s \\$x', () {});\n}\n");
3212 assert_eq!(tests.len(), 1);
3213 assert_eq!(tests[0].name, "it\\'s \\$x");
3214 }
3215
3216 #[test]
3218 fn dartは補間を含む名前のtestとgroupを無視する() {
3219 let tests = dt("void main() {\n test('a $x', () {});\n test('b ${y}', () {});\n test('c' 'd$z', () {});\n group('g$x', () {\n test('t', () {});\n });\n}\n");
3220 assert!(tests.is_empty(), "{tests:?}");
3221 }
3222
3223 #[test]
3225 fn dartは三重引用符の名前を無視する() {
3226 let tests = dt("void main() {\n test('''a''', () {});\n test(\"\"\"b\"\"\", () {});\n test(r'''c''', () {});\n test('d' '''e''', () {});\n}\n");
3227 assert!(tests.is_empty(), "{tests:?}");
3228 }
3229
3230 #[test]
3232 fn dartは無効な名前のgroup配下を走査しない() {
3233 for name in ["''", "'''x'''", "'a\u{2028}b'", "'a\u{1}b'"] {
3234 let source = format!(
3235 "void main() {{\n group({name}, () {{\n test('t', () {{}});\n }});\n}}\n"
3236 );
3237 let tests = dt(&source);
3238 assert!(tests.is_empty(), "{name:?}: {tests:?}");
3239 }
3240 }
3241
3242 #[test]
3244 fn dartは行区切り文字や制御文字を含むtest名を無視する() {
3245 let tests = dt("void main() {\n test('a\u{2028}b', () {});\n test('c\u{1}d', () {});\n test('ok', () {});\n}\n");
3246 assert_eq!(tests.len(), 1, "{tests:?}");
3247 assert_eq!(tests[0].name, "ok");
3248 }
3249
3250 #[test]
3252 fn dartは空文字名を無視する() {
3253 let tests = dt(
3254 "void main() {\n test('', () {});\n test('' '', () {});\n test('ok', () {});\n}\n",
3255 );
3256 assert_eq!(tests.len(), 1);
3257 assert_eq!(tests[0].name, "ok");
3258 }
3259
3260 #[test]
3262 fn dartは第1引数が文字列リテラルでない呼び出しを無視する() {
3263 let tests = dt("void main() {\n test(name, () {});\n group(name, () {\n test('t', () {});\n });\n test();\n test(skip: true);\n}\n");
3264 assert!(tests.is_empty(), "{tests:?}");
3265 }
3266
3267 #[test]
3269 fn dartは修飾付きやメソッドチェーンやawait付きの呼び出しを無視する() {
3270 let tests = dt("import 'package:test/test.dart' as t;\nvoid main() {\n t.test('a', () {});\n t.group('g', () {\n test('b', () {});\n });\n test('c', () {}).foo();\n await test('d', () {});\n test('e', () {});\n}\n");
3271 assert_eq!(tests.len(), 1, "{tests:?}");
3272 assert_eq!(tests[0].name, "e");
3273 }
3274
3275 #[test]
3277 fn dartはtestとgroupとtestwidgets以外の呼び出しを無視する() {
3278 let tests =
3279 dt("void main() {\n setUp(() {});\n it('a', () {});\n Test('b', () {});\n}\n");
3280 assert!(tests.is_empty(), "{tests:?}");
3281 }
3282
3283 #[test]
3285 fn dartはforとifと関数とtest本体の中のtestを無視する() {
3286 let source = "void helper() {\n test('h', () {});\n}\nvoid main() {\n for (final x in [1]) {\n test('f', () {});\n }\n if (true) {\n test('i', () {});\n }\n test('outer', () {\n test('inner', () {});\n });\n void local() {\n test('l', () {});\n }\n group('g', () {\n for (var i = 0; i < 2; i++) {\n test('gf', () {});\n }\n test('ok', () {});\n });\n}\n";
3287 let tests = dt(source);
3288 let mut names: Vec<&str> = tests.iter().map(|t| t.name.as_str()).collect();
3289 names.sort();
3290 assert_eq!(names, vec!["ok", "outer"], "{tests:?}");
3291 }
3292
3293 #[test]
3295 fn dartはmain以外のトップレベル関数を入口にしない() {
3296 let tests =
3297 dt("void other() {\n test('a', () {});\n}\nvoid mainx() {\n test('b', () {});\n}\n");
3298 assert!(tests.is_empty(), "{tests:?}");
3299 }
3300
3301 #[test]
3303 fn dartはmainが無いファイルを0件にする() {
3304 assert!(dt("int x = 1;\n").is_empty());
3305 assert!(dt("").is_empty());
3306 }
3307
3308 #[test]
3310 fn dartは直前に隣接するコメントをマーカーとして抽出し空行を挟むと無視する() {
3311 let source = "void main() {\n // [FR-001]\n test('a', () {});\n\n // [FR-002]\n\n test('b', () {});\n /* [FR-003] */\n test('c', () {});\n}\n";
3312 let tests = dt(source);
3313 assert_eq!(find(&tests, "a").markers, vec!["FR-001".to_string()]);
3314 assert!(find(&tests, "b").markers.is_empty());
3315 assert_eq!(find(&tests, "c").markers, vec!["FR-003".to_string()]);
3316 }
3317
3318 #[test]
3320 fn dartはgroup内のtestでも直前のコメントをマーカーにする() {
3321 let tests = dt(
3322 "void main() {\n group('g', () {\n // [FR-001]\n test('t', () {});\n });\n}\n",
3323 );
3324 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
3325 }
3326
3327 #[test]
3329 fn dartのドキュメントコメントはマーカーにならない() {
3330 let source = "void main() {\n /// [FR-001]\n test('a', () {});\n /** [FR-002] */\n test('b', () {});\n //// [FR-003]\n test('c', () {});\n}\n";
3331 let tests = dt(source);
3332 for n in ["a", "b", "c"] {
3333 assert!(find(&tests, n).markers.is_empty(), "{n}");
3334 }
3335 }
3336
3337 #[test]
3339 fn dartはマーカーとtestの間にdocコメントが挟まってもマーカーを有効にする() {
3340 let source = "void main() {\n // [FR-001]\n /// 説明 [Foo]\n test('a', () {});\n // [FR-002]\n /** doc */\n test('b', () {});\n}\n";
3341 let tests = dt(source);
3342 assert_eq!(find(&tests, "a").markers, vec!["FR-001".to_string()]);
3343 assert_eq!(find(&tests, "b").markers, vec!["FR-002".to_string()]);
3344 }
3345
3346 #[test]
3348 fn dartは前の文の末尾行コメントを次のtestのマーカーにしない() {
3349 let tests = dt("void main() {\n test('a', () {}); // [FR-001]\n test('b', () {});\n}\n");
3350 assert!(find(&tests, "b").markers.is_empty());
3351 }
3352
3353 #[test]
3355 fn dartのcontentは呼び出し文全体でマーカーコメントを含まない() {
3356 let tests = dt("void main() {\n // [FR-001]\n test('a', () { expect(1, 1); });\n}\n");
3357 assert_eq!(tests[0].content, "test('a', () { expect(1, 1); });");
3358 }
3359
3360 #[test]
3362 fn dartの本体変更はcontentに反映される() {
3363 let a = dt("void main() { test('a', () { expect(1, 1); }); }\n");
3364 let b = dt("void main() { test('a', () { expect(1, 2); }); }\n");
3365 assert_ne!(a[0].content, b[0].content);
3366 }
3367
3368 #[test]
3370 fn dartは同名のtestをそのまま複数返す() {
3371 let tests = dt("void main() {\n test('a', () {});\n test('a', () {});\n}\n");
3372 assert_eq!(tests.len(), 2);
3373 assert_eq!(tests[0].id, tests[1].id);
3374 }
3375
3376 #[test]
3378 fn dartは構文エラーのソースでエラーを返す() {
3379 let source = "void main() {\n test('a', () {\n}\n";
3380 assert!(extract_dart_tests("mobile/test/a_test.dart", source).is_err());
3381 }
3382
3383 const KOTLIN_KDOC_FIXTURE: &str = include_str!("../fixtures/kotlin_kdoc_annotation.kt");
3386 const KOTLIN_SUPPORT_FIXTURE: &str = include_str!("../fixtures/kotlin_annotation_support.kt");
3387
3388 #[test]
3390 fn kotlinは直前の宣言とコメントの後の位置引数付き注釈でも解析できる() {
3391 let tests = kt("fun g() {}\n// a\n@S(\"L\")\nfun f() {}\n");
3392 assert!(tests.is_empty(), "{tests:?}");
3393 }
3394
3395 #[test]
3397 fn kotlinは非asciiのkdocと位置引数付き注釈の組み合わせでも解析できる() {
3398 let tests = kt(KOTLIN_KDOC_FIXTURE);
3399 assert!(tests.is_empty(), "{tests:?}");
3400 }
3401
3402 #[test]
3404 fn kotlinは位置引数付き注釈を持つサポートコードのファイルを解析できる() {
3405 let tests = kt(KOTLIN_SUPPORT_FIXTURE);
3406 assert!(tests.is_empty(), "{tests:?}");
3407 }
3408
3409 #[test]
3411 fn kotlinは位置引数付き注釈の後ろにtestが続いてもtestを取る() {
3412 let tests = kt("fun g(){}\n// a\n@DisplayName(\"x\")\n@Test\nfun f(){}\n");
3413 assert_eq!(tests.len(), 1, "{tests:?}");
3414 assert_eq!(tests[0].name, "f");
3415 }
3416
3417 #[test]
3419 fn kotlinは位置引数付き注釈の後ろのtestのマーカーも取る() {
3420 let tests = kt("fun g(){}\n// a\n// [FR-001]\n@DisplayName(\"x\")\n@Test\nfun f(){}\n");
3421 assert_eq!(tests.len(), 1, "{tests:?}");
3422 assert_eq!(tests[0].markers, vec!["FR-001".to_string()]);
3423 }
3424
3425 #[test]
3427 fn kotlinは位置引数付き注釈のある本当の構文エラーを引き続きエラーにする() {
3428 let source = "fun g() {}\n// a\n@S(\"L\")\nfun f( {\n";
3429 assert!(extract_kotlin_tests("tests/A.kt", source).is_err());
3430 }
3431
3432 #[test]
3434 fn kotlinは位置引数付き注釈が複数あってもtestを取る() {
3435 let tests = kt("fun g(){}\n// a\n@A(\"x\")\n@B(\"y\")\n@Test\nfun f(){}\n");
3436 assert_eq!(tests.len(), 1, "{tests:?}");
3437 }
3438}