1use std::collections::{HashMap, HashSet};
2use std::path::Path;
3use std::sync::OnceLock;
4
5use streaming_iterator::StreamingIterator;
6use tree_sitter::{Query, QueryCursor};
7
8use exspec_core::observe::{
9 BarrelReExport, FileMapping, ImportMapping, MappingStrategy, ObserveExtractor,
10 ProductionFunction,
11};
12
13use super::RustExtractor;
14
15const PRODUCTION_FUNCTION_QUERY: &str = include_str!("../queries/production_function.scm");
16static PRODUCTION_FUNCTION_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
17
18const CFG_TEST_QUERY: &str = include_str!("../queries/cfg_test.scm");
19static CFG_TEST_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
20
21const EXPORTED_SYMBOL_QUERY: &str = include_str!("../queries/exported_symbol.scm");
22static EXPORTED_SYMBOL_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
23
24fn rust_language() -> tree_sitter::Language {
25 tree_sitter_rust::LANGUAGE.into()
26}
27
28fn cached_query<'a>(lock: &'a OnceLock<Query>, source: &str) -> &'a Query {
29 lock.get_or_init(|| Query::new(&rust_language(), source).expect("invalid query"))
30}
31
32pub fn test_stem(path: &str) -> Option<&str> {
42 let file_name = Path::new(path).file_name()?.to_str()?;
43 let stem = file_name.strip_suffix(".rs")?;
44
45 if let Some(rest) = stem.strip_prefix("test_") {
47 if !rest.is_empty() {
48 return Some(rest);
49 }
50 }
51
52 if let Some(rest) = stem.strip_suffix("_test") {
54 if !rest.is_empty() {
55 return Some(rest);
56 }
57 }
58
59 let normalized = path.replace('\\', "/");
61 if normalized.starts_with("tests/") || normalized.contains("/tests/") {
62 if stem != "mod" && stem != "main" {
64 return Some(stem);
65 }
66 }
67
68 None
69}
70
71pub fn production_stem(path: &str) -> Option<&str> {
78 let file_name = Path::new(path).file_name()?.to_str()?;
79 let stem = file_name.strip_suffix(".rs")?;
80
81 if stem == "lib" || stem == "mod" || stem == "main" {
83 return None;
84 }
85
86 if test_stem(path).is_some() {
88 return None;
89 }
90
91 if file_name == "build.rs" {
93 return None;
94 }
95
96 Some(stem)
97}
98
99pub fn is_non_sut_helper(file_path: &str, _is_known_production: bool) -> bool {
101 let normalized = file_path.replace('\\', "/");
102 let file_name = Path::new(&normalized)
103 .file_name()
104 .and_then(|f| f.to_str())
105 .unwrap_or("");
106
107 if file_name == "build.rs" {
109 return true;
110 }
111
112 if normalized.contains("/tests/common/") || normalized.starts_with("tests/common/") {
114 return true;
115 }
116
117 if normalized.starts_with("benches/") || normalized.contains("/benches/") {
119 return true;
120 }
121
122 if normalized.starts_with("examples/") || normalized.contains("/examples/") {
124 return true;
125 }
126
127 false
128}
129
130pub fn detect_inline_tests(source: &str) -> bool {
136 let mut parser = RustExtractor::parser();
137 let tree = match parser.parse(source, None) {
138 Some(t) => t,
139 None => return false,
140 };
141 let source_bytes = source.as_bytes();
142 let query = cached_query(&CFG_TEST_QUERY_CACHE, CFG_TEST_QUERY);
143
144 let attr_name_idx = query.capture_index_for_name("attr_name");
145 let cfg_arg_idx = query.capture_index_for_name("cfg_arg");
146 let cfg_test_attr_idx = query.capture_index_for_name("cfg_test_attr");
147
148 let mut cursor = QueryCursor::new();
149 let mut matches = cursor.matches(query, tree.root_node(), source_bytes);
150
151 while let Some(m) = matches.next() {
152 let mut is_cfg = false;
153 let mut is_test = false;
154 let mut attr_node: Option<tree_sitter::Node> = None;
155
156 for cap in m.captures {
157 let text = cap.node.utf8_text(source_bytes).unwrap_or("");
158 if attr_name_idx == Some(cap.index) && text == "cfg" {
159 is_cfg = true;
160 }
161 if cfg_arg_idx == Some(cap.index) && text == "test" {
162 is_test = true;
163 }
164 if cfg_test_attr_idx == Some(cap.index) {
165 attr_node = Some(cap.node);
166 }
167 }
168
169 if is_cfg && is_test {
170 if let Some(attr) = attr_node {
172 let mut sibling = attr.next_sibling();
173 while let Some(s) = sibling {
174 if s.kind() == "mod_item" {
175 return true;
176 }
177 if s.kind() != "attribute_item" {
178 break;
179 }
180 sibling = s.next_sibling();
181 }
182 }
183 }
184 }
185
186 false
187}
188
189impl ObserveExtractor for RustExtractor {
194 fn extract_production_functions(
195 &self,
196 source: &str,
197 file_path: &str,
198 ) -> Vec<ProductionFunction> {
199 let mut parser = Self::parser();
200 let tree = match parser.parse(source, None) {
201 Some(t) => t,
202 None => return Vec::new(),
203 };
204 let source_bytes = source.as_bytes();
205 let query = cached_query(&PRODUCTION_FUNCTION_QUERY_CACHE, PRODUCTION_FUNCTION_QUERY);
206
207 let name_idx = query.capture_index_for_name("name");
208 let class_name_idx = query.capture_index_for_name("class_name");
209 let method_name_idx = query.capture_index_for_name("method_name");
210 let function_idx = query.capture_index_for_name("function");
211 let method_idx = query.capture_index_for_name("method");
212
213 let cfg_test_ranges = find_cfg_test_ranges(source);
215
216 let mut cursor = QueryCursor::new();
217 let mut matches = cursor.matches(query, tree.root_node(), source_bytes);
218 let mut result = Vec::new();
219
220 while let Some(m) = matches.next() {
221 let mut fn_name: Option<String> = None;
222 let mut class_name: Option<String> = None;
223 let mut line: usize = 1;
224 let mut is_exported = false;
225 let mut fn_start_byte: usize = 0;
226
227 for cap in m.captures {
228 let text = cap.node.utf8_text(source_bytes).unwrap_or("").to_string();
229 let node_line = cap.node.start_position().row + 1;
230
231 if name_idx == Some(cap.index) || method_name_idx == Some(cap.index) {
232 fn_name = Some(text);
233 line = node_line;
234 } else if class_name_idx == Some(cap.index) {
235 class_name = Some(text);
236 }
237
238 if function_idx == Some(cap.index) || method_idx == Some(cap.index) {
240 fn_start_byte = cap.node.start_byte();
241 is_exported = has_pub_visibility(cap.node);
242 }
243 }
244
245 if let Some(name) = fn_name {
246 if cfg_test_ranges
248 .iter()
249 .any(|(start, end)| fn_start_byte >= *start && fn_start_byte < *end)
250 {
251 continue;
252 }
253
254 result.push(ProductionFunction {
255 name,
256 file: file_path.to_string(),
257 line,
258 class_name,
259 is_exported,
260 });
261 }
262 }
263
264 let mut seen = HashSet::new();
266 result.retain(|f| seen.insert((f.name.clone(), f.class_name.clone())));
267
268 result
269 }
270
271 fn extract_imports(&self, source: &str, file_path: &str) -> Vec<ImportMapping> {
272 let all = self.extract_all_import_specifiers(source);
274 let mut result = Vec::new();
275 for (specifier, symbols) in all {
276 for sym in &symbols {
277 result.push(ImportMapping {
278 symbol_name: sym.clone(),
279 module_specifier: specifier.clone(),
280 file: file_path.to_string(),
281 line: 1,
282 symbols: symbols.clone(),
283 });
284 }
285 }
286 result
287 }
288
289 fn extract_all_import_specifiers(&self, source: &str) -> Vec<(String, Vec<String>)> {
290 extract_import_specifiers_with_crate_name(source, None)
291 }
292
293 fn extract_barrel_re_exports(&self, source: &str, file_path: &str) -> Vec<BarrelReExport> {
294 if !self.is_barrel_file(file_path) {
295 return Vec::new();
296 }
297
298 let mut parser = Self::parser();
299 let tree = match parser.parse(source, None) {
300 Some(t) => t,
301 None => return Vec::new(),
302 };
303 let source_bytes = source.as_bytes();
304 let root = tree.root_node();
305 let mut result = Vec::new();
306
307 for i in 0..root.child_count() {
308 let child = root.child(i).unwrap();
309
310 if child.kind() == "mod_item" && has_pub_visibility(child) {
312 let has_body = child.child_by_field_name("body").is_some();
314 if !has_body {
315 if let Some(name_node) = child.child_by_field_name("name") {
316 let mod_name = name_node.utf8_text(source_bytes).unwrap_or("");
317 result.push(BarrelReExport {
318 symbols: Vec::new(),
319 from_specifier: format!("./{mod_name}"),
320 wildcard: true,
321 namespace_wildcard: false,
322 });
323 }
324 }
325 }
326
327 if child.kind() == "use_declaration" && has_pub_visibility(child) {
329 if let Some(arg) = child.child_by_field_name("argument") {
330 extract_pub_use_re_exports(&arg, source_bytes, &mut result);
331 }
332 }
333
334 if child.kind() == "macro_invocation" {
336 for j in 0..child.child_count() {
337 if let Some(tt) = child.child(j) {
338 if tt.kind() == "token_tree" {
339 let tt_text = tt.utf8_text(source_bytes).unwrap_or("");
340 extract_re_exports_from_text(tt_text, &mut result);
341 }
342 }
343 }
344 }
345 }
346
347 result
348 }
349
350 fn source_extensions(&self) -> &[&str] {
351 &["rs"]
352 }
353
354 fn index_file_names(&self) -> &[&str] {
355 &["mod.rs", "lib.rs"]
356 }
357
358 fn production_stem<'a>(&self, path: &'a str) -> Option<&'a str> {
359 production_stem(path)
360 }
361
362 fn test_stem<'a>(&self, path: &'a str) -> Option<&'a str> {
363 test_stem(path)
364 }
365
366 fn is_non_sut_helper(&self, file_path: &str, is_known_production: bool) -> bool {
367 is_non_sut_helper(file_path, is_known_production)
368 }
369
370 fn file_exports_any_symbol(&self, path: &Path, symbols: &[String]) -> bool {
371 if symbols.is_empty() {
372 return true;
373 }
374 let source = match std::fs::read_to_string(path) {
377 Ok(s) => s,
378 Err(_) => return true,
379 };
380 let mut parser = Self::parser();
381 let tree = match parser.parse(&source, None) {
382 Some(t) => t,
383 None => return true,
384 };
385 let query = cached_query(&EXPORTED_SYMBOL_QUERY_CACHE, EXPORTED_SYMBOL_QUERY);
386 let symbol_idx = query
387 .capture_index_for_name("symbol_name")
388 .expect("@symbol_name capture not found in exported_symbol.scm");
389 let vis_idx = query.capture_index_for_name("vis");
390
391 let source_bytes = source.as_bytes();
392 let mut cursor = QueryCursor::new();
393 let mut matches = cursor.matches(query, tree.root_node(), source_bytes);
394 while let Some(m) = matches.next() {
395 for cap in m.captures {
396 if cap.index == symbol_idx {
397 let is_pub_only = m.captures.iter().any(|c| {
399 vis_idx == Some(c.index)
400 && c.node.utf8_text(source_bytes).unwrap_or("") == "pub"
401 });
402 if !is_pub_only {
403 continue;
404 }
405 let name = cap.node.utf8_text(source_bytes).unwrap_or("");
406 if symbols.iter().any(|s| s == name) {
407 return true;
408 }
409 }
410 }
411 }
412 for symbol in symbols {
415 for keyword in &["struct", "fn", "type", "enum", "trait", "const", "static"] {
416 let pattern = format!("pub {keyword} {symbol}");
417 if source.lines().any(|line| {
418 let trimmed = line.trim();
419 !trimmed.starts_with("//") && trimmed.contains(&pattern)
420 }) {
421 return true;
422 }
423 }
424 }
425 false
426 }
427}
428
429fn has_pub_visibility(node: tree_sitter::Node) -> bool {
435 for i in 0..node.child_count() {
436 if let Some(child) = node.child(i) {
437 if child.kind() == "visibility_modifier" {
438 return true;
439 }
440 if child.kind() != "attribute_item" && child.kind() != "visibility_modifier" {
442 break;
443 }
444 }
445 }
446 false
447}
448
449fn find_cfg_test_ranges(source: &str) -> Vec<(usize, usize)> {
454 let mut parser = RustExtractor::parser();
455 let tree = match parser.parse(source, None) {
456 Some(t) => t,
457 None => return Vec::new(),
458 };
459 let source_bytes = source.as_bytes();
460 let query = cached_query(&CFG_TEST_QUERY_CACHE, CFG_TEST_QUERY);
461
462 let attr_name_idx = query.capture_index_for_name("attr_name");
463 let cfg_arg_idx = query.capture_index_for_name("cfg_arg");
464 let cfg_test_attr_idx = query.capture_index_for_name("cfg_test_attr");
465
466 let mut cursor = QueryCursor::new();
467 let mut matches = cursor.matches(query, tree.root_node(), source_bytes);
468 let mut ranges = Vec::new();
469
470 while let Some(m) = matches.next() {
471 let mut is_cfg = false;
472 let mut is_test = false;
473 let mut attr_node = None;
474
475 for cap in m.captures {
476 let text = cap.node.utf8_text(source_bytes).unwrap_or("");
477 if attr_name_idx == Some(cap.index) && text == "cfg" {
478 is_cfg = true;
479 }
480 if cfg_arg_idx == Some(cap.index) && text == "test" {
481 is_test = true;
482 }
483 if cfg_test_attr_idx == Some(cap.index) {
484 attr_node = Some(cap.node);
485 }
486 }
487
488 if is_cfg && is_test {
489 if let Some(attr) = attr_node {
490 let mut sibling = attr.next_sibling();
492 while let Some(s) = sibling {
493 if s.kind() == "mod_item" {
494 ranges.push((s.start_byte(), s.end_byte()));
495 break;
496 }
497 sibling = s.next_sibling();
498 }
499 }
500 }
501 }
502
503 ranges
504}
505
506fn extract_use_declaration(
510 node: &tree_sitter::Node,
511 source_bytes: &[u8],
512 result: &mut HashMap<String, Vec<String>>,
513 crate_name: Option<&str>,
514) {
515 let arg = match node.child_by_field_name("argument") {
516 Some(a) => a,
517 None => return,
518 };
519 let full_text = arg.utf8_text(source_bytes).unwrap_or("");
520
521 if let Some(path_after_crate) = full_text.strip_prefix("crate::") {
523 parse_use_path(path_after_crate, result);
524 return;
525 }
526
527 if let Some(name) = crate_name {
529 let prefix = format!("{name}::");
530 if let Some(path_after_name) = full_text.strip_prefix(&prefix) {
531 parse_use_path(path_after_name, result);
532 }
533 }
534}
535
536pub fn extract_import_specifiers_with_crate_name(
540 source: &str,
541 crate_name: Option<&str>,
542) -> Vec<(String, Vec<String>)> {
543 let mut parser = RustExtractor::parser();
544 let tree = match parser.parse(source, None) {
545 Some(t) => t,
546 None => return Vec::new(),
547 };
548 let source_bytes = source.as_bytes();
549
550 let root = tree.root_node();
553 let mut result_map: HashMap<String, Vec<String>> = HashMap::new();
554
555 for i in 0..root.child_count() {
556 let child = root.child(i).unwrap();
557 if child.kind() == "use_declaration" {
558 extract_use_declaration(&child, source_bytes, &mut result_map, crate_name);
559 }
560 }
561
562 result_map.into_iter().collect()
563}
564
565pub fn extract_import_specifiers_with_crate_names(
574 source: &str,
575 crate_names: &[&str],
576) -> Vec<(String, String, Vec<String>)> {
577 let mut parser = RustExtractor::parser();
578 let tree = match parser.parse(source, None) {
579 Some(t) => t,
580 None => return Vec::new(),
581 };
582 let source_bytes = source.as_bytes();
583 let root = tree.root_node();
584 let mut results = Vec::new();
585
586 for i in 0..root.child_count() {
587 let child = root.child(i).unwrap();
588 if child.kind() != "use_declaration" {
589 continue;
590 }
591 let arg = match child.child_by_field_name("argument") {
592 Some(a) => a,
593 None => continue,
594 };
595 let full_text = arg.utf8_text(source_bytes).unwrap_or("");
596
597 if let Some(path_after_crate) = full_text.strip_prefix("crate::") {
599 let mut map: HashMap<String, Vec<String>> = HashMap::new();
600 parse_use_path(path_after_crate, &mut map);
601 for (specifier, symbols) in map {
602 results.push(("crate".to_string(), specifier, symbols));
603 }
604 continue;
605 }
606
607 for &name in crate_names {
609 let prefix = format!("{name}::");
610 if let Some(path_after_name) = full_text.strip_prefix(&prefix) {
611 let mut map: HashMap<String, Vec<String>> = HashMap::new();
612 parse_use_path(path_after_name, &mut map);
613 for (specifier, symbols) in map {
614 results.push((name.to_string(), specifier, symbols));
615 }
616 break; }
618 }
619 }
620
621 results
622}
623
624#[derive(Debug)]
630pub struct WorkspaceMember {
631 pub crate_name: String,
633 pub member_root: std::path::PathBuf,
635}
636
637const SKIP_DIRS: &[&str] = &["target", ".cargo", "vendor"];
639
640const MAX_TRAVERSE_DEPTH: usize = 4;
642
643pub fn has_workspace_section(scan_root: &Path) -> bool {
645 let cargo_toml = scan_root.join("Cargo.toml");
646 let content = match std::fs::read_to_string(&cargo_toml) {
647 Ok(c) => c,
648 Err(_) => return false,
649 };
650 content.lines().any(|line| line.trim() == "[workspace]")
651}
652
653pub fn find_workspace_members(scan_root: &Path) -> Vec<WorkspaceMember> {
664 if !has_workspace_section(scan_root) {
665 return Vec::new();
666 }
667
668 let mut members = Vec::new();
669 find_members_recursive(scan_root, scan_root, 0, &mut members);
670 members
671}
672
673fn find_members_recursive(
674 scan_root: &Path,
675 dir: &Path,
676 depth: usize,
677 members: &mut Vec<WorkspaceMember>,
678) {
679 if depth > MAX_TRAVERSE_DEPTH {
680 return;
681 }
682
683 let read_dir = match std::fs::read_dir(dir) {
684 Ok(rd) => rd,
685 Err(_) => return,
686 };
687
688 for entry in read_dir.flatten() {
689 let path = entry.path();
690 if !path.is_dir() {
691 continue;
692 }
693
694 let dir_name = match path.file_name().and_then(|n| n.to_str()) {
695 Some(n) => n,
696 None => continue,
697 };
698
699 if dir_name.starts_with('.') || SKIP_DIRS.contains(&dir_name) {
701 continue;
702 }
703
704 if path == scan_root {
706 continue;
707 }
708
709 if let Some(crate_name) = parse_crate_name(&path) {
711 members.push(WorkspaceMember {
712 crate_name,
713 member_root: path.to_path_buf(),
714 });
715 continue;
718 }
719
720 find_members_recursive(scan_root, &path, depth + 1, members);
722 }
723}
724
725pub fn find_member_by_crate_name<'a>(
729 name: &str,
730 members: &'a [WorkspaceMember],
731) -> Option<&'a WorkspaceMember> {
732 members.iter().find(|m| m.crate_name == name)
733}
734
735pub fn find_member_for_path<'a>(
739 path: &Path,
740 members: &'a [WorkspaceMember],
741) -> Option<&'a WorkspaceMember> {
742 members
743 .iter()
744 .filter(|m| path.starts_with(&m.member_root))
745 .max_by_key(|m| m.member_root.components().count())
746}
747
748pub fn parse_crate_name(scan_root: &Path) -> Option<String> {
752 let cargo_toml = scan_root.join("Cargo.toml");
753 let content = std::fs::read_to_string(&cargo_toml).ok()?;
754
755 let mut in_package = false;
756 for line in content.lines() {
757 let trimmed = line.trim();
758
759 if trimmed.starts_with('[') {
761 if trimmed == "[package]" {
762 in_package = true;
763 } else {
764 if in_package {
766 break;
767 }
768 }
769 continue;
770 }
771
772 if in_package {
773 if let Some(rest) = trimmed.strip_prefix("name") {
775 let rest = rest.trim();
776 if let Some(rest) = rest.strip_prefix('=') {
777 let rest = rest.trim();
778 let name = if let Some(inner) =
780 rest.strip_prefix('"').and_then(|s| s.strip_suffix('"'))
781 {
782 inner
783 } else if let Some(inner) =
784 rest.strip_prefix('\'').and_then(|s| s.strip_suffix('\''))
785 {
786 inner
787 } else {
788 continue;
789 };
790 return Some(name.replace('-', "_"));
791 }
792 }
793 }
794 }
795
796 None
797}
798
799fn parse_use_path(path: &str, result: &mut HashMap<String, Vec<String>>) {
805 if let Some(brace_start) = path.find('{') {
807 let module_part = &path[..brace_start.saturating_sub(2)]; let specifier = module_part.replace("::", "/");
809 if let Some(brace_end) = path.find('}') {
810 let list_content = &path[brace_start + 1..brace_end];
811 let symbols: Vec<String> = list_content
812 .split(',')
813 .map(|s| s.trim().to_string())
814 .filter(|s| !s.is_empty() && s != "*")
815 .collect();
816 result.entry(specifier).or_default().extend(symbols);
817 }
818 return;
819 }
820
821 if let Some(module_part) = path.strip_suffix("::*") {
823 let specifier = module_part.replace("::", "/");
824 if !specifier.is_empty() {
825 result.entry(specifier).or_default();
826 }
827 return;
828 }
829
830 if !path.contains("::") && !path.is_empty() {
832 result.entry(path.to_string()).or_default();
833 return;
834 }
835
836 let parts: Vec<&str> = path.split("::").collect();
838 if parts.len() >= 2 {
839 let module_parts = &parts[..parts.len() - 1];
840 let symbol = parts[parts.len() - 1];
841 let specifier = module_parts.join("/");
842 result
843 .entry(specifier)
844 .or_default()
845 .push(symbol.to_string());
846 }
847}
848
849fn extract_re_exports_from_text(text: &str, result: &mut Vec<BarrelReExport>) {
852 let joined = join_multiline_pub_use(text);
853 for line in joined.lines() {
854 let trimmed = line.trim();
855 if trimmed == "{" || trimmed == "}" {
857 continue;
858 }
859 let trimmed = trimmed
861 .strip_prefix('{')
862 .unwrap_or(trimmed)
863 .strip_suffix('}')
864 .unwrap_or(trimmed)
865 .trim();
866
867 let statements: Vec<&str> = trimmed
872 .split(';')
873 .map(str::trim)
874 .filter(|s| !s.is_empty())
875 .collect();
876 for stmt in statements {
877 extract_single_re_export_stmt(stmt, result);
878 }
879 }
880}
881
882fn extract_single_re_export_stmt(trimmed: &str, result: &mut Vec<BarrelReExport>) {
883 if trimmed.starts_with("pub mod ") || trimmed.starts_with("pub(crate) mod ") {
885 let mod_name = trimmed
886 .trim_start_matches("pub(crate) mod ")
887 .trim_start_matches("pub mod ")
888 .trim_end_matches(';')
889 .trim();
890 if !mod_name.is_empty() && !mod_name.contains(' ') {
891 result.push(BarrelReExport {
892 symbols: Vec::new(),
893 from_specifier: format!("./{mod_name}"),
894 wildcard: true,
895 namespace_wildcard: false,
896 });
897 }
898 }
899
900 if trimmed.starts_with("pub use ") && trimmed.contains("::") {
902 let use_path = trimmed
903 .trim_start_matches("pub use ")
904 .trim_end_matches(';')
905 .trim();
906 let use_path = use_path.strip_prefix("self::").unwrap_or(use_path);
907 if use_path == "*" {
909 result.push(BarrelReExport {
910 symbols: Vec::new(),
911 from_specifier: "./".to_string(),
912 wildcard: true,
913 namespace_wildcard: false,
914 });
915 return;
916 }
917 if use_path.ends_with("::*") {
919 let module_part = use_path.strip_suffix("::*").unwrap_or("");
920 result.push(BarrelReExport {
921 symbols: Vec::new(),
922 from_specifier: format!("./{}", module_part.replace("::", "/")),
923 wildcard: true,
924 namespace_wildcard: false,
925 });
926 } else if let Some(brace_start) = use_path.find('{') {
927 let module_part = &use_path[..brace_start.saturating_sub(2)];
928 if let Some(brace_end) = use_path.find('}') {
929 let list_content = &use_path[brace_start + 1..brace_end];
930 let symbols: Vec<String> = list_content
931 .split(',')
932 .map(|s| s.trim().to_string())
933 .filter(|s| !s.is_empty() && s != "*")
934 .collect();
935 result.push(BarrelReExport {
936 symbols,
937 from_specifier: format!("./{}", module_part.replace("::", "/")),
938 wildcard: false,
939 namespace_wildcard: false,
940 });
941 }
942 } else {
943 let parts: Vec<&str> = use_path.split("::").collect();
945 if parts.len() >= 2 {
946 let module_parts = &parts[..parts.len() - 1];
947 let symbol = parts[parts.len() - 1];
948 result.push(BarrelReExport {
949 symbols: vec![symbol.to_string()],
950 from_specifier: format!("./{}", module_parts.join("/")),
951 wildcard: false,
952 namespace_wildcard: false,
953 });
954 }
955 }
956 }
957}
958
959fn extract_pub_use_re_exports(
961 arg: &tree_sitter::Node,
962 source_bytes: &[u8],
963 result: &mut Vec<BarrelReExport>,
964) {
965 let full_text = arg.utf8_text(source_bytes).unwrap_or("");
966 let full_text = full_text.strip_prefix("self::").unwrap_or(full_text);
968
969 if full_text == "*" {
971 result.push(BarrelReExport {
972 symbols: Vec::new(),
973 from_specifier: "./".to_string(),
974 wildcard: true,
975 namespace_wildcard: false,
976 });
977 return;
978 }
979
980 if full_text.ends_with("::*") {
982 let module_part = full_text.strip_suffix("::*").unwrap_or("");
983 result.push(BarrelReExport {
984 symbols: Vec::new(),
985 from_specifier: format!("./{}", module_part.replace("::", "/")),
986 wildcard: true,
987 namespace_wildcard: false,
988 });
989 return;
990 }
991
992 if let Some(brace_start) = full_text.find('{') {
994 let module_part = &full_text[..brace_start.saturating_sub(2)]; if let Some(brace_end) = full_text.find('}') {
996 let list_content = &full_text[brace_start + 1..brace_end];
997 let symbols: Vec<String> = list_content
998 .split(',')
999 .map(|s| s.trim().to_string())
1000 .filter(|s| !s.is_empty())
1001 .collect();
1002 result.push(BarrelReExport {
1003 symbols,
1004 from_specifier: format!("./{}", module_part.replace("::", "/")),
1005 wildcard: false,
1006 namespace_wildcard: false,
1007 });
1008 }
1009 return;
1010 }
1011
1012 let parts: Vec<&str> = full_text.split("::").collect();
1014 if parts.len() >= 2 {
1015 let module_parts = &parts[..parts.len() - 1];
1016 let symbol = parts[parts.len() - 1];
1017 result.push(BarrelReExport {
1018 symbols: vec![symbol.to_string()],
1019 from_specifier: format!("./{}", module_parts.join("/")),
1020 wildcard: false,
1021 namespace_wildcard: false,
1022 });
1023 }
1024}
1025
1026fn extract_test_subdir(path: &str) -> Option<String> {
1037 let parts: Vec<&str> = path.split('/').collect();
1038 for (i, part) in parts.iter().enumerate() {
1039 if *part == "tests" && i + 2 < parts.len() {
1040 return Some(parts[i + 1].to_string());
1041 }
1042 }
1043 None
1044}
1045
1046impl RustExtractor {
1051 pub fn map_test_files_with_imports(
1057 &self,
1058 production_files: &[String],
1059 test_sources: &HashMap<String, String>,
1060 scan_root: &Path,
1061 l1_exclusive: bool,
1062 ) -> Vec<FileMapping> {
1063 let test_file_list: Vec<String> = test_sources.keys().cloned().collect();
1064
1065 let mut mappings =
1067 exspec_core::observe::map_test_files(self, production_files, &test_file_list);
1068
1069 for (idx, prod_file) in production_files.iter().enumerate() {
1071 if production_stem(prod_file).is_none() {
1073 continue;
1074 }
1075 if let Ok(source) = std::fs::read_to_string(prod_file) {
1076 if detect_inline_tests(&source) {
1077 if !mappings[idx].test_files.contains(prod_file) {
1079 mappings[idx].test_files.push(prod_file.clone());
1080 }
1081 }
1082 }
1083 }
1084
1085 let canonical_root = match scan_root.canonicalize() {
1087 Ok(r) => r,
1088 Err(_) => return mappings,
1089 };
1090 let mut canonical_to_idx: HashMap<String, usize> = HashMap::new();
1091 for (idx, prod) in production_files.iter().enumerate() {
1092 if let Ok(canonical) = Path::new(prod).canonicalize() {
1093 canonical_to_idx.insert(canonical.to_string_lossy().into_owned(), idx);
1094 }
1095 }
1096
1097 let layer1_tests_per_prod: Vec<HashSet<String>> = mappings
1099 .iter()
1100 .map(|m| m.test_files.iter().cloned().collect())
1101 .collect();
1102
1103 let mut layer1_matched: HashSet<String> = layer1_tests_per_prod
1105 .iter()
1106 .flat_map(|s| s.iter().cloned())
1107 .collect();
1108
1109 self.apply_l1_5_underscore_path_matching(
1112 &mut mappings,
1113 &test_file_list,
1114 &mut layer1_matched,
1115 );
1116
1117 self.apply_l1_subdir_matching(&mut mappings, &test_file_list, &mut layer1_matched);
1121
1122 let crate_name = parse_crate_name(scan_root);
1124 let members = find_workspace_members(scan_root);
1125
1126 if let Some(ref name) = crate_name {
1128 self.apply_l2_imports(
1130 test_sources,
1131 name,
1132 scan_root,
1133 &canonical_root,
1134 &canonical_to_idx,
1135 &mut mappings,
1136 l1_exclusive,
1137 &layer1_matched,
1138 );
1139 }
1140
1141 if !members.is_empty() {
1142 for member in &members {
1144 let member_test_sources: HashMap<String, String> = test_sources
1146 .iter()
1147 .filter(|(path, _)| {
1148 find_member_for_path(Path::new(path.as_str()), &members)
1149 .map(|m| std::ptr::eq(m, member))
1150 .unwrap_or(false)
1151 })
1152 .map(|(k, v)| (k.clone(), v.clone()))
1153 .collect();
1154
1155 self.apply_l2_imports(
1156 &member_test_sources,
1157 &member.crate_name,
1158 &member.member_root,
1159 &canonical_root,
1160 &canonical_to_idx,
1161 &mut mappings,
1162 l1_exclusive,
1163 &layer1_matched,
1164 );
1165 }
1166
1167 let root_test_sources: HashMap<String, String> = test_sources
1172 .iter()
1173 .filter(|(path, _)| {
1174 find_member_for_path(Path::new(path.as_str()), &members).is_none()
1175 })
1176 .map(|(k, v)| (k.clone(), v.clone()))
1177 .collect();
1178
1179 if !root_test_sources.is_empty() {
1180 for member in &members {
1181 self.apply_l2_imports(
1183 &root_test_sources,
1184 &member.crate_name,
1185 &member.member_root,
1186 &canonical_root,
1187 &canonical_to_idx,
1188 &mut mappings,
1189 l1_exclusive,
1190 &layer1_matched,
1191 );
1192 if let Some(ref root_name) = crate_name {
1195 if *root_name != member.crate_name {
1196 self.apply_l2_imports(
1197 &root_test_sources,
1198 root_name,
1199 &member.member_root,
1200 &canonical_root,
1201 &canonical_to_idx,
1202 &mut mappings,
1203 l1_exclusive,
1204 &layer1_matched,
1205 );
1206 }
1207 }
1208 }
1209 }
1210 } else if crate_name.is_none() {
1211 self.apply_l2_imports(
1214 test_sources,
1215 "crate",
1216 scan_root,
1217 &canonical_root,
1218 &canonical_to_idx,
1219 &mut mappings,
1220 l1_exclusive,
1221 &layer1_matched,
1222 );
1223 }
1224
1225 if let Some(ref root_name) = crate_name {
1229 let root_lib = scan_root.join("src/lib.rs");
1230 if root_lib.exists() && !members.is_empty() {
1231 let root_test_sources: HashMap<String, String> = test_sources
1232 .iter()
1233 .filter(|(path, _)| {
1234 find_member_for_path(Path::new(path.as_str()), &members).is_none()
1235 })
1236 .map(|(k, v)| (k.clone(), v.clone()))
1237 .collect();
1238 if !root_test_sources.is_empty() {
1239 self.apply_l2_cross_crate_barrel(
1240 &root_test_sources,
1241 root_name,
1242 &root_lib,
1243 &members,
1244 &canonical_root,
1245 &canonical_to_idx,
1246 &mut mappings,
1247 l1_exclusive,
1248 &layer1_matched,
1249 );
1250 }
1251 }
1252 }
1253
1254 for (i, mapping) in mappings.iter_mut().enumerate() {
1257 let has_layer1 = !layer1_tests_per_prod[i].is_empty();
1258 if !has_layer1 && !mapping.test_files.is_empty() {
1259 mapping.strategy = MappingStrategy::ImportTracing;
1260 }
1261 }
1262
1263 mappings
1264 }
1265
1266 #[allow(clippy::too_many_arguments)]
1271 fn apply_l2_imports(
1272 &self,
1273 test_sources: &HashMap<String, String>,
1274 crate_name: &str,
1275 crate_root: &Path,
1276 canonical_root: &Path,
1277 canonical_to_idx: &HashMap<String, usize>,
1278 mappings: &mut [FileMapping],
1279 l1_exclusive: bool,
1280 layer1_matched: &HashSet<String>,
1281 ) {
1282 for (test_file, source) in test_sources {
1283 if l1_exclusive && layer1_matched.contains(test_file) {
1284 continue;
1285 }
1286 let imports = extract_import_specifiers_with_crate_name(source, Some(crate_name));
1287 let mut matched_indices = HashSet::<usize>::new();
1288
1289 for (specifier, symbols) in &imports {
1290 let src_relative = crate_root.join("src").join(specifier);
1292
1293 if let Some(resolved) = exspec_core::observe::resolve_absolute_base_to_file(
1294 self,
1295 &src_relative,
1296 canonical_root,
1297 ) {
1298 let mut per_specifier_indices = HashSet::<usize>::new();
1299 exspec_core::observe::collect_import_matches(
1300 self,
1301 &resolved,
1302 symbols,
1303 canonical_to_idx,
1304 &mut per_specifier_indices,
1305 canonical_root,
1306 );
1307 for idx in per_specifier_indices {
1309 let prod_path = Path::new(&mappings[idx].production_file);
1310 if self.file_exports_any_symbol(prod_path, symbols) {
1311 matched_indices.insert(idx);
1312 }
1313 }
1314 }
1315 }
1316
1317 for idx in matched_indices {
1318 if !mappings[idx].test_files.contains(test_file) {
1319 mappings[idx].test_files.push(test_file.clone());
1320 }
1321 }
1322 }
1323 }
1324
1325 #[allow(clippy::too_many_arguments)]
1334 fn apply_l2_cross_crate_barrel(
1335 &self,
1336 test_sources: &HashMap<String, String>,
1337 root_crate_name: &str,
1338 root_lib_path: &Path,
1339 members: &[WorkspaceMember],
1340 canonical_root: &Path,
1341 canonical_to_idx: &HashMap<String, usize>,
1342 mappings: &mut [FileMapping],
1343 l1_exclusive: bool,
1344 layer1_matched: &HashSet<String>,
1345 ) {
1346 let root_lib_source = match std::fs::read_to_string(root_lib_path) {
1348 Ok(s) => s,
1349 Err(_) => return,
1350 };
1351 let root_lib_str = root_lib_path.to_string_lossy();
1352
1353 let barrel_exports = self.extract_barrel_re_exports(&root_lib_source, &root_lib_str);
1355 if barrel_exports.is_empty() {
1356 return;
1357 }
1358
1359 for (test_file, source) in test_sources {
1360 if l1_exclusive && layer1_matched.contains(test_file) {
1361 continue;
1362 }
1363
1364 let imports = extract_import_specifiers_with_crate_name(source, Some(root_crate_name));
1369 let root_symbols: Vec<String> = {
1370 let mut syms = Vec::new();
1371 for (specifier, symbols) in &imports {
1372 if specifier.is_empty() && !symbols.is_empty() {
1373 syms.extend(symbols.clone());
1375 } else if !specifier.is_empty()
1376 && !specifier.contains('/')
1377 && symbols.is_empty()
1378 {
1379 syms.push(specifier.clone());
1381 }
1382 }
1383 syms
1384 };
1385
1386 if root_symbols.is_empty() {
1387 continue;
1388 }
1389
1390 let mut matched_indices = HashSet::<usize>::new();
1391
1392 for barrel in &barrel_exports {
1393 let crate_candidate = barrel
1395 .from_specifier
1396 .strip_prefix("./")
1397 .unwrap_or(&barrel.from_specifier);
1398
1399 if crate_candidate.contains('/') {
1402 continue;
1403 }
1404
1405 let member = match find_member_by_crate_name(crate_candidate, members) {
1407 Some(m) => m,
1408 None => continue,
1409 };
1410
1411 let symbols_matched: Vec<String> = if barrel.wildcard {
1413 root_symbols.clone()
1415 } else {
1416 root_symbols
1418 .iter()
1419 .filter(|sym| barrel.symbols.contains(sym))
1420 .cloned()
1421 .collect()
1422 };
1423
1424 if symbols_matched.is_empty() {
1425 continue;
1426 }
1427
1428 let member_lib = member.member_root.join("src/lib.rs");
1430
1431 if barrel.wildcard {
1435 let canonical_member_root = member
1436 .member_root
1437 .canonicalize()
1438 .unwrap_or_else(|_| member.member_root.clone());
1439 let canonical_member_str = canonical_member_root.to_string_lossy().into_owned();
1440 for (prod_str, &idx) in canonical_to_idx.iter() {
1441 if prod_str.starts_with(&canonical_member_str) {
1442 let prod_path = Path::new(&mappings[idx].production_file);
1443 if self.file_exports_any_symbol(prod_path, &symbols_matched) {
1444 matched_indices.insert(idx);
1445 }
1446 }
1447 }
1448 }
1449
1450 if let Some(resolved) = exspec_core::observe::resolve_absolute_base_to_file(
1451 self,
1452 &member_lib,
1453 canonical_root,
1454 ) {
1455 if let Some(&idx) = canonical_to_idx.get(&resolved) {
1459 let prod_path = Path::new(&mappings[idx].production_file);
1460 if self.file_exports_any_symbol(prod_path, &symbols_matched) {
1461 matched_indices.insert(idx);
1462 }
1463 }
1464
1465 let mut per_member_indices = HashSet::<usize>::new();
1468 exspec_core::observe::collect_import_matches(
1469 self,
1470 &resolved,
1471 &symbols_matched,
1472 canonical_to_idx,
1473 &mut per_member_indices,
1474 canonical_root,
1475 );
1476 for idx in per_member_indices {
1478 let prod_path = Path::new(&mappings[idx].production_file);
1479 if self.file_exports_any_symbol(prod_path, &symbols_matched) {
1480 matched_indices.insert(idx);
1481 }
1482 }
1483 }
1484 }
1485
1486 for idx in matched_indices {
1487 if !mappings[idx].test_files.contains(test_file) {
1488 mappings[idx].test_files.push(test_file.clone());
1489 }
1490 }
1491 }
1492 }
1493
1494 fn apply_l1_subdir_matching(
1503 &self,
1504 mappings: &mut [FileMapping],
1505 test_paths: &[String],
1506 layer1_matched: &mut HashSet<String>,
1507 ) {
1508 for test_path in test_paths {
1509 if layer1_matched.contains(test_path) {
1510 continue;
1511 }
1512
1513 let test_stem = match self.test_stem(test_path) {
1514 Some(s) => s,
1515 None => continue,
1516 };
1517
1518 let normalized = test_path.replace('\\', "/");
1519 let test_subdir = extract_test_subdir(&normalized);
1520 if test_subdir.as_ref().is_none_or(|s| s.len() < 3) {
1521 continue;
1522 }
1523 let test_subdir = test_subdir.unwrap();
1524
1525 let subdir_lower = test_subdir.to_lowercase();
1526 let stem_lower = test_stem.to_lowercase();
1527 let dir_segment = format!("/{subdir_lower}/");
1528
1529 for mapping in mappings.iter_mut() {
1530 let prod_stem = match self.production_stem(&mapping.production_file) {
1531 Some(s) => s,
1532 None => continue,
1533 };
1534
1535 if prod_stem.to_lowercase() != stem_lower {
1536 continue;
1537 }
1538
1539 let prod_path_lower = mapping.production_file.replace('\\', "/").to_lowercase();
1540 if prod_path_lower.contains(&dir_segment) {
1541 if !mapping.test_files.contains(test_path) {
1542 mapping.test_files.push(test_path.clone());
1543 }
1544 layer1_matched.insert(test_path.clone());
1545 break;
1546 }
1547 }
1548 }
1549 }
1550
1551 fn apply_l1_5_underscore_path_matching(
1559 &self,
1560 mappings: &mut [FileMapping],
1561 test_paths: &[String],
1562 layer1_matched: &mut HashSet<String>,
1563 ) {
1564 for test_path in test_paths {
1565 if layer1_matched.contains(test_path) {
1566 continue;
1567 }
1568
1569 let test_stem = match self.test_stem(test_path) {
1570 Some(s) => s,
1571 None => continue,
1572 };
1573
1574 if !test_stem.contains('_') {
1575 continue;
1576 }
1577
1578 let underscore_pos = match test_stem.find('_') {
1579 Some(pos) => pos,
1580 None => continue,
1581 };
1582
1583 let prefix = &test_stem[..underscore_pos];
1584 let suffix = &test_stem[underscore_pos + 1..];
1585
1586 if suffix.len() <= 2 {
1587 continue;
1588 }
1589
1590 let prefix_lower = prefix.to_lowercase();
1591 let suffix_lower = suffix.to_lowercase();
1592 let dir_segment = format!("/{prefix_lower}/");
1593
1594 for mapping in mappings.iter_mut() {
1595 let prod_stem = match self.production_stem(&mapping.production_file) {
1596 Some(s) => s,
1597 None => continue,
1598 };
1599
1600 if prod_stem.to_lowercase() != suffix_lower {
1601 continue;
1602 }
1603
1604 let prod_path_lower = mapping.production_file.replace('\\', "/").to_lowercase();
1605 let test_first = test_path.split('/').next().unwrap_or("");
1608 let prod_first = mapping.production_file.split('/').next().unwrap_or("");
1609 let test_has_crate_prefix = test_first != "tests" && test_first != "src";
1610 let prod_has_crate_prefix = prod_first != "tests" && prod_first != "src";
1611 if test_has_crate_prefix
1612 && prod_has_crate_prefix
1613 && !test_first.eq_ignore_ascii_case(prod_first)
1614 {
1615 continue;
1616 }
1617 if prod_path_lower.contains(&dir_segment) {
1618 mapping.test_files.push(test_path.clone());
1619 layer1_matched.insert(test_path.clone());
1620 break;
1621 }
1622 }
1623 }
1624 }
1625}
1626
1627pub(crate) fn join_multiline_pub_use(text: &str) -> String {
1634 let mut result = String::new();
1635 let mut accumulator: Option<String> = None;
1636 let mut brace_depth: usize = 0;
1637 for line in text.lines() {
1638 let trimmed = line.trim();
1639 if let Some(ref mut acc) = accumulator {
1640 acc.push(' ');
1641 acc.push_str(trimmed);
1642 for ch in trimmed.chars() {
1643 match ch {
1644 '{' => brace_depth += 1,
1645 '}' => brace_depth = brace_depth.saturating_sub(1),
1646 _ => {}
1647 }
1648 }
1649 if brace_depth == 0 {
1650 result.push_str(acc);
1651 result.push('\n');
1652 accumulator = None;
1653 }
1654 } else if trimmed.starts_with("pub use ") && trimmed.contains('{') && !trimmed.contains('}')
1655 {
1656 brace_depth = trimmed.chars().filter(|&c| c == '{').count()
1657 - trimmed.chars().filter(|&c| c == '}').count();
1658 accumulator = Some(trimmed.to_string());
1659 } else {
1660 result.push_str(line);
1661 result.push('\n');
1662 }
1663 }
1664 if let Some(acc) = accumulator {
1665 result.push_str(&acc);
1666 result.push('\n');
1667 }
1668 result
1669}
1670
1671#[cfg(test)]
1676mod tests {
1677 use super::*;
1678 use std::path::PathBuf;
1679
1680 #[test]
1684 fn rs_stem_01_test_prefix() {
1685 let extractor = RustExtractor::new();
1689 assert_eq!(extractor.test_stem("tests/test_foo.rs"), Some("foo"));
1690 }
1691
1692 #[test]
1696 fn rs_stem_02_test_suffix() {
1697 let extractor = RustExtractor::new();
1701 assert_eq!(extractor.test_stem("tests/foo_test.rs"), Some("foo"));
1702 }
1703
1704 #[test]
1708 fn rs_stem_03_tests_dir_integration() {
1709 let extractor = RustExtractor::new();
1713 assert_eq!(
1714 extractor.test_stem("tests/integration.rs"),
1715 Some("integration")
1716 );
1717 }
1718
1719 #[test]
1723 fn rs_stem_04_production_file_no_test_stem() {
1724 let extractor = RustExtractor::new();
1728 assert_eq!(extractor.test_stem("src/user.rs"), None);
1729 }
1730
1731 #[test]
1735 fn rs_stem_05_production_stem_regular() {
1736 let extractor = RustExtractor::new();
1740 assert_eq!(extractor.production_stem("src/user.rs"), Some("user"));
1741 }
1742
1743 #[test]
1747 fn rs_stem_06_production_stem_lib() {
1748 let extractor = RustExtractor::new();
1752 assert_eq!(extractor.production_stem("src/lib.rs"), None);
1753 }
1754
1755 #[test]
1759 fn rs_stem_07_production_stem_mod() {
1760 let extractor = RustExtractor::new();
1764 assert_eq!(extractor.production_stem("src/mod.rs"), None);
1765 }
1766
1767 #[test]
1771 fn rs_stem_08_production_stem_main() {
1772 let extractor = RustExtractor::new();
1776 assert_eq!(extractor.production_stem("src/main.rs"), None);
1777 }
1778
1779 #[test]
1783 fn rs_stem_09_production_stem_test_file() {
1784 let extractor = RustExtractor::new();
1788 assert_eq!(extractor.production_stem("tests/test_foo.rs"), None);
1789 }
1790
1791 #[test]
1795 fn rs_helper_01_build_rs() {
1796 let extractor = RustExtractor::new();
1800 assert!(extractor.is_non_sut_helper("build.rs", false));
1801 }
1802
1803 #[test]
1807 fn rs_helper_02_tests_common() {
1808 let extractor = RustExtractor::new();
1812 assert!(extractor.is_non_sut_helper("tests/common/mod.rs", false));
1813 }
1814
1815 #[test]
1819 fn rs_helper_03_regular_production_file() {
1820 let extractor = RustExtractor::new();
1824 assert!(!extractor.is_non_sut_helper("src/user.rs", false));
1825 }
1826
1827 #[test]
1831 fn rs_helper_04_benches() {
1832 let extractor = RustExtractor::new();
1836 assert!(extractor.is_non_sut_helper("benches/bench.rs", false));
1837 }
1838
1839 #[test]
1843 fn rs_l0_01_cfg_test_present() {
1844 let source = r#"
1846pub fn add(a: i32, b: i32) -> i32 { a + b }
1847
1848#[cfg(test)]
1849mod tests {
1850 use super::*;
1851
1852 #[test]
1853 fn test_add() {
1854 assert_eq!(add(1, 2), 3);
1855 }
1856}
1857"#;
1858 assert!(detect_inline_tests(source));
1861 }
1862
1863 #[test]
1867 fn rs_l0_02_no_cfg_test() {
1868 let source = r#"
1870pub fn add(a: i32, b: i32) -> i32 { a + b }
1871"#;
1872 assert!(!detect_inline_tests(source));
1875 }
1876
1877 #[test]
1881 fn rs_l0_03_cfg_not_test() {
1882 let source = r#"
1884#[cfg(not(test))]
1885mod production_only {
1886 pub fn real_thing() {}
1887}
1888"#;
1889 assert!(!detect_inline_tests(source));
1892 }
1893
1894 #[test]
1898 fn rs_func_01_pub_function() {
1899 let source = "pub fn create_user() {}\n";
1901
1902 let extractor = RustExtractor::new();
1904 let result = extractor.extract_production_functions(source, "src/user.rs");
1905
1906 let func = result.iter().find(|f| f.name == "create_user");
1908 assert!(func.is_some(), "create_user not found in {:?}", result);
1909 assert!(func.unwrap().is_exported);
1910 }
1911
1912 #[test]
1916 fn rs_func_02_private_function() {
1917 let source = "fn private_fn() {}\n";
1919
1920 let extractor = RustExtractor::new();
1922 let result = extractor.extract_production_functions(source, "src/internal.rs");
1923
1924 let func = result.iter().find(|f| f.name == "private_fn");
1926 assert!(func.is_some(), "private_fn not found in {:?}", result);
1927 assert!(!func.unwrap().is_exported);
1928 }
1929
1930 #[test]
1934 fn rs_func_03_impl_method() {
1935 let source = r#"
1937struct User;
1938
1939impl User {
1940 pub fn save(&self) {}
1941}
1942"#;
1943 let extractor = RustExtractor::new();
1945 let result = extractor.extract_production_functions(source, "src/user.rs");
1946
1947 let method = result.iter().find(|f| f.name == "save");
1949 assert!(method.is_some(), "save not found in {:?}", result);
1950 let method = method.unwrap();
1951 assert_eq!(method.class_name, Some("User".to_string()));
1952 assert!(method.is_exported);
1953 }
1954
1955 #[test]
1959 fn rs_func_04_cfg_test_excluded() {
1960 let source = r#"
1962pub fn real_function() {}
1963
1964#[cfg(test)]
1965mod tests {
1966 use super::*;
1967
1968 #[test]
1969 fn test_real_function() {
1970 assert!(true);
1971 }
1972}
1973"#;
1974 let extractor = RustExtractor::new();
1976 let result = extractor.extract_production_functions(source, "src/lib.rs");
1977
1978 assert_eq!(result.len(), 1);
1980 assert_eq!(result[0].name, "real_function");
1981 }
1982
1983 #[test]
1987 fn rs_imp_01_simple_crate_import() {
1988 let source = "use crate::user::User;\n";
1990
1991 let extractor = RustExtractor::new();
1993 let result = extractor.extract_all_import_specifiers(source);
1994
1995 let entry = result.iter().find(|(spec, _)| spec == "user");
1997 assert!(entry.is_some(), "user not found in {:?}", result);
1998 let (_, symbols) = entry.unwrap();
1999 assert!(symbols.contains(&"User".to_string()));
2000 }
2001
2002 #[test]
2006 fn rs_imp_02_nested_crate_import() {
2007 let source = "use crate::models::user::User;\n";
2009
2010 let extractor = RustExtractor::new();
2012 let result = extractor.extract_all_import_specifiers(source);
2013
2014 let entry = result.iter().find(|(spec, _)| spec == "models/user");
2016 assert!(entry.is_some(), "models/user not found in {:?}", result);
2017 let (_, symbols) = entry.unwrap();
2018 assert!(symbols.contains(&"User".to_string()));
2019 }
2020
2021 #[test]
2025 fn rs_imp_03_use_list() {
2026 let source = "use crate::user::{User, Admin};\n";
2028
2029 let extractor = RustExtractor::new();
2031 let result = extractor.extract_all_import_specifiers(source);
2032
2033 let entry = result.iter().find(|(spec, _)| spec == "user");
2035 assert!(entry.is_some(), "user not found in {:?}", result);
2036 let (_, symbols) = entry.unwrap();
2037 assert!(
2038 symbols.contains(&"User".to_string()),
2039 "User not in {:?}",
2040 symbols
2041 );
2042 assert!(
2043 symbols.contains(&"Admin".to_string()),
2044 "Admin not in {:?}",
2045 symbols
2046 );
2047 }
2048
2049 #[test]
2053 fn rs_imp_04_external_crate_skipped() {
2054 let source = "use std::collections::HashMap;\n";
2056
2057 let extractor = RustExtractor::new();
2059 let result = extractor.extract_all_import_specifiers(source);
2060
2061 assert!(
2063 result.is_empty(),
2064 "external imports should be skipped: {:?}",
2065 result
2066 );
2067 }
2068
2069 #[test]
2073 fn rs_barrel_01_mod_rs() {
2074 let extractor = RustExtractor::new();
2078 assert!(extractor.is_barrel_file("src/models/mod.rs"));
2079 }
2080
2081 #[test]
2085 fn rs_barrel_02_lib_rs() {
2086 let extractor = RustExtractor::new();
2090 assert!(extractor.is_barrel_file("src/lib.rs"));
2091 }
2092
2093 #[test]
2097 fn rs_barrel_03_pub_mod() {
2098 let source = "pub mod user;\n";
2100
2101 let extractor = RustExtractor::new();
2103 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
2104
2105 let entry = result.iter().find(|e| e.from_specifier == "./user");
2107 assert!(entry.is_some(), "./user not found in {:?}", result);
2108 assert!(entry.unwrap().wildcard);
2109 }
2110
2111 #[test]
2115 fn rs_barrel_04_pub_use_wildcard() {
2116 let source = "pub use user::*;\n";
2118
2119 let extractor = RustExtractor::new();
2121 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
2122
2123 let entry = result.iter().find(|e| e.from_specifier == "./user");
2125 assert!(entry.is_some(), "./user not found in {:?}", result);
2126 assert!(entry.unwrap().wildcard);
2127 }
2128
2129 #[test]
2133 fn rs_e2e_01_inline_test_self_map() {
2134 let tmp = tempfile::tempdir().unwrap();
2136 let src_dir = tmp.path().join("src");
2137 std::fs::create_dir_all(&src_dir).unwrap();
2138
2139 let user_rs = src_dir.join("user.rs");
2140 std::fs::write(
2141 &user_rs,
2142 r#"pub fn create_user() {}
2143
2144#[cfg(test)]
2145mod tests {
2146 use super::*;
2147 #[test]
2148 fn test_create_user() { assert!(true); }
2149}
2150"#,
2151 )
2152 .unwrap();
2153
2154 let extractor = RustExtractor::new();
2155 let prod_path = user_rs.to_string_lossy().into_owned();
2156 let production_files = vec![prod_path.clone()];
2157 let test_sources: HashMap<String, String> = HashMap::new();
2158
2159 let result = extractor.map_test_files_with_imports(
2161 &production_files,
2162 &test_sources,
2163 tmp.path(),
2164 false,
2165 );
2166
2167 let mapping = result.iter().find(|m| m.production_file == prod_path);
2169 assert!(mapping.is_some());
2170 assert!(
2171 mapping.unwrap().test_files.contains(&prod_path),
2172 "Expected self-map for inline tests: {:?}",
2173 mapping.unwrap().test_files
2174 );
2175 }
2176
2177 #[test]
2181 fn rs_e2e_02_layer1_stem_match() {
2182 let extractor = RustExtractor::new();
2184 let production_files = vec!["src/user.rs".to_string()];
2185 let test_sources: HashMap<String, String> =
2186 [("tests/test_user.rs".to_string(), String::new())]
2187 .into_iter()
2188 .collect();
2189
2190 let scan_root = PathBuf::from(".");
2192 let result = extractor.map_test_files_with_imports(
2193 &production_files,
2194 &test_sources,
2195 &scan_root,
2196 false,
2197 );
2198
2199 let mapping = result.iter().find(|m| m.production_file == "src/user.rs");
2205 assert!(mapping.is_some());
2206 }
2207
2208 #[test]
2212 fn rs_e2e_03_layer2_import_tracing() {
2213 let tmp = tempfile::tempdir().unwrap();
2215 let src_dir = tmp.path().join("src");
2216 let tests_dir = tmp.path().join("tests");
2217 std::fs::create_dir_all(&src_dir).unwrap();
2218 std::fs::create_dir_all(&tests_dir).unwrap();
2219
2220 let service_rs = src_dir.join("service.rs");
2221 std::fs::write(&service_rs, "pub struct Service;\n").unwrap();
2222
2223 let test_service_rs = tests_dir.join("test_service.rs");
2224 let test_source = "use crate::service::Service;\n\n#[test]\nfn test_it() {}\n";
2225 std::fs::write(&test_service_rs, test_source).unwrap();
2226
2227 let extractor = RustExtractor::new();
2228 let prod_path = service_rs.to_string_lossy().into_owned();
2229 let test_path = test_service_rs.to_string_lossy().into_owned();
2230 let production_files = vec![prod_path.clone()];
2231 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
2232 .into_iter()
2233 .collect();
2234
2235 let result = extractor.map_test_files_with_imports(
2237 &production_files,
2238 &test_sources,
2239 tmp.path(),
2240 false,
2241 );
2242
2243 let mapping = result.iter().find(|m| m.production_file == prod_path);
2245 assert!(mapping.is_some());
2246 assert!(
2247 mapping.unwrap().test_files.contains(&test_path),
2248 "Expected import tracing match: {:?}",
2249 mapping.unwrap().test_files
2250 );
2251 }
2252
2253 #[test]
2257 fn rs_e2e_04_helper_excluded() {
2258 let extractor = RustExtractor::new();
2260 let production_files = vec!["src/user.rs".to_string()];
2261 let test_sources: HashMap<String, String> = [
2262 ("tests/test_user.rs".to_string(), String::new()),
2263 (
2264 "tests/common/mod.rs".to_string(),
2265 "pub fn setup() {}\n".to_string(),
2266 ),
2267 ]
2268 .into_iter()
2269 .collect();
2270
2271 let scan_root = PathBuf::from(".");
2273 let result = extractor.map_test_files_with_imports(
2274 &production_files,
2275 &test_sources,
2276 &scan_root,
2277 false,
2278 );
2279
2280 for mapping in &result {
2282 assert!(
2283 !mapping
2284 .test_files
2285 .iter()
2286 .any(|f| f.contains("common/mod.rs")),
2287 "common/mod.rs should not appear: {:?}",
2288 mapping
2289 );
2290 }
2291 }
2292
2293 #[test]
2297 fn rs_crate_01_parse_crate_name_hyphen() {
2298 let tmp = tempfile::tempdir().unwrap();
2300 std::fs::write(
2301 tmp.path().join("Cargo.toml"),
2302 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\n",
2303 )
2304 .unwrap();
2305
2306 let result = parse_crate_name(tmp.path());
2308
2309 assert_eq!(result, Some("my_crate".to_string()));
2311 }
2312
2313 #[test]
2317 fn rs_crate_02_parse_crate_name_no_hyphen() {
2318 let tmp = tempfile::tempdir().unwrap();
2320 std::fs::write(
2321 tmp.path().join("Cargo.toml"),
2322 "[package]\nname = \"tokio\"\nversion = \"1.0.0\"\n",
2323 )
2324 .unwrap();
2325
2326 let result = parse_crate_name(tmp.path());
2328
2329 assert_eq!(result, Some("tokio".to_string()));
2331 }
2332
2333 #[test]
2337 fn rs_crate_03_parse_crate_name_no_file() {
2338 let tmp = tempfile::tempdir().unwrap();
2340
2341 let result = parse_crate_name(tmp.path());
2343
2344 assert_eq!(result, None);
2346 }
2347
2348 #[test]
2352 fn rs_crate_04_parse_crate_name_workspace() {
2353 let tmp = tempfile::tempdir().unwrap();
2355 std::fs::write(
2356 tmp.path().join("Cargo.toml"),
2357 "[workspace]\nmembers = [\"crate1\"]\n",
2358 )
2359 .unwrap();
2360
2361 let result = parse_crate_name(tmp.path());
2363
2364 assert_eq!(result, None);
2366 }
2367
2368 #[test]
2372 fn rs_imp_05_crate_name_simple_import() {
2373 let source = "use my_crate::user::User;\n";
2375
2376 let result = extract_import_specifiers_with_crate_name(source, Some("my_crate"));
2378
2379 let entry = result.iter().find(|(spec, _)| spec == "user");
2381 assert!(entry.is_some(), "user not found in {:?}", result);
2382 let (_, symbols) = entry.unwrap();
2383 assert!(
2384 symbols.contains(&"User".to_string()),
2385 "User not in {:?}",
2386 symbols
2387 );
2388 }
2389
2390 #[test]
2394 fn rs_imp_06_crate_name_use_list() {
2395 let source = "use my_crate::user::{User, Admin};\n";
2397
2398 let result = extract_import_specifiers_with_crate_name(source, Some("my_crate"));
2400
2401 let entry = result.iter().find(|(spec, _)| spec == "user");
2403 assert!(entry.is_some(), "user not found in {:?}", result);
2404 let (_, symbols) = entry.unwrap();
2405 assert!(
2406 symbols.contains(&"User".to_string()),
2407 "User not in {:?}",
2408 symbols
2409 );
2410 assert!(
2411 symbols.contains(&"Admin".to_string()),
2412 "Admin not in {:?}",
2413 symbols
2414 );
2415 }
2416
2417 #[test]
2421 fn rs_imp_07_crate_name_none_skips() {
2422 let source = "use my_crate::user::User;\n";
2424
2425 let result = extract_import_specifiers_with_crate_name(source, None);
2427
2428 assert!(
2430 result.is_empty(),
2431 "Expected empty result when crate_name=None, got: {:?}",
2432 result
2433 );
2434 }
2435
2436 #[test]
2440 fn rs_imp_08_mixed_crate_and_crate_name() {
2441 let source = "use crate::service::Service;\nuse my_crate::user::User;\n";
2444
2445 let result = extract_import_specifiers_with_crate_name(source, Some("my_crate"));
2447
2448 let service_entry = result.iter().find(|(spec, _)| spec == "service");
2450 assert!(service_entry.is_some(), "service not found in {:?}", result);
2451 let (_, service_symbols) = service_entry.unwrap();
2452 assert!(
2453 service_symbols.contains(&"Service".to_string()),
2454 "Service not in {:?}",
2455 service_symbols
2456 );
2457
2458 let user_entry = result.iter().find(|(spec, _)| spec == "user");
2459 assert!(user_entry.is_some(), "user not found in {:?}", result);
2460 let (_, user_symbols) = user_entry.unwrap();
2461 assert!(
2462 user_symbols.contains(&"User".to_string()),
2463 "User not in {:?}",
2464 user_symbols
2465 );
2466 }
2467
2468 #[test]
2472 fn rs_l2_integ_crate_name_import_layer2() {
2473 let tmp = tempfile::tempdir().unwrap();
2478 let src_dir = tmp.path().join("src");
2479 let tests_dir = tmp.path().join("tests");
2480 std::fs::create_dir_all(&src_dir).unwrap();
2481 std::fs::create_dir_all(&tests_dir).unwrap();
2482
2483 std::fs::write(
2484 tmp.path().join("Cargo.toml"),
2485 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
2486 )
2487 .unwrap();
2488
2489 let user_rs = src_dir.join("user.rs");
2490 std::fs::write(&user_rs, "pub struct User;\n").unwrap();
2491
2492 let test_user_rs = tests_dir.join("test_user.rs");
2493 let test_source = "use my_crate::user::User;\n\n#[test]\nfn test_user() {}\n";
2494 std::fs::write(&test_user_rs, test_source).unwrap();
2495
2496 let extractor = RustExtractor::new();
2497 let prod_path = user_rs.to_string_lossy().into_owned();
2498 let test_path = test_user_rs.to_string_lossy().into_owned();
2499 let production_files = vec![prod_path.clone()];
2500 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
2501 .into_iter()
2502 .collect();
2503
2504 let result = extractor.map_test_files_with_imports(
2506 &production_files,
2507 &test_sources,
2508 tmp.path(),
2509 false,
2510 );
2511
2512 let mapping = result.iter().find(|m| m.production_file == prod_path);
2514 assert!(mapping.is_some(), "production file mapping not found");
2515 let mapping = mapping.unwrap();
2516 assert!(
2517 mapping.test_files.contains(&test_path),
2518 "Expected test_user.rs to map to user.rs via Layer 2, got: {:?}",
2519 mapping.test_files
2520 );
2521 assert_eq!(
2522 mapping.strategy,
2523 MappingStrategy::ImportTracing,
2524 "Expected ImportTracing strategy, got: {:?}",
2525 mapping.strategy
2526 );
2527 }
2528
2529 #[test]
2533 fn rs_deep_reexport_01_two_hop() {
2534 let tmp = tempfile::tempdir().unwrap();
2540 let src_models_dir = tmp.path().join("src").join("models");
2541 let tests_dir = tmp.path().join("tests");
2542 std::fs::create_dir_all(&src_models_dir).unwrap();
2543 std::fs::create_dir_all(&tests_dir).unwrap();
2544
2545 std::fs::write(
2546 tmp.path().join("Cargo.toml"),
2547 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
2548 )
2549 .unwrap();
2550
2551 let mod_rs = src_models_dir.join("mod.rs");
2552 std::fs::write(&mod_rs, "pub mod user;\n").unwrap();
2553
2554 let user_rs = src_models_dir.join("user.rs");
2555 std::fs::write(&user_rs, "pub struct User;\n").unwrap();
2556
2557 let test_models_rs = tests_dir.join("test_models.rs");
2558 let test_source = "use my_crate::models::User;\n\n#[test]\nfn test_user() {}\n";
2559 std::fs::write(&test_models_rs, test_source).unwrap();
2560
2561 let extractor = RustExtractor::new();
2562 let user_path = user_rs.to_string_lossy().into_owned();
2563 let test_path = test_models_rs.to_string_lossy().into_owned();
2564 let production_files = vec![user_path.clone()];
2565 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
2566 .into_iter()
2567 .collect();
2568
2569 let result = extractor.map_test_files_with_imports(
2571 &production_files,
2572 &test_sources,
2573 tmp.path(),
2574 false,
2575 );
2576
2577 let mapping = result.iter().find(|m| m.production_file == user_path);
2579 assert!(mapping.is_some(), "production file mapping not found");
2580 let mapping = mapping.unwrap();
2581 assert!(
2582 mapping.test_files.contains(&test_path),
2583 "Expected test_models.rs to map to user.rs via Layer 2 (pub mod chain), got: {:?}",
2584 mapping.test_files
2585 );
2586 assert_eq!(
2587 mapping.strategy,
2588 MappingStrategy::ImportTracing,
2589 "Expected ImportTracing strategy, got: {:?}",
2590 mapping.strategy
2591 );
2592 }
2593
2594 #[test]
2600 fn rs_deep_reexport_02_three_hop() {
2601 let tmp = tempfile::tempdir().unwrap();
2608 let src_dir = tmp.path().join("src");
2609 let src_models_dir = src_dir.join("models");
2610 let tests_dir = tmp.path().join("tests");
2611 std::fs::create_dir_all(&src_models_dir).unwrap();
2612 std::fs::create_dir_all(&tests_dir).unwrap();
2613
2614 std::fs::write(
2615 tmp.path().join("Cargo.toml"),
2616 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
2617 )
2618 .unwrap();
2619
2620 std::fs::write(src_dir.join("lib.rs"), "pub mod models;\n").unwrap();
2621
2622 let mod_rs = src_models_dir.join("mod.rs");
2623 std::fs::write(&mod_rs, "pub mod user;\n").unwrap();
2624
2625 let user_rs = src_models_dir.join("user.rs");
2626 std::fs::write(&user_rs, "pub struct User;\n").unwrap();
2627
2628 let test_account_rs = tests_dir.join("test_account.rs");
2630 let test_source = "use my_crate::models::User;\n\n#[test]\nfn test_account() {}\n";
2631 std::fs::write(&test_account_rs, test_source).unwrap();
2632
2633 let extractor = RustExtractor::new();
2634 let user_path = user_rs.to_string_lossy().into_owned();
2635 let test_path = test_account_rs.to_string_lossy().into_owned();
2636 let production_files = vec![user_path.clone()];
2637 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
2638 .into_iter()
2639 .collect();
2640
2641 let result = extractor.map_test_files_with_imports(
2643 &production_files,
2644 &test_sources,
2645 tmp.path(),
2646 false,
2647 );
2648
2649 let mapping = result.iter().find(|m| m.production_file == user_path);
2652 assert!(mapping.is_some(), "production file mapping not found");
2653 let mapping = mapping.unwrap();
2654 assert!(
2655 mapping.test_files.contains(&test_path),
2656 "Expected test_account.rs to map to user.rs via Layer 2 (3-hop pub mod chain), got: {:?}",
2657 mapping.test_files
2658 );
2659 assert_eq!(
2660 mapping.strategy,
2661 MappingStrategy::ImportTracing,
2662 "Expected ImportTracing strategy, got: {:?}",
2663 mapping.strategy
2664 );
2665 }
2666
2667 #[test]
2671 fn rs_deep_reexport_03_pub_use_and_pub_mod() {
2672 let source = "pub mod internal;\npub use internal::Exported;\n";
2674
2675 let extractor = RustExtractor::new();
2677 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
2678
2679 let wildcard_entry = result
2683 .iter()
2684 .find(|e| e.from_specifier == "./internal" && e.wildcard);
2685 assert!(
2686 wildcard_entry.is_some(),
2687 "Expected wildcard=true entry for pub mod internal, got: {:?}",
2688 result
2689 );
2690
2691 let symbol_entry = result.iter().find(|e| {
2692 e.from_specifier == "./internal"
2693 && !e.wildcard
2694 && e.symbols.contains(&"Exported".to_string())
2695 });
2696 assert!(
2697 symbol_entry.is_some(),
2698 "Expected symbols=[\"Exported\"] entry for pub use internal::Exported, got: {:?}",
2699 result
2700 );
2701 }
2702
2703 #[test]
2707 fn rs_export_01_pub_fn_match() {
2708 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2710 .join("../../tests/fixtures/rust/observe/exported_pub_symbols.rs");
2711 let extractor = RustExtractor::new();
2712 let symbols = vec!["create_user".to_string()];
2713
2714 let result = extractor.file_exports_any_symbol(&path, &symbols);
2716
2717 assert!(result, "Expected true for pub fn create_user");
2719 }
2720
2721 #[test]
2725 fn rs_export_02_pub_struct_match() {
2726 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2728 .join("../../tests/fixtures/rust/observe/exported_pub_symbols.rs");
2729 let extractor = RustExtractor::new();
2730 let symbols = vec!["User".to_string()];
2731
2732 let result = extractor.file_exports_any_symbol(&path, &symbols);
2734
2735 assert!(result, "Expected true for pub struct User");
2737 }
2738
2739 #[test]
2743 fn rs_export_03_nonexistent_symbol() {
2744 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2746 .join("../../tests/fixtures/rust/observe/exported_pub_symbols.rs");
2747 let extractor = RustExtractor::new();
2748 let symbols = vec!["NonExistent".to_string()];
2749
2750 let result = extractor.file_exports_any_symbol(&path, &symbols);
2752
2753 assert!(!result, "Expected false for NonExistent symbol");
2755 }
2756
2757 #[test]
2761 fn rs_export_04_no_pub_symbols() {
2762 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2764 .join("../../tests/fixtures/rust/observe/no_pub_symbols.rs");
2765 let extractor = RustExtractor::new();
2766 let symbols = vec!["internal_only".to_string()];
2767
2768 let result = extractor.file_exports_any_symbol(&path, &symbols);
2770
2771 assert!(!result, "Expected false for file with no pub symbols");
2773 }
2774
2775 #[test]
2779 fn rs_export_05_pub_use_mod_only() {
2780 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2782 .join("../../tests/fixtures/rust/observe/pub_use_only.rs");
2783 let extractor = RustExtractor::new();
2784 let symbols = vec!["Foo".to_string()];
2785
2786 let result = extractor.file_exports_any_symbol(&path, &symbols);
2788
2789 assert!(
2791 !result,
2792 "Expected false for pub use/mod only file (barrel resolution handles these)"
2793 );
2794 }
2795
2796 #[test]
2800 fn rs_export_06_empty_symbols() {
2801 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2803 .join("../../tests/fixtures/rust/observe/exported_pub_symbols.rs");
2804 let extractor = RustExtractor::new();
2805 let symbols: Vec<String> = vec![];
2806
2807 let result = extractor.file_exports_any_symbol(&path, &symbols);
2809
2810 assert!(result, "Expected true for empty symbol list");
2812 }
2813
2814 #[test]
2818 fn rs_export_07_nonexistent_file() {
2819 let path = PathBuf::from("/nonexistent/path/to/file.rs");
2821 let extractor = RustExtractor::new();
2822 let symbols = vec!["Foo".to_string()];
2823
2824 let result = extractor.file_exports_any_symbol(&path, &symbols);
2827 assert!(
2828 result,
2829 "Expected true for non-existent file (optimistic fallback)"
2830 );
2831 }
2832
2833 #[test]
2837 fn rs_export_pub_only_01_pub_fn_matches() {
2838 let dir = tempfile::tempdir().unwrap();
2840 let file = dir.path().join("service.rs");
2841 std::fs::write(&file, "pub fn create_user() {}").unwrap();
2842 let extractor = RustExtractor::new();
2843 let symbols = vec!["create_user".to_string()];
2844
2845 let result = extractor.file_exports_any_symbol(&file, &symbols);
2847
2848 assert!(result, "Expected true for pub fn create_user");
2850 }
2851
2852 #[test]
2856 fn rs_export_pub_only_02_pub_crate_excluded() {
2857 let dir = tempfile::tempdir().unwrap();
2859 let file = dir.path().join("driver.rs");
2860 std::fs::write(&file, "pub(crate) struct Handle {}").unwrap();
2861 let extractor = RustExtractor::new();
2862 let symbols = vec!["Handle".to_string()];
2863
2864 let result = extractor.file_exports_any_symbol(&file, &symbols);
2866
2867 assert!(!result, "Expected false for pub(crate) struct Handle");
2869 }
2870
2871 #[test]
2875 fn rs_export_pub_only_03_pub_super_excluded() {
2876 let dir = tempfile::tempdir().unwrap();
2878 let file = dir.path().join("internal.rs");
2879 std::fs::write(&file, "pub(super) fn helper() {}").unwrap();
2880 let extractor = RustExtractor::new();
2881 let symbols = vec!["helper".to_string()];
2882
2883 let result = extractor.file_exports_any_symbol(&file, &symbols);
2885
2886 assert!(!result, "Expected false for pub(super) fn helper");
2888 }
2889
2890 #[test]
2894 fn rs_export_pub_only_04_mixed_visibility() {
2895 let dir = tempfile::tempdir().unwrap();
2897 let file = dir.path().join("models.rs");
2898 std::fs::write(&file, "pub struct User {}\npub(crate) struct Inner {}").unwrap();
2899 let extractor = RustExtractor::new();
2900 let symbols = vec!["User".to_string()];
2901
2902 let result = extractor.file_exports_any_symbol(&file, &symbols);
2904
2905 assert!(
2907 result,
2908 "Expected true for pub struct User in mixed visibility file"
2909 );
2910 }
2911
2912 #[test]
2916 fn rs_ws_01_workspace_two_members() {
2917 let tmp = tempfile::tempdir().unwrap();
2919 std::fs::write(
2920 tmp.path().join("Cargo.toml"),
2921 "[workspace]\nmembers = [\"crate_a\", \"crate_b\"]\n",
2922 )
2923 .unwrap();
2924 std::fs::create_dir_all(tmp.path().join("crate_a/src")).unwrap();
2925 std::fs::write(
2926 tmp.path().join("crate_a/Cargo.toml"),
2927 "[package]\nname = \"crate_a\"\nversion = \"0.1.0\"\n",
2928 )
2929 .unwrap();
2930 std::fs::create_dir_all(tmp.path().join("crate_b/src")).unwrap();
2931 std::fs::write(
2932 tmp.path().join("crate_b/Cargo.toml"),
2933 "[package]\nname = \"crate_b\"\nversion = \"0.1.0\"\n",
2934 )
2935 .unwrap();
2936
2937 let members = find_workspace_members(tmp.path());
2939
2940 assert_eq!(members.len(), 2, "Expected 2 members, got: {:?}", members);
2942 let names: Vec<&str> = members.iter().map(|m| m.crate_name.as_str()).collect();
2943 assert!(
2944 names.contains(&"crate_a"),
2945 "crate_a not found in {:?}",
2946 names
2947 );
2948 assert!(
2949 names.contains(&"crate_b"),
2950 "crate_b not found in {:?}",
2951 names
2952 );
2953 }
2954
2955 #[test]
2959 fn rs_ws_02_single_crate_returns_empty() {
2960 let tmp = tempfile::tempdir().unwrap();
2962 std::fs::write(
2963 tmp.path().join("Cargo.toml"),
2964 "[package]\nname = \"my_crate\"\nversion = \"0.1.0\"\n",
2965 )
2966 .unwrap();
2967 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
2968
2969 let members = find_workspace_members(tmp.path());
2971
2972 assert!(members.is_empty(), "Expected empty, got: {:?}", members);
2974 }
2975
2976 #[test]
2980 fn rs_ws_03_target_dir_skipped() {
2981 let tmp = tempfile::tempdir().unwrap();
2983 std::fs::write(
2984 tmp.path().join("Cargo.toml"),
2985 "[workspace]\nmembers = [\"crate_a\"]\n",
2986 )
2987 .unwrap();
2988 std::fs::create_dir_all(tmp.path().join("crate_a/src")).unwrap();
2989 std::fs::write(
2990 tmp.path().join("crate_a/Cargo.toml"),
2991 "[package]\nname = \"crate_a\"\nversion = \"0.1.0\"\n",
2992 )
2993 .unwrap();
2994 std::fs::create_dir_all(tmp.path().join("target/debug/build/fake")).unwrap();
2996 std::fs::write(
2997 tmp.path().join("target/debug/build/fake/Cargo.toml"),
2998 "[package]\nname = \"fake_crate\"\nversion = \"0.1.0\"\n",
2999 )
3000 .unwrap();
3001
3002 let members = find_workspace_members(tmp.path());
3004
3005 assert_eq!(members.len(), 1, "Expected 1 member, got: {:?}", members);
3007 assert_eq!(members[0].crate_name, "crate_a");
3008 }
3009
3010 #[test]
3014 fn rs_ws_04_hyphenated_crate_name_converted() {
3015 let tmp = tempfile::tempdir().unwrap();
3017 std::fs::write(
3018 tmp.path().join("Cargo.toml"),
3019 "[workspace]\nmembers = [\"my-crate\"]\n",
3020 )
3021 .unwrap();
3022 std::fs::create_dir_all(tmp.path().join("my-crate/src")).unwrap();
3023 std::fs::write(
3024 tmp.path().join("my-crate/Cargo.toml"),
3025 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\n",
3026 )
3027 .unwrap();
3028
3029 let members = find_workspace_members(tmp.path());
3031
3032 assert_eq!(members.len(), 1, "Expected 1 member, got: {:?}", members);
3034 assert_eq!(members[0].crate_name, "my_crate");
3035 }
3036
3037 #[test]
3041 fn rs_ws_05_find_member_for_path_in_tests() {
3042 let tmp = tempfile::tempdir().unwrap();
3044 let member_root = tmp.path().join("crate_a");
3045 std::fs::create_dir_all(&member_root).unwrap();
3046 let members = vec![WorkspaceMember {
3047 crate_name: "crate_a".to_string(),
3048 member_root: member_root.clone(),
3049 }];
3050
3051 let test_file = member_root.join("tests").join("integration.rs");
3053 let result = find_member_for_path(&test_file, &members);
3054
3055 assert!(result.is_some(), "Expected Some(crate_a), got None");
3057 assert_eq!(result.unwrap().crate_name, "crate_a");
3058 }
3059
3060 #[test]
3064 fn rs_ws_06_find_member_for_path_not_in_any() {
3065 let tmp = tempfile::tempdir().unwrap();
3067 let member_root = tmp.path().join("crate_a");
3068 std::fs::create_dir_all(&member_root).unwrap();
3069 let members = vec![WorkspaceMember {
3070 crate_name: "crate_a".to_string(),
3071 member_root: member_root.clone(),
3072 }];
3073
3074 let outside_path = tmp.path().join("other").join("test.rs");
3076 let result = find_member_for_path(&outside_path, &members);
3077
3078 assert!(
3080 result.is_none(),
3081 "Expected None, got: {:?}",
3082 result.map(|m| &m.crate_name)
3083 );
3084 }
3085
3086 #[test]
3090 fn rs_ws_07_find_member_longest_prefix() {
3091 let tmp = tempfile::tempdir().unwrap();
3093 let foo_root = tmp.path().join("crates").join("foo");
3094 let foo_extra_root = tmp.path().join("crates").join("foo-extra");
3095 std::fs::create_dir_all(&foo_root).unwrap();
3096 std::fs::create_dir_all(&foo_extra_root).unwrap();
3097 let members = vec![
3098 WorkspaceMember {
3099 crate_name: "foo".to_string(),
3100 member_root: foo_root.clone(),
3101 },
3102 WorkspaceMember {
3103 crate_name: "foo_extra".to_string(),
3104 member_root: foo_extra_root.clone(),
3105 },
3106 ];
3107
3108 let test_file = foo_extra_root.join("tests").join("test_bar.rs");
3110 let result = find_member_for_path(&test_file, &members);
3111
3112 assert!(result.is_some(), "Expected Some(foo_extra), got None");
3114 assert_eq!(result.unwrap().crate_name, "foo_extra");
3115 }
3116
3117 #[test]
3121 fn rs_ws_e2e_01_workspace_l2_import_tracing() {
3122 let tmp = tempfile::tempdir().unwrap();
3125 std::fs::write(
3126 tmp.path().join("Cargo.toml"),
3127 "[workspace]\nmembers = [\"crate_a\"]\n",
3128 )
3129 .unwrap();
3130
3131 let member_dir = tmp.path().join("crate_a");
3132 std::fs::create_dir_all(member_dir.join("src")).unwrap();
3133 std::fs::create_dir_all(member_dir.join("tests")).unwrap();
3134 std::fs::write(
3135 member_dir.join("Cargo.toml"),
3136 "[package]\nname = \"crate_a\"\nversion = \"0.1.0\"\n",
3137 )
3138 .unwrap();
3139
3140 let user_rs = member_dir.join("src").join("user.rs");
3141 std::fs::write(&user_rs, "pub fn create_user() {}\n").unwrap();
3142
3143 let test_rs = member_dir.join("tests").join("test_user.rs");
3144 std::fs::write(
3145 &test_rs,
3146 "use crate_a::user::create_user;\n#[test]\nfn test_create_user() { create_user(); }\n",
3147 )
3148 .unwrap();
3149
3150 let extractor = RustExtractor::new();
3151 let prod_path = user_rs.to_string_lossy().into_owned();
3152 let test_path = test_rs.to_string_lossy().into_owned();
3153 let production_files = vec![prod_path.clone()];
3154 let test_sources: HashMap<String, String> = [(
3155 test_path.clone(),
3156 std::fs::read_to_string(&test_rs).unwrap(),
3157 )]
3158 .into_iter()
3159 .collect();
3160
3161 let result = extractor.map_test_files_with_imports(
3163 &production_files,
3164 &test_sources,
3165 tmp.path(),
3166 false,
3167 );
3168
3169 let mapping = result.iter().find(|m| m.production_file == prod_path);
3171 assert!(mapping.is_some(), "No mapping for user.rs");
3172 let mapping = mapping.unwrap();
3173 assert!(
3174 mapping.test_files.contains(&test_path),
3175 "Expected test_user.rs in test_files, got: {:?}",
3176 mapping.test_files
3177 );
3178 assert_eq!(
3179 mapping.strategy,
3180 MappingStrategy::ImportTracing,
3181 "Expected ImportTracing strategy, got: {:?}",
3182 mapping.strategy
3183 );
3184 }
3185
3186 #[test]
3196 fn rs_ws_e2e_02_l0_l1_still_work_at_workspace_level() {
3197 let tmp = tempfile::tempdir().unwrap();
3200 std::fs::write(
3201 tmp.path().join("Cargo.toml"),
3202 "[workspace]\nmembers = [\"crate_a\"]\n",
3203 )
3204 .unwrap();
3205
3206 let member_dir = tmp.path().join("crate_a");
3207 std::fs::create_dir_all(member_dir.join("src")).unwrap();
3208 std::fs::write(
3209 member_dir.join("Cargo.toml"),
3210 "[package]\nname = \"crate_a\"\nversion = \"0.1.0\"\n",
3211 )
3212 .unwrap();
3213
3214 let service_rs = member_dir.join("src").join("service.rs");
3216 std::fs::write(
3217 &service_rs,
3218 r#"pub fn do_work() {}
3219
3220#[cfg(test)]
3221mod tests {
3222 use super::*;
3223 #[test]
3224 fn test_do_work() { do_work(); }
3225}
3226"#,
3227 )
3228 .unwrap();
3229
3230 let test_service_rs = member_dir.join("src").join("test_service.rs");
3232 std::fs::write(
3233 &test_service_rs,
3234 "#[test]\nfn test_service_smoke() { assert!(true); }\n",
3235 )
3236 .unwrap();
3237
3238 let extractor = RustExtractor::new();
3239 let prod_path = service_rs.to_string_lossy().into_owned();
3240 let test_path = test_service_rs.to_string_lossy().into_owned();
3241 let production_files = vec![prod_path.clone()];
3242 let test_sources: HashMap<String, String> = [(
3243 test_path.clone(),
3244 std::fs::read_to_string(&test_service_rs).unwrap(),
3245 )]
3246 .into_iter()
3247 .collect();
3248
3249 let result = extractor.map_test_files_with_imports(
3251 &production_files,
3252 &test_sources,
3253 tmp.path(),
3254 false,
3255 );
3256
3257 let mapping = result.iter().find(|m| m.production_file == prod_path);
3259 assert!(mapping.is_some(), "No mapping for service.rs");
3260 let mapping = mapping.unwrap();
3261 assert!(
3262 mapping.test_files.contains(&prod_path),
3263 "Expected service.rs self-mapped (Layer 0), got: {:?}",
3264 mapping.test_files
3265 );
3266 assert!(
3267 mapping.test_files.contains(&test_path),
3268 "Expected test_service.rs mapped (Layer 1), got: {:?}",
3269 mapping.test_files
3270 );
3271 }
3272
3273 #[test]
3280 fn rs_ws_e2e_03_non_virtual_workspace_l2() {
3281 let tmp = tempfile::tempdir().unwrap();
3284 std::fs::write(
3285 tmp.path().join("Cargo.toml"),
3286 "[workspace]\nmembers = [\"member_a\"]\n\n[package]\nname = \"root_pkg\"\nversion = \"0.1.0\"\n",
3287 )
3288 .unwrap();
3289
3290 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
3292 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
3293 let root_src = tmp.path().join("src").join("lib.rs");
3294 std::fs::write(&root_src, "pub fn root_fn() {}\n").unwrap();
3295 let root_test = tmp.path().join("tests").join("test_root.rs");
3296 std::fs::write(
3297 &root_test,
3298 "use root_pkg::lib::root_fn;\n#[test]\nfn test_root() { }\n",
3299 )
3300 .unwrap();
3301
3302 let member_dir = tmp.path().join("member_a");
3304 std::fs::create_dir_all(member_dir.join("src")).unwrap();
3305 std::fs::create_dir_all(member_dir.join("tests")).unwrap();
3306 std::fs::write(
3307 member_dir.join("Cargo.toml"),
3308 "[package]\nname = \"member_a\"\nversion = \"0.1.0\"\n",
3309 )
3310 .unwrap();
3311 let member_src = member_dir.join("src").join("handler.rs");
3312 std::fs::write(&member_src, "pub fn handle() {}\n").unwrap();
3313 let member_test = member_dir.join("tests").join("test_handler.rs");
3314 std::fs::write(
3315 &member_test,
3316 "use member_a::handler::handle;\n#[test]\nfn test_handle() { handle(); }\n",
3317 )
3318 .unwrap();
3319
3320 let extractor = RustExtractor::new();
3321 let root_src_path = root_src.to_string_lossy().into_owned();
3322 let member_src_path = member_src.to_string_lossy().into_owned();
3323 let root_test_path = root_test.to_string_lossy().into_owned();
3324 let member_test_path = member_test.to_string_lossy().into_owned();
3325
3326 let production_files = vec![root_src_path.clone(), member_src_path.clone()];
3327 let test_sources: HashMap<String, String> = [
3328 (
3329 root_test_path.clone(),
3330 std::fs::read_to_string(&root_test).unwrap(),
3331 ),
3332 (
3333 member_test_path.clone(),
3334 std::fs::read_to_string(&member_test).unwrap(),
3335 ),
3336 ]
3337 .into_iter()
3338 .collect();
3339
3340 let result = extractor.map_test_files_with_imports(
3342 &production_files,
3343 &test_sources,
3344 tmp.path(),
3345 false,
3346 );
3347
3348 let member_mapping = result.iter().find(|m| m.production_file == member_src_path);
3350 assert!(member_mapping.is_some(), "No mapping for member handler.rs");
3351 let member_mapping = member_mapping.unwrap();
3352 assert!(
3353 member_mapping.test_files.contains(&member_test_path),
3354 "Expected member test mapped via L2, got: {:?}",
3355 member_mapping.test_files
3356 );
3357 assert_eq!(
3358 member_mapping.strategy,
3359 MappingStrategy::ImportTracing,
3360 "Expected ImportTracing for member, got: {:?}",
3361 member_mapping.strategy
3362 );
3363 }
3364
3365 #[test]
3369 fn rs_ws_08_has_workspace_section() {
3370 let tmp = tempfile::tempdir().unwrap();
3371
3372 std::fs::write(
3374 tmp.path().join("Cargo.toml"),
3375 "[workspace]\nmembers = [\"a\"]\n",
3376 )
3377 .unwrap();
3378 assert!(has_workspace_section(tmp.path()));
3379
3380 std::fs::write(
3382 tmp.path().join("Cargo.toml"),
3383 "[workspace]\nmembers = [\"a\"]\n\n[package]\nname = \"root\"\n",
3384 )
3385 .unwrap();
3386 assert!(has_workspace_section(tmp.path()));
3387
3388 std::fs::write(
3390 tmp.path().join("Cargo.toml"),
3391 "[package]\nname = \"single\"\n",
3392 )
3393 .unwrap();
3394 assert!(!has_workspace_section(tmp.path()));
3395
3396 std::fs::remove_file(tmp.path().join("Cargo.toml")).unwrap();
3398 assert!(!has_workspace_section(tmp.path()));
3399 }
3400
3401 #[test]
3405 fn rs_l0_barrel_01_mod_rs_excluded() {
3406 let tmp = tempfile::tempdir().unwrap();
3408 let src_dir = tmp.path().join("src");
3409 std::fs::create_dir_all(&src_dir).unwrap();
3410
3411 let mod_rs = src_dir.join("mod.rs");
3412 std::fs::write(
3413 &mod_rs,
3414 r#"pub mod sub;
3415
3416#[cfg(test)]
3417mod tests {
3418 #[test]
3419 fn test_something() {}
3420}
3421"#,
3422 )
3423 .unwrap();
3424
3425 let extractor = RustExtractor::new();
3426 let prod_path = mod_rs.to_string_lossy().into_owned();
3427 let production_files = vec![prod_path.clone()];
3428 let test_sources: HashMap<String, String> = HashMap::new();
3429
3430 let result = extractor.map_test_files_with_imports(
3432 &production_files,
3433 &test_sources,
3434 tmp.path(),
3435 false,
3436 );
3437
3438 let mapping = result.iter().find(|m| m.production_file == prod_path);
3440 assert!(mapping.is_some());
3441 assert!(
3442 !mapping.unwrap().test_files.contains(&prod_path),
3443 "mod.rs should NOT be self-mapped, but found in: {:?}",
3444 mapping.unwrap().test_files
3445 );
3446 }
3447
3448 #[test]
3452 fn rs_l0_barrel_02_lib_rs_excluded() {
3453 let tmp = tempfile::tempdir().unwrap();
3455 let src_dir = tmp.path().join("src");
3456 std::fs::create_dir_all(&src_dir).unwrap();
3457
3458 let lib_rs = src_dir.join("lib.rs");
3459 std::fs::write(
3460 &lib_rs,
3461 r#"pub mod utils;
3462
3463#[cfg(test)]
3464mod tests {
3465 #[test]
3466 fn test_lib() {}
3467}
3468"#,
3469 )
3470 .unwrap();
3471
3472 let extractor = RustExtractor::new();
3473 let prod_path = lib_rs.to_string_lossy().into_owned();
3474 let production_files = vec![prod_path.clone()];
3475 let test_sources: HashMap<String, String> = HashMap::new();
3476
3477 let result = extractor.map_test_files_with_imports(
3479 &production_files,
3480 &test_sources,
3481 tmp.path(),
3482 false,
3483 );
3484
3485 let mapping = result.iter().find(|m| m.production_file == prod_path);
3487 assert!(mapping.is_some());
3488 assert!(
3489 !mapping.unwrap().test_files.contains(&prod_path),
3490 "lib.rs should NOT be self-mapped, but found in: {:?}",
3491 mapping.unwrap().test_files
3492 );
3493 }
3494
3495 #[test]
3499 fn rs_l0_barrel_03_regular_file_self_mapped() {
3500 let tmp = tempfile::tempdir().unwrap();
3502 let src_dir = tmp.path().join("src");
3503 std::fs::create_dir_all(&src_dir).unwrap();
3504
3505 let service_rs = src_dir.join("service.rs");
3506 std::fs::write(
3507 &service_rs,
3508 r#"pub fn do_work() {}
3509
3510#[cfg(test)]
3511mod tests {
3512 use super::*;
3513 #[test]
3514 fn test_do_work() { assert!(true); }
3515}
3516"#,
3517 )
3518 .unwrap();
3519
3520 let extractor = RustExtractor::new();
3521 let prod_path = service_rs.to_string_lossy().into_owned();
3522 let production_files = vec![prod_path.clone()];
3523 let test_sources: HashMap<String, String> = HashMap::new();
3524
3525 let result = extractor.map_test_files_with_imports(
3527 &production_files,
3528 &test_sources,
3529 tmp.path(),
3530 false,
3531 );
3532
3533 let mapping = result.iter().find(|m| m.production_file == prod_path);
3535 assert!(mapping.is_some());
3536 assert!(
3537 mapping.unwrap().test_files.contains(&prod_path),
3538 "service.rs should be self-mapped, but not found in: {:?}",
3539 mapping.unwrap().test_files
3540 );
3541 }
3542
3543 #[test]
3547 fn rs_l0_barrel_04_main_rs_excluded() {
3548 let tmp = tempfile::tempdir().unwrap();
3550 let src_dir = tmp.path().join("src");
3551 std::fs::create_dir_all(&src_dir).unwrap();
3552
3553 let main_rs = src_dir.join("main.rs");
3554 std::fs::write(
3555 &main_rs,
3556 r#"fn main() {}
3557
3558#[cfg(test)]
3559mod tests {
3560 #[test]
3561 fn test_main() {}
3562}
3563"#,
3564 )
3565 .unwrap();
3566
3567 let extractor = RustExtractor::new();
3568 let prod_path = main_rs.to_string_lossy().into_owned();
3569 let production_files = vec![prod_path.clone()];
3570 let test_sources: HashMap<String, String> = HashMap::new();
3571
3572 let result = extractor.map_test_files_with_imports(
3574 &production_files,
3575 &test_sources,
3576 tmp.path(),
3577 false,
3578 );
3579
3580 let mapping = result.iter().find(|m| m.production_file == prod_path);
3582 assert!(mapping.is_some());
3583 assert!(
3584 !mapping.unwrap().test_files.contains(&prod_path),
3585 "main.rs should NOT be self-mapped, but found in: {:?}",
3586 mapping.unwrap().test_files
3587 );
3588 }
3589
3590 #[test]
3595 fn rs_l0_detect_01_cfg_test_with_mod_block() {
3596 let source = r#"
3598pub fn add(a: i32, b: i32) -> i32 { a + b }
3599
3600#[cfg(test)]
3601mod tests {
3602 use super::*;
3603
3604 #[test]
3605 fn test_add() {
3606 assert_eq!(add(1, 2), 3);
3607 }
3608}
3609"#;
3610 assert!(detect_inline_tests(source));
3613 }
3614
3615 #[test]
3619 fn rs_l0_detect_02_cfg_test_for_helper_method() {
3620 let source = r#"
3622pub struct Connection;
3623
3624impl Connection {
3625 #[cfg(test)]
3626 pub fn test_helper(&self) -> bool {
3627 true
3628 }
3629}
3630"#;
3631 assert!(!detect_inline_tests(source));
3634 }
3635
3636 #[test]
3640 fn rs_l0_detect_03_cfg_test_for_use_statement() {
3641 let source = r#"
3643#[cfg(not(test))]
3644use real_http::Client;
3645
3646#[cfg(test)]
3647use mock_http::Client;
3648
3649pub fn fetch(url: &str) -> String {
3650 Client::get(url)
3651}
3652"#;
3653 assert!(!detect_inline_tests(source));
3656 }
3657
3658 #[test]
3663 fn rs_l0_detect_04_cfg_test_with_external_mod_ref() {
3664 let source = r#"
3666pub fn compute(x: i32) -> i32 { x * 2 }
3667
3668#[cfg(test)]
3669mod tests;
3670"#;
3671 assert!(detect_inline_tests(source));
3674 }
3675
3676 #[test]
3686 fn rs_l2_export_filter_01_no_export_not_mapped() {
3687 let tmp = tempfile::tempdir().unwrap();
3691 let src_runtime = tmp.path().join("src").join("runtime");
3692 let tests_dir = tmp.path().join("tests");
3693 std::fs::create_dir_all(&src_runtime).unwrap();
3694 std::fs::create_dir_all(&tests_dir).unwrap();
3695
3696 std::fs::write(
3698 tmp.path().join("Cargo.toml"),
3699 "[package]\nname = \"myapp\"\nversion = \"0.1.0\"\n",
3700 )
3701 .unwrap();
3702
3703 let driver_rs = src_runtime.join("driver.rs");
3705 std::fs::write(&driver_rs, "pub fn spawn() {}\npub struct Driver;\n").unwrap();
3706
3707 let test_rs = tests_dir.join("test_runtime.rs");
3710 let test_source = "use myapp::runtime::driver::{Builder};\n\n#[test]\nfn test_build() {}\n";
3711 std::fs::write(&test_rs, test_source).unwrap();
3712
3713 let extractor = RustExtractor::new();
3714 let driver_path = driver_rs.to_string_lossy().into_owned();
3715 let test_path = test_rs.to_string_lossy().into_owned();
3716 let production_files = vec![driver_path.clone()];
3717 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
3718 .into_iter()
3719 .collect();
3720
3721 let result = extractor.map_test_files_with_imports(
3723 &production_files,
3724 &test_sources,
3725 tmp.path(),
3726 false,
3727 );
3728
3729 let mapping = result.iter().find(|m| m.production_file == driver_path);
3732 if let Some(m) = mapping {
3733 assert!(
3734 !m.test_files.contains(&test_path),
3735 "driver.rs should NOT be mapped (does not export Builder), but found: {:?}",
3736 m.test_files
3737 );
3738 }
3739 }
3741
3742 #[test]
3748 fn rs_l2_export_filter_02_exports_symbol_is_mapped() {
3749 let tmp = tempfile::tempdir().unwrap();
3754 let src_app = tmp.path().join("src").join("app");
3755 let tests_dir = tmp.path().join("tests");
3756 std::fs::create_dir_all(&src_app).unwrap();
3757 std::fs::create_dir_all(&tests_dir).unwrap();
3758
3759 std::fs::write(
3761 tmp.path().join("Cargo.toml"),
3762 "[package]\nname = \"myapp\"\nversion = \"0.1.0\"\n",
3763 )
3764 .unwrap();
3765
3766 let mod_rs = src_app.join("mod.rs");
3768 std::fs::write(&mod_rs, "pub mod service;\n").unwrap();
3769
3770 let service_rs = src_app.join("service.rs");
3772 std::fs::write(&service_rs, "pub fn service_fn() {}\n").unwrap();
3773
3774 let test_rs = tests_dir.join("test_app.rs");
3776 let test_source = "use myapp::app::{service_fn};\n\n#[test]\nfn test_service() {}\n";
3777 std::fs::write(&test_rs, test_source).unwrap();
3778
3779 let extractor = RustExtractor::new();
3780 let service_path = service_rs.to_string_lossy().into_owned();
3781 let test_path = test_rs.to_string_lossy().into_owned();
3782 let production_files = vec![service_path.clone()];
3783 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
3784 .into_iter()
3785 .collect();
3786
3787 let result = extractor.map_test_files_with_imports(
3789 &production_files,
3790 &test_sources,
3791 tmp.path(),
3792 false,
3793 );
3794
3795 let mapping = result.iter().find(|m| m.production_file == service_path);
3798 assert!(mapping.is_some(), "service.rs should have a mapping entry");
3799 assert!(
3800 mapping.unwrap().test_files.contains(&test_path),
3801 "service.rs should be mapped to test_app.rs, got: {:?}",
3802 mapping.unwrap().test_files
3803 );
3804 }
3805
3806 #[test]
3810 fn rs_barrel_cfg_macro_pub_mod() {
3811 let source = r#"
3813cfg_feat! {
3814 pub mod sub;
3815}
3816"#;
3817
3818 let ext = RustExtractor::new();
3820 let result = ext.extract_barrel_re_exports(source, "src/mod.rs");
3821
3822 assert!(
3824 !result.is_empty(),
3825 "Expected non-empty result, got: {:?}",
3826 result
3827 );
3828 assert!(
3829 result
3830 .iter()
3831 .any(|r| r.from_specifier == "./sub" && r.wildcard),
3832 "./sub with wildcard=true not found in {:?}",
3833 result
3834 );
3835 }
3836
3837 #[test]
3841 fn rs_barrel_cfg_macro_pub_use_braces() {
3842 let source = r#"
3844cfg_feat! {
3845 pub use util::{Symbol};
3846}
3847"#;
3848
3849 let ext = RustExtractor::new();
3851 let result = ext.extract_barrel_re_exports(source, "src/mod.rs");
3852
3853 assert!(
3855 !result.is_empty(),
3856 "Expected non-empty result, got: {:?}",
3857 result
3858 );
3859 assert!(
3860 result.iter().any(|r| r.from_specifier == "./util"
3861 && !r.wildcard
3862 && r.symbols.contains(&"Symbol".to_string())),
3863 "./util with symbols=[\"Symbol\"] not found in {:?}",
3864 result
3865 );
3866 }
3867
3868 #[test]
3872 fn rs_barrel_top_level_regression() {
3873 let source = "pub mod foo;\n";
3875
3876 let ext = RustExtractor::new();
3878 let result = ext.extract_barrel_re_exports(source, "src/mod.rs");
3879
3880 let entry = result.iter().find(|e| e.from_specifier == "./foo");
3882 assert!(
3883 entry.is_some(),
3884 "./foo not found in {:?} (regression: top-level pub mod broken)",
3885 result
3886 );
3887 assert!(entry.unwrap().wildcard);
3888 }
3889
3890 #[test]
3894 fn rs_imp_09_single_segment_module_import() {
3895 let source = "use crate::fs;\n";
3897
3898 let extractor = RustExtractor::new();
3900 let result = extractor.extract_all_import_specifiers(source);
3901
3902 let entry = result.iter().find(|(spec, _)| spec == "fs");
3904 assert!(
3905 entry.is_some(),
3906 "fs not found in {:?} (single-segment module import should be registered)",
3907 result
3908 );
3909 let (_, symbols) = entry.unwrap();
3910 assert!(
3911 symbols.is_empty(),
3912 "Expected empty symbols for module import, got: {:?}",
3913 symbols
3914 );
3915 }
3916
3917 #[test]
3921 fn rs_imp_10_single_segment_with_crate_name() {
3922 let source = "use my_crate::util;\n";
3924
3925 let result = extract_import_specifiers_with_crate_name(source, Some("my_crate"));
3927
3928 let entry = result.iter().find(|(spec, _)| spec == "util");
3930 assert!(
3931 entry.is_some(),
3932 "util not found in {:?} (single-segment with crate_name should be registered)",
3933 result
3934 );
3935 let (_, symbols) = entry.unwrap();
3936 assert!(
3937 symbols.is_empty(),
3938 "Expected empty symbols for module import, got: {:?}",
3939 symbols
3940 );
3941 }
3942
3943 #[test]
3947 fn rs_barrel_self_01_strips_self_from_wildcard() {
3948 let source = "pub use self::sub::*;\n";
3950
3951 let extractor = RustExtractor::new();
3953 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
3954
3955 let entry = result.iter().find(|e| e.from_specifier == "./sub");
3957 assert!(
3958 entry.is_some(),
3959 "./sub not found in {:?} (self:: prefix should be stripped from wildcard)",
3960 result
3961 );
3962 assert!(
3963 entry.unwrap().wildcard,
3964 "Expected wildcard=true for pub use self::sub::*"
3965 );
3966 }
3967
3968 #[test]
3972 fn rs_barrel_self_02_strips_self_from_symbol() {
3973 let source = "pub use self::file::File;\n";
3975
3976 let extractor = RustExtractor::new();
3978 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
3979
3980 let entry = result.iter().find(|e| e.from_specifier == "./file");
3982 assert!(
3983 entry.is_some(),
3984 "./file not found in {:?} (self:: prefix should be stripped from symbol import)",
3985 result
3986 );
3987 let entry = entry.unwrap();
3988 assert!(
3989 entry.symbols.contains(&"File".to_string()),
3990 "Expected symbols=[\"File\"], got: {:?}",
3991 entry.symbols
3992 );
3993 }
3994
3995 #[test]
3999 fn rs_barrel_self_03_strips_self_from_use_list() {
4000 let source = "pub use self::sync::{Mutex, RwLock};\n";
4002
4003 let extractor = RustExtractor::new();
4005 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
4006
4007 let entry = result.iter().find(|e| e.from_specifier == "./sync");
4009 assert!(
4010 entry.is_some(),
4011 "./sync not found in {:?} (self:: prefix should be stripped from use list)",
4012 result
4013 );
4014 let entry = entry.unwrap();
4015 assert!(
4016 entry.symbols.contains(&"Mutex".to_string()),
4017 "Expected Mutex in symbols, got: {:?}",
4018 entry.symbols
4019 );
4020 assert!(
4021 entry.symbols.contains(&"RwLock".to_string()),
4022 "Expected RwLock in symbols, got: {:?}",
4023 entry.symbols
4024 );
4025 }
4026
4027 #[test]
4031 fn rs_barrel_cfg_self_01_strips_self_in_cfg_macro() {
4032 let source = "cfg_feat! { pub use self::inner::Symbol; }\n";
4034
4035 let extractor = RustExtractor::new();
4037 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
4038
4039 let entry = result.iter().find(|e| e.from_specifier == "./inner");
4041 assert!(
4042 entry.is_some(),
4043 "./inner not found in {:?} (self:: prefix should be stripped in cfg macro text path)",
4044 result
4045 );
4046 let entry = entry.unwrap();
4047 assert!(
4048 entry.symbols.contains(&"Symbol".to_string()),
4049 "Expected symbols=[\"Symbol\"], got: {:?}",
4050 entry.symbols
4051 );
4052 }
4053
4054 #[test]
4058 fn rs_l2_self_barrel_e2e_resolves_through_self_barrel() {
4059 let tmp = tempfile::tempdir().unwrap();
4064 let src_fs_dir = tmp.path().join("src").join("fs");
4065 let tests_dir = tmp.path().join("tests");
4066 std::fs::create_dir_all(&src_fs_dir).unwrap();
4067 std::fs::create_dir_all(&tests_dir).unwrap();
4068
4069 std::fs::write(
4070 tmp.path().join("Cargo.toml"),
4071 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
4072 )
4073 .unwrap();
4074
4075 let mod_rs = src_fs_dir.join("mod.rs");
4076 std::fs::write(&mod_rs, "pub use self::file::File;\n").unwrap();
4077
4078 let file_rs = src_fs_dir.join("file.rs");
4079 std::fs::write(&file_rs, "pub struct File;\n").unwrap();
4080
4081 let test_fs_rs = tests_dir.join("test_fs.rs");
4082 let test_source = "use my_crate::fs::File;\n\n#[test]\nfn test_fs() {}\n";
4083 std::fs::write(&test_fs_rs, test_source).unwrap();
4084
4085 let extractor = RustExtractor::new();
4086 let file_path = file_rs.to_string_lossy().into_owned();
4087 let test_path = test_fs_rs.to_string_lossy().into_owned();
4088 let production_files = vec![file_path.clone()];
4089 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
4090 .into_iter()
4091 .collect();
4092
4093 let result = extractor.map_test_files_with_imports(
4095 &production_files,
4096 &test_sources,
4097 tmp.path(),
4098 false,
4099 );
4100
4101 let mapping = result.iter().find(|m| m.production_file == file_path);
4103 assert!(mapping.is_some(), "No mapping for src/fs/file.rs");
4104 let mapping = mapping.unwrap();
4105 assert!(
4106 mapping.test_files.contains(&test_path),
4107 "Expected test_fs.rs to map to file.rs through self:: barrel (L2), got: {:?}",
4108 mapping.test_files
4109 );
4110 }
4111
4112 #[test]
4116 fn rs_l2_single_seg_e2e_resolves_single_segment_module() {
4117 let tmp = tempfile::tempdir().unwrap();
4122 let src_fs_dir = tmp.path().join("src").join("fs");
4123 let tests_dir = tmp.path().join("tests");
4124 std::fs::create_dir_all(&src_fs_dir).unwrap();
4125 std::fs::create_dir_all(&tests_dir).unwrap();
4126
4127 std::fs::write(
4128 tmp.path().join("Cargo.toml"),
4129 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
4130 )
4131 .unwrap();
4132
4133 let mod_rs = src_fs_dir.join("mod.rs");
4134 std::fs::write(&mod_rs, "pub mod copy;\n").unwrap();
4135
4136 let copy_rs = src_fs_dir.join("copy.rs");
4137 std::fs::write(©_rs, "pub fn copy_file() {}\n").unwrap();
4138
4139 let test_fs_rs = tests_dir.join("test_fs.rs");
4140 let test_source = "use my_crate::fs;\n\n#[test]\nfn test_fs() {}\n";
4141 std::fs::write(&test_fs_rs, test_source).unwrap();
4142
4143 let extractor = RustExtractor::new();
4144 let mod_path = mod_rs.to_string_lossy().into_owned();
4145 let copy_path = copy_rs.to_string_lossy().into_owned();
4146 let test_path = test_fs_rs.to_string_lossy().into_owned();
4147 let production_files = vec![mod_path.clone(), copy_path.clone()];
4148 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
4149 .into_iter()
4150 .collect();
4151
4152 let result = extractor.map_test_files_with_imports(
4154 &production_files,
4155 &test_sources,
4156 tmp.path(),
4157 false,
4158 );
4159
4160 let mod_mapping = result.iter().find(|m| m.production_file == mod_path);
4163 let copy_mapping = result.iter().find(|m| m.production_file == copy_path);
4164 let mod_mapped = mod_mapping
4165 .map(|m| m.test_files.contains(&test_path))
4166 .unwrap_or(false);
4167 let copy_mapped = copy_mapping
4168 .map(|m| m.test_files.contains(&test_path))
4169 .unwrap_or(false);
4170 assert!(
4171 mod_mapped || copy_mapped,
4172 "Expected test_fs.rs to map to src/fs/mod.rs or src/fs/copy.rs via single-segment L2, \
4173 mod_mapping: {:?}, copy_mapping: {:?}",
4174 mod_mapping.map(|m| &m.test_files),
4175 copy_mapping.map(|m| &m.test_files)
4176 );
4177 }
4178
4179 #[test]
4183 fn rs_export_cfg_01_finds_pub_struct_inside_cfg_macro() {
4184 use std::io::Write;
4185
4186 let mut tmp = tempfile::NamedTempFile::new().unwrap();
4188 write!(
4189 tmp,
4190 "cfg_net! {{ pub struct TcpListener {{ field: u32 }} }}\n"
4191 )
4192 .unwrap();
4193 let path = tmp.path();
4194
4195 let extractor = RustExtractor::new();
4197 let symbols = vec!["TcpListener".to_string()];
4198 let result = extractor.file_exports_any_symbol(path, &symbols);
4199
4200 assert!(
4202 result,
4203 "Expected file_exports_any_symbol to return true for pub struct inside cfg macro, got false"
4204 );
4205 }
4206
4207 #[test]
4211 fn rs_export_cfg_02_returns_false_for_missing_symbol() {
4212 use std::io::Write;
4213
4214 let mut tmp = tempfile::NamedTempFile::new().unwrap();
4216 write!(
4217 tmp,
4218 "cfg_net! {{ pub struct TcpListener {{ field: u32 }} }}\n"
4219 )
4220 .unwrap();
4221 let path = tmp.path();
4222
4223 let extractor = RustExtractor::new();
4225 let symbols = vec!["NotHere".to_string()];
4226 let result = extractor.file_exports_any_symbol(path, &symbols);
4227
4228 assert!(
4230 !result,
4231 "Expected file_exports_any_symbol to return false for symbol not in file, got true"
4232 );
4233 }
4234
4235 #[test]
4239 fn rs_export_cfg_03_does_not_match_pub_crate() {
4240 use std::io::Write;
4241
4242 let mut tmp = tempfile::NamedTempFile::new().unwrap();
4244 write!(
4245 tmp,
4246 "cfg_net! {{ pub(crate) struct Internal {{ field: u32 }} }}\n"
4247 )
4248 .unwrap();
4249 let path = tmp.path();
4250
4251 let extractor = RustExtractor::new();
4253 let symbols = vec!["Internal".to_string()];
4254 let result = extractor.file_exports_any_symbol(path, &symbols);
4255
4256 assert!(
4258 !result,
4259 "Expected file_exports_any_symbol to return false for pub(crate) struct, got true"
4260 );
4261 }
4262
4263 #[test]
4267 fn rs_multiline_use_01_joins_multiline_pub_use() {
4268 let text = " pub use util::{\n AsyncReadExt,\n AsyncWriteExt,\n };\n";
4270
4271 let result = join_multiline_pub_use(text);
4273
4274 assert!(
4276 result.contains("pub use util::{"),
4277 "Expected result to contain 'pub use util::{{', got: {:?}",
4278 result
4279 );
4280 assert!(
4281 result.contains("AsyncReadExt"),
4282 "Expected result to contain 'AsyncReadExt', got: {:?}",
4283 result
4284 );
4285 assert!(
4286 result.contains('}'),
4287 "Expected result to contain '}}', got: {:?}",
4288 result
4289 );
4290 let joined_line = result.lines().find(|l| l.contains("pub use util::"));
4292 assert!(
4293 joined_line.is_some(),
4294 "Expected a single line containing 'pub use util::', got: {:?}",
4295 result
4296 );
4297 let joined_line = joined_line.unwrap();
4298 assert!(
4299 joined_line.contains("AsyncReadExt") && joined_line.contains('}'),
4300 "Expected joined line to contain both 'AsyncReadExt' and '}}', got: {:?}",
4301 joined_line
4302 );
4303 }
4304
4305 #[test]
4309 fn rs_multiline_use_02_extract_re_exports_parses_multiline_cfg_pub_use() {
4310 let source =
4312 "cfg_io! {\n pub use util::{\n Copy,\n AsyncReadExt,\n };\n}\n";
4313
4314 let extractor = RustExtractor::new();
4316 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
4317
4318 let entry = result.iter().find(|e| e.from_specifier == "./util");
4320 assert!(
4321 entry.is_some(),
4322 "Expected entry with from_specifier='./util', got: {:?}",
4323 result
4324 );
4325 let entry = entry.unwrap();
4326 assert!(
4327 entry.symbols.contains(&"Copy".to_string()),
4328 "Expected 'Copy' in symbols, got: {:?}",
4329 entry.symbols
4330 );
4331 assert!(
4332 entry.symbols.contains(&"AsyncReadExt".to_string()),
4333 "Expected 'AsyncReadExt' in symbols, got: {:?}",
4334 entry.symbols
4335 );
4336 }
4337
4338 #[test]
4342 fn rs_l2_cfg_export_e2e_resolves_through_cfg_wrapped_production_file() {
4343 let tmp = tempfile::tempdir().unwrap();
4349 let src_net_dir = tmp.path().join("src").join("net");
4350 let src_tcp_dir = src_net_dir.join("tcp");
4351 let src_listener_dir = src_tcp_dir.join("listener");
4352 let tests_dir = tmp.path().join("tests");
4353 std::fs::create_dir_all(&src_net_dir).unwrap();
4354 std::fs::create_dir_all(&src_tcp_dir).unwrap();
4355 std::fs::create_dir_all(&src_listener_dir).unwrap();
4356 std::fs::create_dir_all(&tests_dir).unwrap();
4357
4358 std::fs::write(
4359 tmp.path().join("Cargo.toml"),
4360 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
4361 )
4362 .unwrap();
4363
4364 let net_mod_rs = src_net_dir.join("mod.rs");
4365 std::fs::write(
4366 &net_mod_rs,
4367 "cfg_net! { pub mod tcp; pub use tcp::listener::TcpListener; }\n",
4368 )
4369 .unwrap();
4370
4371 let tcp_mod_rs = src_tcp_dir.join("mod.rs");
4372 std::fs::write(&tcp_mod_rs, "pub mod listener;\n").unwrap();
4373
4374 let listener_rs = src_tcp_dir.join("listener.rs");
4375 std::fs::write(&listener_rs, "cfg_net! { pub struct TcpListener; }\n").unwrap();
4376
4377 let test_net_rs = tests_dir.join("test_net.rs");
4378 let test_source = "use my_crate::net::TcpListener;\n\n#[test]\nfn test_net() {}\n";
4379 std::fs::write(&test_net_rs, test_source).unwrap();
4380
4381 let extractor = RustExtractor::new();
4382 let listener_path = listener_rs.to_string_lossy().into_owned();
4383 let test_path = test_net_rs.to_string_lossy().into_owned();
4384 let production_files = vec![
4385 net_mod_rs.to_string_lossy().into_owned(),
4386 tcp_mod_rs.to_string_lossy().into_owned(),
4387 listener_path.clone(),
4388 ];
4389 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
4390 .into_iter()
4391 .collect();
4392
4393 let result = extractor.map_test_files_with_imports(
4395 &production_files,
4396 &test_sources,
4397 tmp.path(),
4398 false,
4399 );
4400
4401 let mapping = result.iter().find(|m| m.production_file == listener_path);
4403 assert!(
4404 mapping.is_some(),
4405 "No mapping found for src/net/tcp/listener.rs"
4406 );
4407 let mapping = mapping.unwrap();
4408 assert!(
4409 mapping.test_files.contains(&test_path),
4410 "Expected test_net.rs to map to listener.rs through cfg-wrapped barrel (L2), got: {:?}",
4411 mapping.test_files
4412 );
4413 }
4414
4415 #[test]
4419 fn rs_l2_cfg_multiline_e2e_resolves_through_multiline_cfg_pub_use() {
4420 let tmp = tempfile::tempdir().unwrap();
4425 let src_io_dir = tmp.path().join("src").join("io");
4426 let tests_dir = tmp.path().join("tests");
4427 std::fs::create_dir_all(&src_io_dir).unwrap();
4428 std::fs::create_dir_all(&tests_dir).unwrap();
4429
4430 std::fs::write(
4431 tmp.path().join("Cargo.toml"),
4432 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
4433 )
4434 .unwrap();
4435
4436 let io_mod_rs = src_io_dir.join("mod.rs");
4437 std::fs::write(
4438 &io_mod_rs,
4439 "cfg_io! {\n pub use util::{\n AsyncReadExt,\n Copy,\n };\n}\n",
4440 )
4441 .unwrap();
4442
4443 let util_rs = src_io_dir.join("util.rs");
4444 std::fs::write(&util_rs, "pub trait AsyncReadExt {}\npub fn Copy() {}\n").unwrap();
4445
4446 let test_io_rs = tests_dir.join("test_io.rs");
4447 let test_source = "use my_crate::io::AsyncReadExt;\n\n#[test]\nfn test_io() {}\n";
4448 std::fs::write(&test_io_rs, test_source).unwrap();
4449
4450 let extractor = RustExtractor::new();
4451 let util_path = util_rs.to_string_lossy().into_owned();
4452 let test_path = test_io_rs.to_string_lossy().into_owned();
4453 let production_files = vec![io_mod_rs.to_string_lossy().into_owned(), util_path.clone()];
4454 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
4455 .into_iter()
4456 .collect();
4457
4458 let result = extractor.map_test_files_with_imports(
4460 &production_files,
4461 &test_sources,
4462 tmp.path(),
4463 false,
4464 );
4465
4466 let mapping = result.iter().find(|m| m.production_file == util_path);
4468 assert!(mapping.is_some(), "No mapping found for src/io/util.rs");
4469 let mapping = mapping.unwrap();
4470 assert!(
4471 mapping.test_files.contains(&test_path),
4472 "Expected test_io.rs to map to util.rs through multi-line cfg pub use (L2), got: {:?}",
4473 mapping.test_files
4474 );
4475 }
4476
4477 #[test]
4481 fn us_01_underscore_path_sync_broadcast() {
4482 let extractor = RustExtractor::new();
4484 let production_files = vec!["src/sync/broadcast.rs".to_string()];
4485 let test_sources: HashMap<String, String> =
4486 [("tests/sync_broadcast.rs".to_string(), String::new())]
4487 .into_iter()
4488 .collect();
4489 let scan_root = PathBuf::from(".");
4490
4491 let result = extractor.map_test_files_with_imports(
4493 &production_files,
4494 &test_sources,
4495 &scan_root,
4496 false,
4497 );
4498
4499 let mapping = result
4501 .iter()
4502 .find(|m| m.production_file == "src/sync/broadcast.rs");
4503 assert!(mapping.is_some(), "No mapping for src/sync/broadcast.rs");
4504 assert!(
4505 mapping
4506 .unwrap()
4507 .test_files
4508 .contains(&"tests/sync_broadcast.rs".to_string()),
4509 "Expected tests/sync_broadcast.rs to map to src/sync/broadcast.rs via L1.5, got: {:?}",
4510 mapping.unwrap().test_files
4511 );
4512 }
4513
4514 #[test]
4518 fn us_02_underscore_path_sync_oneshot() {
4519 let extractor = RustExtractor::new();
4521 let production_files = vec!["src/sync/oneshot.rs".to_string()];
4522 let test_sources: HashMap<String, String> =
4523 [("tests/sync_oneshot.rs".to_string(), String::new())]
4524 .into_iter()
4525 .collect();
4526 let scan_root = PathBuf::from(".");
4527
4528 let result = extractor.map_test_files_with_imports(
4530 &production_files,
4531 &test_sources,
4532 &scan_root,
4533 false,
4534 );
4535
4536 let mapping = result
4538 .iter()
4539 .find(|m| m.production_file == "src/sync/oneshot.rs");
4540 assert!(mapping.is_some(), "No mapping for src/sync/oneshot.rs");
4541 assert!(
4542 mapping
4543 .unwrap()
4544 .test_files
4545 .contains(&"tests/sync_oneshot.rs".to_string()),
4546 "Expected tests/sync_oneshot.rs to map to src/sync/oneshot.rs via L1.5, got: {:?}",
4547 mapping.unwrap().test_files
4548 );
4549 }
4550
4551 #[test]
4555 fn us_03_underscore_path_task_blocking() {
4556 let extractor = RustExtractor::new();
4558 let production_files = vec!["src/task/blocking.rs".to_string()];
4559 let test_sources: HashMap<String, String> =
4560 [("tests/task_blocking.rs".to_string(), String::new())]
4561 .into_iter()
4562 .collect();
4563 let scan_root = PathBuf::from(".");
4564
4565 let result = extractor.map_test_files_with_imports(
4567 &production_files,
4568 &test_sources,
4569 &scan_root,
4570 false,
4571 );
4572
4573 let mapping = result
4575 .iter()
4576 .find(|m| m.production_file == "src/task/blocking.rs");
4577 assert!(mapping.is_some(), "No mapping for src/task/blocking.rs");
4578 assert!(
4579 mapping
4580 .unwrap()
4581 .test_files
4582 .contains(&"tests/task_blocking.rs".to_string()),
4583 "Expected tests/task_blocking.rs to map to src/task/blocking.rs via L1.5, got: {:?}",
4584 mapping.unwrap().test_files
4585 );
4586 }
4587
4588 #[test]
4592 fn us_04_underscore_path_macros_select() {
4593 let extractor = RustExtractor::new();
4595 let production_files = vec!["src/macros/select.rs".to_string()];
4596 let test_sources: HashMap<String, String> =
4597 [("tests/macros_select.rs".to_string(), String::new())]
4598 .into_iter()
4599 .collect();
4600 let scan_root = PathBuf::from(".");
4601
4602 let result = extractor.map_test_files_with_imports(
4604 &production_files,
4605 &test_sources,
4606 &scan_root,
4607 false,
4608 );
4609
4610 let mapping = result
4612 .iter()
4613 .find(|m| m.production_file == "src/macros/select.rs");
4614 assert!(mapping.is_some(), "No mapping for src/macros/select.rs");
4615 assert!(
4616 mapping
4617 .unwrap()
4618 .test_files
4619 .contains(&"tests/macros_select.rs".to_string()),
4620 "Expected tests/macros_select.rs to map to src/macros/select.rs via L1.5, got: {:?}",
4621 mapping.unwrap().test_files
4622 );
4623 }
4624
4625 #[test]
4629 fn us_05_underscore_path_no_underscore_unchanged() {
4630 let extractor = RustExtractor::new();
4634 let production_files = vec!["src/abc.rs".to_string()];
4635 let test_sources: HashMap<String, String> = [("tests/abc.rs".to_string(), String::new())]
4636 .into_iter()
4637 .collect();
4638 let scan_root = PathBuf::from(".");
4639
4640 let result = extractor.map_test_files_with_imports(
4642 &production_files,
4643 &test_sources,
4644 &scan_root,
4645 false,
4646 );
4647
4648 let mapping = result.iter().find(|m| m.production_file == "src/abc.rs");
4652 assert!(mapping.is_some(), "No mapping entry for src/abc.rs");
4653 assert!(
4655 !mapping
4656 .unwrap()
4657 .test_files
4658 .contains(&"tests/abc.rs".to_string()),
4659 "L1.5 must not match tests/abc.rs -> src/abc.rs (different dirs, no underscore): {:?}",
4660 mapping.unwrap().test_files
4661 );
4662 }
4663
4664 #[test]
4668 fn us_06_underscore_path_wrong_dir_no_match() {
4669 let extractor = RustExtractor::new();
4671 let production_files = vec!["src/runtime/broadcast.rs".to_string()];
4672 let test_sources: HashMap<String, String> =
4673 [("tests/sync_broadcast.rs".to_string(), String::new())]
4674 .into_iter()
4675 .collect();
4676 let scan_root = PathBuf::from(".");
4677
4678 let result = extractor.map_test_files_with_imports(
4680 &production_files,
4681 &test_sources,
4682 &scan_root,
4683 false,
4684 );
4685
4686 let mapping = result
4688 .iter()
4689 .find(|m| m.production_file == "src/runtime/broadcast.rs");
4690 assert!(
4691 mapping.is_some(),
4692 "No mapping entry for src/runtime/broadcast.rs"
4693 );
4694 assert!(
4695 !mapping.unwrap().test_files.contains(&"tests/sync_broadcast.rs".to_string()),
4696 "L1.5 must NOT match tests/sync_broadcast.rs -> src/runtime/broadcast.rs (wrong dir), got: {:?}",
4697 mapping.unwrap().test_files
4698 );
4699 }
4700
4701 #[test]
4705 fn us_07_underscore_path_short_suffix_no_match() {
4706 let extractor = RustExtractor::new();
4708 let production_files = vec!["src/a/b.rs".to_string()];
4709 let test_sources: HashMap<String, String> = [("tests/a_b.rs".to_string(), String::new())]
4710 .into_iter()
4711 .collect();
4712 let scan_root = PathBuf::from(".");
4713
4714 let result = extractor.map_test_files_with_imports(
4716 &production_files,
4717 &test_sources,
4718 &scan_root,
4719 false,
4720 );
4721
4722 let mapping = result.iter().find(|m| m.production_file == "src/a/b.rs");
4724 assert!(mapping.is_some(), "No mapping entry for src/a/b.rs");
4725 assert!(
4726 !mapping
4727 .unwrap()
4728 .test_files
4729 .contains(&"tests/a_b.rs".to_string()),
4730 "L1.5 must NOT match tests/a_b.rs -> src/a/b.rs (short suffix guard), got: {:?}",
4731 mapping.unwrap().test_files
4732 );
4733 }
4734
4735 #[test]
4739 fn us_08_underscore_path_already_l1_matched_skipped() {
4740 let extractor = RustExtractor::new();
4743 let production_files = vec![
4744 "src/broadcast.rs".to_string(),
4745 "src/sync/broadcast.rs".to_string(),
4746 ];
4747 let test_sources: HashMap<String, String> = [
4748 ("tests/broadcast.rs".to_string(), String::new()),
4749 ("tests/sync_broadcast.rs".to_string(), String::new()),
4750 ]
4751 .into_iter()
4752 .collect();
4753 let scan_root = PathBuf::from(".");
4754
4755 let result = extractor.map_test_files_with_imports(
4757 &production_files,
4758 &test_sources,
4759 &scan_root,
4760 false,
4761 );
4762
4763 let sync_mapping = result
4767 .iter()
4768 .find(|m| m.production_file == "src/sync/broadcast.rs");
4769 assert!(
4770 sync_mapping.is_some(),
4771 "No mapping entry for src/sync/broadcast.rs"
4772 );
4773 assert!(
4774 sync_mapping
4775 .unwrap()
4776 .test_files
4777 .contains(&"tests/sync_broadcast.rs".to_string()),
4778 "Expected tests/sync_broadcast.rs to map to src/sync/broadcast.rs via L1.5, got: {:?}",
4779 sync_mapping.unwrap().test_files
4780 );
4781 }
4782
4783 #[test]
4787 fn xc_01_cross_crate_extract_root_crate_name() {
4788 let source = "use clap::builder::Arg;\n";
4790 let crate_names = ["clap", "clap_builder"];
4791
4792 let result = extract_import_specifiers_with_crate_names(source, &crate_names);
4794
4795 assert!(
4797 !result.is_empty(),
4798 "Expected at least one import entry, got empty"
4799 );
4800 let entry = result
4801 .iter()
4802 .find(|(crate_n, spec, _)| crate_n == "clap" && spec == "builder");
4803 assert!(
4804 entry.is_some(),
4805 "Expected entry (clap, builder, [Arg]), got: {:?}",
4806 result
4807 );
4808 let (_, _, symbols) = entry.unwrap();
4809 assert!(
4810 symbols.contains(&"Arg".to_string()),
4811 "Expected symbols to contain 'Arg', got: {:?}",
4812 symbols
4813 );
4814 }
4815
4816 #[test]
4820 fn xc_02_cross_crate_extract_member_crate_name() {
4821 let source = "use clap_builder::error::ErrorKind;\n";
4824 let crate_names = ["clap", "clap_builder"];
4825
4826 let result = extract_import_specifiers_with_crate_names(source, &crate_names);
4828
4829 assert!(
4832 !result.is_empty(),
4833 "Expected at least one import entry, got empty"
4834 );
4835 let entry = result
4836 .iter()
4837 .find(|(crate_n, spec, _)| crate_n == "clap_builder" && spec == "error");
4838 assert!(
4839 entry.is_some(),
4840 "Expected entry (clap_builder, error, [ErrorKind]), got: {:?}",
4841 result
4842 );
4843 let (_, _, symbols) = entry.unwrap();
4844 assert!(
4845 symbols.contains(&"ErrorKind".to_string()),
4846 "Expected symbols to contain 'ErrorKind', got: {:?}",
4847 symbols
4848 );
4849 }
4850
4851 #[test]
4855 fn xc_03_cross_crate_skips_external() {
4856 let source = "use std::collections::HashMap;\n";
4858 let crate_names = ["clap"];
4859
4860 let result = extract_import_specifiers_with_crate_names(source, &crate_names);
4862
4863 assert!(
4865 result.is_empty(),
4866 "Expected empty result for std:: import not in crate_names, got: {:?}",
4867 result
4868 );
4869 }
4870
4871 #[test]
4875 fn xc_04_cross_crate_crate_prefix_still_works() {
4876 let source = "use crate::utils;\n";
4878 let crate_names = ["clap"];
4879
4880 let result = extract_import_specifiers_with_crate_names(source, &crate_names);
4882
4883 assert!(
4886 !result.is_empty(),
4887 "Expected at least one import entry for `use crate::utils`, got empty"
4888 );
4889 let entry = result
4890 .iter()
4891 .find(|(crate_n, spec, _)| crate_n == "crate" && spec == "utils");
4892 assert!(
4893 entry.is_some(),
4894 "Expected entry (crate, utils, []), got: {:?}",
4895 result
4896 );
4897 }
4898
4899 #[test]
4903 fn xc_05_cross_crate_root_test_maps_to_root_src() {
4904 let tmp = tempfile::tempdir().unwrap();
4909 let src_dir = tmp.path().join("src");
4910 let tests_dir = tmp.path().join("tests");
4911 std::fs::create_dir_all(&src_dir).unwrap();
4912 std::fs::create_dir_all(&tests_dir).unwrap();
4913
4914 std::fs::write(
4915 tmp.path().join("Cargo.toml"),
4916 "[package]\nname = \"my_crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
4917 )
4918 .unwrap();
4919
4920 let builder_rs = src_dir.join("builder.rs");
4921 std::fs::write(&builder_rs, "pub struct Command;\n").unwrap();
4922
4923 let test_builder_rs = tests_dir.join("test_builder.rs");
4924 let test_source = "use my_crate::builder::Command;\n\n#[test]\nfn test_builder() {}\n";
4925 std::fs::write(&test_builder_rs, test_source).unwrap();
4926
4927 let extractor = RustExtractor::new();
4928 let prod_path = builder_rs.to_string_lossy().into_owned();
4929 let test_path = test_builder_rs.to_string_lossy().into_owned();
4930 let production_files = vec![prod_path.clone()];
4931 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
4932 .into_iter()
4933 .collect();
4934
4935 let result = extractor.map_test_files_with_imports(
4937 &production_files,
4938 &test_sources,
4939 tmp.path(),
4940 false,
4941 );
4942
4943 let mapping = result.iter().find(|m| m.production_file == prod_path);
4945 assert!(mapping.is_some(), "No mapping found for src/builder.rs");
4946 assert!(
4947 mapping.unwrap().test_files.contains(&test_path),
4948 "Expected test_builder.rs to map to builder.rs via cross-crate L2, got: {:?}",
4949 mapping.unwrap().test_files
4950 );
4951 }
4952
4953 #[test]
4957 fn xc_06_cross_crate_root_test_maps_to_member() {
4958 let tmp = tempfile::tempdir().unwrap();
4965 let member_dir = tmp.path().join("member_a");
4966 let member_src = member_dir.join("src");
4967 let tests_dir = tmp.path().join("tests");
4968 std::fs::create_dir_all(&member_src).unwrap();
4969 std::fs::create_dir_all(&tests_dir).unwrap();
4970
4971 std::fs::write(
4973 tmp.path().join("Cargo.toml"),
4974 "[workspace]\nmembers = [\"member_a\"]\n",
4975 )
4976 .unwrap();
4977
4978 std::fs::write(
4980 member_dir.join("Cargo.toml"),
4981 "[package]\nname = \"member_a\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
4982 )
4983 .unwrap();
4984
4985 let builder_rs = member_src.join("builder.rs");
4986 std::fs::write(&builder_rs, "pub struct Cmd;\n").unwrap();
4987
4988 let test_builder_rs = tests_dir.join("test_builder.rs");
4989 let test_source = "use member_a::builder::Cmd;\n\n#[test]\nfn test_builder() {}\n";
4990 std::fs::write(&test_builder_rs, test_source).unwrap();
4991
4992 let extractor = RustExtractor::new();
4993 let prod_path = builder_rs.to_string_lossy().into_owned();
4994 let test_path = test_builder_rs.to_string_lossy().into_owned();
4995 let production_files = vec![prod_path.clone()];
4996 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
4997 .into_iter()
4998 .collect();
4999
5000 let result = extractor.map_test_files_with_imports(
5002 &production_files,
5003 &test_sources,
5004 tmp.path(),
5005 false,
5006 );
5007
5008 let mapping = result.iter().find(|m| m.production_file == prod_path);
5010 assert!(
5011 mapping.is_some(),
5012 "No mapping found for member_a/src/builder.rs"
5013 );
5014 assert!(
5015 mapping.unwrap().test_files.contains(&test_path),
5016 "Expected root test_builder.rs to map to member_a/src/builder.rs via cross-crate L2, got: {:?}",
5017 mapping.unwrap().test_files
5018 );
5019 }
5020
5021 #[test]
5030 fn xc_07_cross_crate_member_test_not_affected() {
5031 let tmp = tempfile::tempdir().unwrap();
5038 let member_dir = tmp.path().join("member_a");
5039 let member_src = member_dir.join("src");
5040 let member_tests = member_dir.join("tests");
5041 std::fs::create_dir_all(&member_src).unwrap();
5042 std::fs::create_dir_all(&member_tests).unwrap();
5043
5044 std::fs::write(
5046 tmp.path().join("Cargo.toml"),
5047 "[workspace]\nmembers = [\"member_a\"]\n",
5048 )
5049 .unwrap();
5050
5051 std::fs::write(
5053 member_dir.join("Cargo.toml"),
5054 "[package]\nname = \"member_a\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
5055 )
5056 .unwrap();
5057
5058 let engine_rs = member_src.join("engine.rs");
5059 std::fs::write(&engine_rs, "pub struct Engine;\n").unwrap();
5060
5061 let test_rs = member_tests.join("test_engine.rs");
5062 let test_source = "use member_a::engine::Engine;\n\n#[test]\nfn test_engine() {}\n";
5063 std::fs::write(&test_rs, test_source).unwrap();
5064
5065 let extractor = RustExtractor::new();
5066 let prod_path = engine_rs.to_string_lossy().into_owned();
5067 let test_path = test_rs.to_string_lossy().into_owned();
5068 let production_files = vec![prod_path.clone()];
5069 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
5070 .into_iter()
5071 .collect();
5072
5073 let result = extractor.map_test_files_with_imports(
5076 &production_files,
5077 &test_sources,
5078 tmp.path(),
5079 false,
5080 );
5081
5082 let mapping = result.iter().find(|m| m.production_file == prod_path);
5086 assert!(
5087 mapping.is_some(),
5088 "No mapping found for member_a/src/engine.rs"
5089 );
5090 assert!(
5091 mapping.unwrap().test_files.contains(&test_path),
5092 "Expected member_a/tests/test_engine.rs to map to member_a/src/engine.rs via per-member L2, got: {:?}",
5093 mapping.unwrap().test_files
5094 );
5095 }
5096
5097 #[test]
5101 fn sd_01_subdir_stem_match_same_crate() {
5102 let extractor = RustExtractor::new();
5104 let production_files = vec!["src/builder/action.rs".to_string()];
5105 let test_sources: HashMap<String, String> =
5106 [("tests/builder/action.rs".to_string(), String::new())]
5107 .into_iter()
5108 .collect();
5109 let scan_root = PathBuf::from(".");
5110
5111 let result = extractor.map_test_files_with_imports(
5113 &production_files,
5114 &test_sources,
5115 &scan_root,
5116 false,
5117 );
5118
5119 let mapping = result
5121 .iter()
5122 .find(|m| m.production_file == "src/builder/action.rs");
5123 assert!(mapping.is_some(), "No mapping for src/builder/action.rs");
5124 assert!(
5125 mapping
5126 .unwrap()
5127 .test_files
5128 .contains(&"tests/builder/action.rs".to_string()),
5129 "Expected tests/builder/action.rs to map to src/builder/action.rs via L1.6 subdir matching, got: {:?}",
5130 mapping.unwrap().test_files
5131 );
5132 }
5133
5134 #[test]
5138 fn sd_02_subdir_stem_match_cross_crate() {
5139 let extractor = RustExtractor::new();
5141 let production_files = vec!["member_a/src/builder/command.rs".to_string()];
5142 let test_sources: HashMap<String, String> =
5143 [("tests/builder/command.rs".to_string(), String::new())]
5144 .into_iter()
5145 .collect();
5146 let scan_root = PathBuf::from(".");
5147
5148 let result = extractor.map_test_files_with_imports(
5150 &production_files,
5151 &test_sources,
5152 &scan_root,
5153 false,
5154 );
5155
5156 let mapping = result
5158 .iter()
5159 .find(|m| m.production_file == "member_a/src/builder/command.rs");
5160 assert!(
5161 mapping.is_some(),
5162 "No mapping for member_a/src/builder/command.rs"
5163 );
5164 assert!(
5165 mapping
5166 .unwrap()
5167 .test_files
5168 .contains(&"tests/builder/command.rs".to_string()),
5169 "Expected tests/builder/command.rs to map to member_a/src/builder/command.rs via L1.6 subdir matching, got: {:?}",
5170 mapping.unwrap().test_files
5171 );
5172 }
5173
5174 #[test]
5178 fn sd_03_subdir_wrong_dir_no_match() {
5179 let extractor = RustExtractor::new();
5181 let production_files = vec!["src/parser/action.rs".to_string()];
5182 let test_sources: HashMap<String, String> =
5183 [("tests/builder/action.rs".to_string(), String::new())]
5184 .into_iter()
5185 .collect();
5186 let scan_root = PathBuf::from(".");
5187
5188 let result = extractor.map_test_files_with_imports(
5190 &production_files,
5191 &test_sources,
5192 &scan_root,
5193 false,
5194 );
5195
5196 let mapping = result
5198 .iter()
5199 .find(|m| m.production_file == "src/parser/action.rs");
5200 assert!(
5201 mapping.is_some(),
5202 "No mapping entry for src/parser/action.rs"
5203 );
5204 assert!(
5205 mapping.unwrap().test_files.is_empty(),
5206 "Expected NO match for src/parser/action.rs (wrong dir), got: {:?}",
5207 mapping.unwrap().test_files
5208 );
5209 }
5210
5211 #[test]
5215 fn sd_04_subdir_no_subdir_skip() {
5216 let extractor = RustExtractor::new();
5218 let production_files = vec!["src/builder/action.rs".to_string()];
5219 let test_sources: HashMap<String, String> =
5220 [("tests/action.rs".to_string(), String::new())]
5221 .into_iter()
5222 .collect();
5223 let scan_root = PathBuf::from(".");
5224
5225 let result = extractor.map_test_files_with_imports(
5227 &production_files,
5228 &test_sources,
5229 &scan_root,
5230 false,
5231 );
5232
5233 let mapping = result
5235 .iter()
5236 .find(|m| m.production_file == "src/builder/action.rs");
5237 assert!(
5238 mapping.is_some(),
5239 "No mapping entry for src/builder/action.rs"
5240 );
5241 assert!(
5242 mapping.unwrap().test_files.is_empty(),
5243 "Expected NO match for src/builder/action.rs (no test subdir), got: {:?}",
5244 mapping.unwrap().test_files
5245 );
5246 }
5247
5248 #[test]
5252 fn sd_05_subdir_main_rs_skip() {
5253 let extractor = RustExtractor::new();
5256 let production_files = vec!["src/builder/action.rs".to_string()];
5257 let test_sources: HashMap<String, String> =
5258 [("tests/builder/main.rs".to_string(), String::new())]
5259 .into_iter()
5260 .collect();
5261 let scan_root = PathBuf::from(".");
5262
5263 let result = extractor.map_test_files_with_imports(
5265 &production_files,
5266 &test_sources,
5267 &scan_root,
5268 false,
5269 );
5270
5271 let mapping = result
5273 .iter()
5274 .find(|m| m.production_file == "src/builder/action.rs");
5275 assert!(
5276 mapping.is_some(),
5277 "No mapping entry for src/builder/action.rs"
5278 );
5279 assert!(
5280 mapping.unwrap().test_files.is_empty(),
5281 "Expected NO match for src/builder/action.rs (main.rs skipped), got: {:?}",
5282 mapping.unwrap().test_files
5283 );
5284 }
5285
5286 #[test]
5293 fn sd_06_subdir_already_matched_skip() {
5294 let extractor = RustExtractor::new();
5298 let production_files = vec![
5299 "src/sync/action.rs".to_string(),
5300 "src/builder/command.rs".to_string(),
5301 ];
5302 let test_sources: HashMap<String, String> = [
5303 ("tests/sync_action.rs".to_string(), String::new()),
5304 ("tests/builder/command.rs".to_string(), String::new()),
5305 ]
5306 .into_iter()
5307 .collect();
5308 let scan_root = PathBuf::from(".");
5309
5310 let result = extractor.map_test_files_with_imports(
5312 &production_files,
5313 &test_sources,
5314 &scan_root,
5315 false,
5316 );
5317
5318 let l15_mapping = result
5320 .iter()
5321 .find(|m| m.production_file == "src/sync/action.rs");
5322 assert!(
5323 l15_mapping.is_some(),
5324 "No mapping entry for src/sync/action.rs"
5325 );
5326 assert!(
5327 l15_mapping
5328 .unwrap()
5329 .test_files
5330 .contains(&"tests/sync_action.rs".to_string()),
5331 "Expected tests/sync_action.rs to L1.5-match to src/sync/action.rs, got: {:?}",
5332 l15_mapping.unwrap().test_files
5333 );
5334
5335 let sd_mapping = result
5338 .iter()
5339 .find(|m| m.production_file == "src/builder/command.rs");
5340 assert!(
5341 sd_mapping.is_some(),
5342 "No mapping entry for src/builder/command.rs"
5343 );
5344 assert!(
5345 sd_mapping
5346 .unwrap()
5347 .test_files
5348 .contains(&"tests/builder/command.rs".to_string()),
5349 "Expected tests/builder/command.rs to L1.6-match to src/builder/command.rs, got: {:?}",
5350 sd_mapping.unwrap().test_files
5351 );
5352 }
5353
5354 #[test]
5364 fn ccb_01_wildcard_cross_crate_barrel_maps_test_to_member() {
5365 let tmp = tempfile::tempdir().unwrap();
5374
5375 std::fs::write(
5377 tmp.path().join("Cargo.toml"),
5378 "[workspace]\nmembers = [\"sibling_crate\"]\n\n[package]\nname = \"root_crate\"\nversion = \"0.1.0\"\n",
5379 )
5380 .unwrap();
5381
5382 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
5384 let root_lib = tmp.path().join("src").join("lib.rs");
5385 std::fs::write(&root_lib, "pub use sibling_crate::*;\n").unwrap();
5386
5387 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
5389 let test_file = tmp.path().join("tests").join("test_symbol.rs");
5390 std::fs::write(
5391 &test_file,
5392 "use root_crate::Symbol;\n#[test]\nfn test_symbol() { let _s = Symbol {}; }\n",
5393 )
5394 .unwrap();
5395
5396 let sib_dir = tmp.path().join("sibling_crate");
5398 std::fs::create_dir_all(sib_dir.join("src")).unwrap();
5399 std::fs::write(
5400 sib_dir.join("Cargo.toml"),
5401 "[package]\nname = \"sibling_crate\"\nversion = \"0.1.0\"\n",
5402 )
5403 .unwrap();
5404
5405 let sib_lib = sib_dir.join("src").join("lib.rs");
5407 std::fs::write(&sib_lib, "pub struct Symbol {}\n").unwrap();
5408
5409 let extractor = RustExtractor::new();
5410 let sib_lib_path = sib_lib.to_string_lossy().into_owned();
5411 let test_file_path = test_file.to_string_lossy().into_owned();
5412
5413 let production_files = vec![sib_lib_path.clone()];
5414 let test_sources: HashMap<String, String> = [(
5415 test_file_path.clone(),
5416 std::fs::read_to_string(&test_file).unwrap(),
5417 )]
5418 .into_iter()
5419 .collect();
5420
5421 let result = extractor.map_test_files_with_imports(
5423 &production_files,
5424 &test_sources,
5425 tmp.path(),
5426 false,
5427 );
5428
5429 let mapping = result.iter().find(|m| m.production_file == sib_lib_path);
5431 assert!(
5432 mapping.is_some(),
5433 "No mapping entry for sibling_crate/src/lib.rs. All mappings: {:#?}",
5434 result
5435 );
5436 assert!(
5437 mapping.unwrap().test_files.contains(&test_file_path),
5438 "Expected test_symbol.rs mapped to sibling_crate/src/lib.rs via CCB, \
5439 but test_files: {:?}",
5440 mapping.unwrap().test_files
5441 );
5442 }
5443
5444 #[test]
5452 fn ccb_02_named_cross_crate_reexport_maps_test_to_member() {
5453 let tmp = tempfile::tempdir().unwrap();
5454
5455 std::fs::write(
5456 tmp.path().join("Cargo.toml"),
5457 "[workspace]\nmembers = [\"sibling_crate\"]\n\n[package]\nname = \"root_crate\"\nversion = \"0.1.0\"\n",
5458 )
5459 .unwrap();
5460
5461 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
5462 let root_lib = tmp.path().join("src").join("lib.rs");
5463 std::fs::write(&root_lib, "pub use sibling_crate::SpecificType;\n").unwrap();
5465
5466 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
5467 let test_file = tmp.path().join("tests").join("test_specific.rs");
5468 std::fs::write(
5469 &test_file,
5470 "use root_crate::SpecificType;\n#[test]\nfn test_it() { let _x: SpecificType = todo!(); }\n",
5471 )
5472 .unwrap();
5473
5474 let sib_dir = tmp.path().join("sibling_crate");
5475 std::fs::create_dir_all(sib_dir.join("src")).unwrap();
5476 std::fs::write(
5477 sib_dir.join("Cargo.toml"),
5478 "[package]\nname = \"sibling_crate\"\nversion = \"0.1.0\"\n",
5479 )
5480 .unwrap();
5481 let sib_lib = sib_dir.join("src").join("lib.rs");
5482 std::fs::write(&sib_lib, "pub struct SpecificType {}\n").unwrap();
5483
5484 let extractor = RustExtractor::new();
5485 let sib_lib_path = sib_lib.to_string_lossy().into_owned();
5486 let test_file_path = test_file.to_string_lossy().into_owned();
5487
5488 let production_files = vec![sib_lib_path.clone()];
5489 let test_sources: HashMap<String, String> = [(
5490 test_file_path.clone(),
5491 std::fs::read_to_string(&test_file).unwrap(),
5492 )]
5493 .into_iter()
5494 .collect();
5495
5496 let result = extractor.map_test_files_with_imports(
5498 &production_files,
5499 &test_sources,
5500 tmp.path(),
5501 false,
5502 );
5503
5504 let mapping = result.iter().find(|m| m.production_file == sib_lib_path);
5506 assert!(
5507 mapping.is_some(),
5508 "No mapping entry for sibling_crate/src/lib.rs. All mappings: {:#?}",
5509 result
5510 );
5511 assert!(
5512 mapping.unwrap().test_files.contains(&test_file_path),
5513 "Expected test_specific.rs mapped via named CCB re-export, \
5514 but test_files: {:?}",
5515 mapping.unwrap().test_files
5516 );
5517 }
5518
5519 #[test]
5528 fn ccb_03_nonexistent_member_produces_empty_mapping_no_panic() {
5529 let tmp = tempfile::tempdir().unwrap();
5530
5531 std::fs::write(
5533 tmp.path().join("Cargo.toml"),
5534 "[workspace]\nmembers = []\n\n[package]\nname = \"root_crate\"\nversion = \"0.1.0\"\n",
5535 )
5536 .unwrap();
5537
5538 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
5539 let root_lib = tmp.path().join("src").join("lib.rs");
5540 std::fs::write(&root_lib, "pub use nonexistent_crate::*;\n").unwrap();
5541
5542 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
5543 let test_file = tmp.path().join("tests").join("test_ghost.rs");
5544 std::fs::write(
5545 &test_file,
5546 "use root_crate::Ghost;\n#[test]\nfn test_ghost() {}\n",
5547 )
5548 .unwrap();
5549
5550 let extractor = RustExtractor::new();
5551 let root_lib_path = root_lib.to_string_lossy().into_owned();
5553 let test_file_path = test_file.to_string_lossy().into_owned();
5554
5555 let production_files = vec![root_lib_path.clone()];
5556 let test_sources: HashMap<String, String> = [(
5557 test_file_path.clone(),
5558 std::fs::read_to_string(&test_file).unwrap(),
5559 )]
5560 .into_iter()
5561 .collect();
5562
5563 let result = extractor.map_test_files_with_imports(
5565 &production_files,
5566 &test_sources,
5567 tmp.path(),
5568 false,
5569 );
5570
5571 let ghost_mapped = result
5574 .iter()
5575 .any(|m| m.test_files.contains(&test_file_path));
5576 assert!(
5577 !ghost_mapped,
5578 "test_ghost.rs should not be mapped (nonexistent_crate is not a member), \
5579 but found in mappings: {:#?}",
5580 result
5581 );
5582 }
5583
5584 #[test]
5592 fn ccb_04_local_pubmod_still_resolved_by_existing_l2() {
5593 let tmp = tempfile::tempdir().unwrap();
5594
5595 std::fs::write(
5596 tmp.path().join("Cargo.toml"),
5597 "[workspace]\nmembers = [\"sibling\"]\n\n[package]\nname = \"root_crate\"\nversion = \"0.1.0\"\n",
5598 )
5599 .unwrap();
5600
5601 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
5602
5603 let root_lib = tmp.path().join("src").join("lib.rs");
5605 std::fs::write(&root_lib, "pub mod local_module;\npub use sibling::*;\n").unwrap();
5606
5607 let local_mod = tmp.path().join("src").join("local_module.rs");
5609 std::fs::write(&local_mod, "pub fn local_fn() {}\n").unwrap();
5610
5611 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
5613 let test_file = tmp.path().join("tests").join("test_local.rs");
5614 std::fs::write(
5615 &test_file,
5616 "use root_crate::local_module::local_fn;\n#[test]\nfn test_local_fn() { local_fn(); }\n",
5617 )
5618 .unwrap();
5619
5620 let sib_dir = tmp.path().join("sibling");
5622 std::fs::create_dir_all(sib_dir.join("src")).unwrap();
5623 std::fs::write(
5624 sib_dir.join("Cargo.toml"),
5625 "[package]\nname = \"sibling\"\nversion = \"0.1.0\"\n",
5626 )
5627 .unwrap();
5628 std::fs::write(sib_dir.join("src").join("lib.rs"), "pub fn sib_fn() {}\n").unwrap();
5629
5630 let extractor = RustExtractor::new();
5631 let local_mod_path = local_mod.to_string_lossy().into_owned();
5632 let test_file_path = test_file.to_string_lossy().into_owned();
5633
5634 let production_files = vec![local_mod_path.clone()];
5635 let test_sources: HashMap<String, String> = [(
5636 test_file_path.clone(),
5637 std::fs::read_to_string(&test_file).unwrap(),
5638 )]
5639 .into_iter()
5640 .collect();
5641
5642 let result = extractor.map_test_files_with_imports(
5644 &production_files,
5645 &test_sources,
5646 tmp.path(),
5647 false,
5648 );
5649
5650 let mapping = result.iter().find(|m| m.production_file == local_mod_path);
5652 assert!(
5653 mapping.is_some(),
5654 "No mapping entry for local_module.rs. All mappings: {:#?}",
5655 result
5656 );
5657 assert!(
5658 mapping.unwrap().test_files.contains(&test_file_path),
5659 "Expected test_local.rs mapped to local_module.rs via existing L2, \
5660 but test_files: {:?}",
5661 mapping.unwrap().test_files
5662 );
5663 }
5664
5665 #[test]
5680 fn ccb_05_two_level_cross_crate_barrel_chain() {
5681 let tmp = tempfile::tempdir().unwrap();
5682
5683 std::fs::write(
5685 tmp.path().join("Cargo.toml"),
5686 "[workspace]\nmembers = [\"mid\"]\n\n[package]\nname = \"root_crate\"\nversion = \"0.1.0\"\n",
5687 )
5688 .unwrap();
5689
5690 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
5691 let root_lib = tmp.path().join("src").join("lib.rs");
5692 std::fs::write(&root_lib, "pub use mid::*;\n").unwrap();
5694
5695 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
5697 let test_file = tmp.path().join("tests").join("test_mid_item.rs");
5698 std::fs::write(
5699 &test_file,
5700 "use root_crate::MidItem;\n#[test]\nfn test_mid_item() { let _ = MidItem {}; }\n",
5701 )
5702 .unwrap();
5703
5704 let mid_dir = tmp.path().join("mid");
5706 std::fs::create_dir_all(mid_dir.join("src")).unwrap();
5707 std::fs::write(
5708 mid_dir.join("Cargo.toml"),
5709 "[package]\nname = \"mid\"\nversion = \"0.1.0\"\n",
5710 )
5711 .unwrap();
5712 std::fs::write(
5714 mid_dir.join("src").join("lib.rs"),
5715 "pub mod sub;\npub use sub::*;\n",
5716 )
5717 .unwrap();
5718
5719 let sub_rs = mid_dir.join("src").join("sub.rs");
5721 std::fs::write(&sub_rs, "pub struct MidItem {}\n").unwrap();
5722
5723 let extractor = RustExtractor::new();
5724 let sub_rs_path = sub_rs.to_string_lossy().into_owned();
5725 let test_file_path = test_file.to_string_lossy().into_owned();
5726
5727 let production_files = vec![sub_rs_path.clone()];
5728 let test_sources: HashMap<String, String> = [(
5729 test_file_path.clone(),
5730 std::fs::read_to_string(&test_file).unwrap(),
5731 )]
5732 .into_iter()
5733 .collect();
5734
5735 let result = extractor.map_test_files_with_imports(
5737 &production_files,
5738 &test_sources,
5739 tmp.path(),
5740 false,
5741 );
5742
5743 let mapping = result.iter().find(|m| m.production_file == sub_rs_path);
5745 assert!(
5746 mapping.is_some(),
5747 "No mapping entry for mid/src/sub.rs. All mappings: {:#?}",
5748 result
5749 );
5750 assert!(
5751 mapping.unwrap().test_files.contains(&test_file_path),
5752 "Expected test_mid_item.rs mapped to mid/src/sub.rs via 2-level CCB chain, \
5753 but test_files: {:?}",
5754 mapping.unwrap().test_files
5755 );
5756 }
5757
5758 #[test]
5767 fn ccb_06_wildcard_with_many_items_filters_to_single_file() {
5768 let tmp = tempfile::tempdir().unwrap();
5769
5770 std::fs::write(
5771 tmp.path().join("Cargo.toml"),
5772 "[workspace]\nmembers = [\"big_crate\"]\n\n[package]\nname = \"root_crate\"\nversion = \"0.1.0\"\n",
5773 )
5774 .unwrap();
5775
5776 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
5777 let root_lib = tmp.path().join("src").join("lib.rs");
5778 std::fs::write(&root_lib, "pub use big_crate::*;\n").unwrap();
5779
5780 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
5781 let test_file = tmp.path().join("tests").join("test_specific_fn.rs");
5782 std::fs::write(
5783 &test_file,
5784 "use root_crate::SpecificFn;\n#[test]\nfn test_specific() { SpecificFn::run(); }\n",
5785 )
5786 .unwrap();
5787
5788 let big_dir = tmp.path().join("big_crate");
5790 std::fs::create_dir_all(big_dir.join("src")).unwrap();
5791 std::fs::write(
5792 big_dir.join("Cargo.toml"),
5793 "[package]\nname = \"big_crate\"\nversion = \"0.1.0\"\n",
5794 )
5795 .unwrap();
5796
5797 let mut lib_content = String::new();
5799 for i in 0..50 {
5801 lib_content.push_str(&format!("pub struct Item{i} {{}}\n"));
5802 }
5803 std::fs::write(big_dir.join("src").join("lib.rs"), &lib_content).unwrap();
5804
5805 let specific_rs = big_dir.join("src").join("specific.rs");
5807 std::fs::write(
5808 &specific_rs,
5809 "pub struct SpecificFn;\nimpl SpecificFn { pub fn run() {} }\n",
5810 )
5811 .unwrap();
5812
5813 let extractor = RustExtractor::new();
5814 let specific_rs_path = specific_rs.to_string_lossy().into_owned();
5815 let test_file_path = test_file.to_string_lossy().into_owned();
5816
5817 let big_lib_path = big_dir
5819 .join("src")
5820 .join("lib.rs")
5821 .to_string_lossy()
5822 .into_owned();
5823 let production_files = vec![big_lib_path.clone(), specific_rs_path.clone()];
5824 let test_sources: HashMap<String, String> = [(
5825 test_file_path.clone(),
5826 std::fs::read_to_string(&test_file).unwrap(),
5827 )]
5828 .into_iter()
5829 .collect();
5830
5831 let result = extractor.map_test_files_with_imports(
5833 &production_files,
5834 &test_sources,
5835 tmp.path(),
5836 false,
5837 );
5838
5839 let specific_mapping = result
5841 .iter()
5842 .find(|m| m.production_file == specific_rs_path);
5843 assert!(
5844 specific_mapping.is_some(),
5845 "No mapping entry for big_crate/src/specific.rs. All mappings: {:#?}",
5846 result
5847 );
5848 assert!(
5849 specific_mapping
5850 .unwrap()
5851 .test_files
5852 .contains(&test_file_path),
5853 "Expected test_specific_fn.rs mapped to specific.rs via CCB + symbol filter, \
5854 but test_files: {:?}",
5855 specific_mapping.unwrap().test_files
5856 );
5857
5858 let lib_mapping = result.iter().find(|m| m.production_file == big_lib_path);
5861 if let Some(lib_m) = lib_mapping {
5862 assert!(
5863 !lib_m.test_files.contains(&test_file_path),
5864 "test_specific_fn.rs should NOT be fan-out mapped to big_crate/src/lib.rs \
5865 (which does not export SpecificFn), but found in: {:?}",
5866 lib_m.test_files
5867 );
5868 }
5869 }
5870}