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(ref attr) = attr_node {
172 let attr_text = attr.utf8_text(source_bytes).unwrap_or("");
173 if attr_text.contains("not(test)") || attr_text.contains("not( test") {
174 continue;
175 }
176 }
177 if let Some(attr) = attr_node {
179 let mut sibling = attr.next_sibling();
180 while let Some(s) = sibling {
181 if s.kind() == "mod_item" {
182 return true;
183 }
184 if s.kind() != "attribute_item" {
185 break;
186 }
187 sibling = s.next_sibling();
188 }
189 }
190 }
191 }
192
193 false
194}
195
196impl ObserveExtractor for RustExtractor {
201 fn extract_production_functions(
202 &self,
203 source: &str,
204 file_path: &str,
205 ) -> Vec<ProductionFunction> {
206 let mut parser = Self::parser();
207 let tree = match parser.parse(source, None) {
208 Some(t) => t,
209 None => return Vec::new(),
210 };
211 let source_bytes = source.as_bytes();
212 let query = cached_query(&PRODUCTION_FUNCTION_QUERY_CACHE, PRODUCTION_FUNCTION_QUERY);
213
214 let name_idx = query.capture_index_for_name("name");
215 let class_name_idx = query.capture_index_for_name("class_name");
216 let method_name_idx = query.capture_index_for_name("method_name");
217 let function_idx = query.capture_index_for_name("function");
218 let method_idx = query.capture_index_for_name("method");
219
220 let cfg_test_ranges = find_cfg_test_ranges(source);
222
223 let mut cursor = QueryCursor::new();
224 let mut matches = cursor.matches(query, tree.root_node(), source_bytes);
225 let mut result = Vec::new();
226
227 while let Some(m) = matches.next() {
228 let mut fn_name: Option<String> = None;
229 let mut class_name: Option<String> = None;
230 let mut line: usize = 1;
231 let mut is_exported = false;
232 let mut fn_start_byte: usize = 0;
233
234 for cap in m.captures {
235 let text = cap.node.utf8_text(source_bytes).unwrap_or("").to_string();
236 let node_line = cap.node.start_position().row + 1;
237
238 if name_idx == Some(cap.index) || method_name_idx == Some(cap.index) {
239 fn_name = Some(text);
240 line = node_line;
241 } else if class_name_idx == Some(cap.index) {
242 class_name = Some(text);
243 }
244
245 if function_idx == Some(cap.index) || method_idx == Some(cap.index) {
247 fn_start_byte = cap.node.start_byte();
248 is_exported = has_pub_visibility(cap.node);
249 }
250 }
251
252 if let Some(name) = fn_name {
253 if cfg_test_ranges
255 .iter()
256 .any(|(start, end)| fn_start_byte >= *start && fn_start_byte < *end)
257 {
258 continue;
259 }
260
261 result.push(ProductionFunction {
262 name,
263 file: file_path.to_string(),
264 line,
265 class_name,
266 is_exported,
267 });
268 }
269 }
270
271 let mut seen = HashSet::new();
273 result.retain(|f| seen.insert((f.name.clone(), f.class_name.clone())));
274
275 result
276 }
277
278 fn extract_imports(&self, source: &str, file_path: &str) -> Vec<ImportMapping> {
279 let all = self.extract_all_import_specifiers(source);
281 let mut result = Vec::new();
282 for (specifier, symbols) in all {
283 for sym in &symbols {
284 result.push(ImportMapping {
285 symbol_name: sym.clone(),
286 module_specifier: specifier.clone(),
287 file: file_path.to_string(),
288 line: 1,
289 symbols: symbols.clone(),
290 });
291 }
292 }
293 result
294 }
295
296 fn extract_all_import_specifiers(&self, source: &str) -> Vec<(String, Vec<String>)> {
297 extract_import_specifiers_with_crate_name(source, None)
298 }
299
300 fn extract_barrel_re_exports(&self, source: &str, file_path: &str) -> Vec<BarrelReExport> {
301 if !self.is_barrel_file(file_path) {
302 return Vec::new();
303 }
304
305 let mut parser = Self::parser();
306 let tree = match parser.parse(source, None) {
307 Some(t) => t,
308 None => return Vec::new(),
309 };
310 let source_bytes = source.as_bytes();
311 let root = tree.root_node();
312 let mut result = Vec::new();
313
314 for i in 0..root.child_count() {
315 let child = root.child(i).unwrap();
316
317 if child.kind() == "mod_item" && has_pub_visibility(child) {
319 let has_body = child.child_by_field_name("body").is_some();
321 if !has_body {
322 if let Some(name_node) = child.child_by_field_name("name") {
323 let mod_name = name_node.utf8_text(source_bytes).unwrap_or("");
324 result.push(BarrelReExport {
325 symbols: Vec::new(),
326 from_specifier: format!("./{mod_name}"),
327 wildcard: true,
328 namespace_wildcard: false,
329 });
330 }
331 }
332 }
333
334 if child.kind() == "use_declaration" && has_pub_visibility(child) {
336 if let Some(arg) = child.child_by_field_name("argument") {
337 extract_pub_use_re_exports(&arg, source_bytes, &mut result);
338 }
339 }
340
341 if child.kind() == "macro_invocation" {
343 for j in 0..child.child_count() {
344 if let Some(tt) = child.child(j) {
345 if tt.kind() == "token_tree" {
346 let tt_text = tt.utf8_text(source_bytes).unwrap_or("");
347 extract_re_exports_from_text(tt_text, &mut result);
348 }
349 }
350 }
351 }
352 }
353
354 result
355 }
356
357 fn source_extensions(&self) -> &[&str] {
358 &["rs"]
359 }
360
361 fn index_file_names(&self) -> &[&str] {
362 &["mod.rs", "lib.rs"]
363 }
364
365 fn production_stem<'a>(&self, path: &'a str) -> Option<&'a str> {
366 production_stem(path)
367 }
368
369 fn test_stem<'a>(&self, path: &'a str) -> Option<&'a str> {
370 test_stem(path)
371 }
372
373 fn is_non_sut_helper(&self, file_path: &str, is_known_production: bool) -> bool {
374 is_non_sut_helper(file_path, is_known_production)
375 }
376
377 fn file_exports_any_symbol(&self, path: &Path, symbols: &[String]) -> bool {
378 if symbols.is_empty() {
379 return true;
380 }
381 let source = match std::fs::read_to_string(path) {
384 Ok(s) => s,
385 Err(_) => return true,
386 };
387 let mut parser = Self::parser();
388 let tree = match parser.parse(&source, None) {
389 Some(t) => t,
390 None => return true,
391 };
392 let query = cached_query(&EXPORTED_SYMBOL_QUERY_CACHE, EXPORTED_SYMBOL_QUERY);
393 let symbol_idx = query
394 .capture_index_for_name("symbol_name")
395 .expect("@symbol_name capture not found in exported_symbol.scm");
396 let vis_idx = query.capture_index_for_name("vis");
397
398 let source_bytes = source.as_bytes();
399 let mut cursor = QueryCursor::new();
400 let mut matches = cursor.matches(query, tree.root_node(), source_bytes);
401 while let Some(m) = matches.next() {
402 for cap in m.captures {
403 if cap.index == symbol_idx {
404 let is_pub_only = m.captures.iter().any(|c| {
406 vis_idx == Some(c.index)
407 && c.node.utf8_text(source_bytes).unwrap_or("") == "pub"
408 });
409 if !is_pub_only {
410 continue;
411 }
412 let name = cap.node.utf8_text(source_bytes).unwrap_or("");
413 if symbols.iter().any(|s| s == name) {
414 return true;
415 }
416 }
417 }
418 }
419 for symbol in symbols {
422 for keyword in &["struct", "fn", "type", "enum", "trait", "const", "static"] {
423 let pattern = format!("pub {keyword} {symbol}");
424 if source.lines().any(|line| {
425 let trimmed = line.trim();
426 !trimmed.starts_with("//") && trimmed.contains(&pattern)
427 }) {
428 return true;
429 }
430 }
431 }
432 false
433 }
434}
435
436fn has_pub_visibility(node: tree_sitter::Node) -> bool {
442 for i in 0..node.child_count() {
443 if let Some(child) = node.child(i) {
444 if child.kind() == "visibility_modifier" {
445 return true;
446 }
447 if child.kind() != "attribute_item" && child.kind() != "visibility_modifier" {
449 break;
450 }
451 }
452 }
453 false
454}
455
456fn find_cfg_test_ranges(source: &str) -> Vec<(usize, usize)> {
461 let mut parser = RustExtractor::parser();
462 let tree = match parser.parse(source, None) {
463 Some(t) => t,
464 None => return Vec::new(),
465 };
466 let source_bytes = source.as_bytes();
467 let query = cached_query(&CFG_TEST_QUERY_CACHE, CFG_TEST_QUERY);
468
469 let attr_name_idx = query.capture_index_for_name("attr_name");
470 let cfg_arg_idx = query.capture_index_for_name("cfg_arg");
471 let cfg_test_attr_idx = query.capture_index_for_name("cfg_test_attr");
472
473 let mut cursor = QueryCursor::new();
474 let mut matches = cursor.matches(query, tree.root_node(), source_bytes);
475 let mut ranges = Vec::new();
476
477 while let Some(m) = matches.next() {
478 let mut is_cfg = false;
479 let mut is_test = false;
480 let mut attr_node = None;
481
482 for cap in m.captures {
483 let text = cap.node.utf8_text(source_bytes).unwrap_or("");
484 if attr_name_idx == Some(cap.index) && text == "cfg" {
485 is_cfg = true;
486 }
487 if cfg_arg_idx == Some(cap.index) && text == "test" {
488 is_test = true;
489 }
490 if cfg_test_attr_idx == Some(cap.index) {
491 attr_node = Some(cap.node);
492 }
493 }
494
495 if is_cfg && is_test {
496 if let Some(attr) = attr_node {
497 let mut sibling = attr.next_sibling();
499 while let Some(s) = sibling {
500 if s.kind() == "mod_item" {
501 ranges.push((s.start_byte(), s.end_byte()));
502 break;
503 }
504 sibling = s.next_sibling();
505 }
506 }
507 }
508 }
509
510 ranges
511}
512
513fn extract_use_declaration(
517 node: &tree_sitter::Node,
518 source_bytes: &[u8],
519 result: &mut HashMap<String, Vec<String>>,
520 crate_name: Option<&str>,
521) {
522 let arg = match node.child_by_field_name("argument") {
523 Some(a) => a,
524 None => return,
525 };
526 let full_text = arg.utf8_text(source_bytes).unwrap_or("");
527
528 if let Some(path_after_crate) = full_text.strip_prefix("crate::") {
530 parse_use_path(path_after_crate, result);
531 return;
532 }
533
534 if let Some(name) = crate_name {
536 let prefix = format!("{name}::");
537 if let Some(path_after_name) = full_text.strip_prefix(&prefix) {
538 parse_use_path(path_after_name, result);
539 }
540 }
541}
542
543pub fn extract_import_specifiers_with_crate_name(
547 source: &str,
548 crate_name: Option<&str>,
549) -> Vec<(String, Vec<String>)> {
550 let mut parser = RustExtractor::parser();
551 let tree = match parser.parse(source, None) {
552 Some(t) => t,
553 None => return Vec::new(),
554 };
555 let source_bytes = source.as_bytes();
556
557 let root = tree.root_node();
560 let mut result_map: HashMap<String, Vec<String>> = HashMap::new();
561
562 for i in 0..root.child_count() {
563 let child = root.child(i).unwrap();
564 if child.kind() == "use_declaration" {
565 extract_use_declaration(&child, source_bytes, &mut result_map, crate_name);
566 }
567 }
568
569 result_map.into_iter().collect()
570}
571
572pub fn extract_import_specifiers_with_crate_names(
581 source: &str,
582 crate_names: &[&str],
583) -> Vec<(String, String, Vec<String>)> {
584 let mut parser = RustExtractor::parser();
585 let tree = match parser.parse(source, None) {
586 Some(t) => t,
587 None => return Vec::new(),
588 };
589 let source_bytes = source.as_bytes();
590 let root = tree.root_node();
591 let mut results = Vec::new();
592
593 for i in 0..root.child_count() {
594 let child = root.child(i).unwrap();
595 if child.kind() != "use_declaration" {
596 continue;
597 }
598 let arg = match child.child_by_field_name("argument") {
599 Some(a) => a,
600 None => continue,
601 };
602 let full_text = arg.utf8_text(source_bytes).unwrap_or("");
603
604 if let Some(path_after_crate) = full_text.strip_prefix("crate::") {
606 let mut map: HashMap<String, Vec<String>> = HashMap::new();
607 parse_use_path(path_after_crate, &mut map);
608 for (specifier, symbols) in map {
609 results.push(("crate".to_string(), specifier, symbols));
610 }
611 continue;
612 }
613
614 for &name in crate_names {
616 let prefix = format!("{name}::");
617 if let Some(path_after_name) = full_text.strip_prefix(&prefix) {
618 let mut map: HashMap<String, Vec<String>> = HashMap::new();
619 parse_use_path(path_after_name, &mut map);
620 for (specifier, symbols) in map {
621 results.push((name.to_string(), specifier, symbols));
622 }
623 break; }
625 }
626 }
627
628 results
629}
630
631#[derive(Debug)]
637pub struct WorkspaceMember {
638 pub crate_name: String,
640 pub member_root: std::path::PathBuf,
642}
643
644const SKIP_DIRS: &[&str] = &["target", ".cargo", "vendor"];
646
647const MAX_TRAVERSE_DEPTH: usize = 4;
649
650pub fn has_workspace_section(scan_root: &Path) -> bool {
652 let cargo_toml = scan_root.join("Cargo.toml");
653 let content = match std::fs::read_to_string(&cargo_toml) {
654 Ok(c) => c,
655 Err(_) => return false,
656 };
657 content.lines().any(|line| line.trim() == "[workspace]")
658}
659
660pub fn find_workspace_members(scan_root: &Path) -> Vec<WorkspaceMember> {
671 if !has_workspace_section(scan_root) {
672 return Vec::new();
673 }
674
675 let mut members = Vec::new();
676 find_members_recursive(scan_root, scan_root, 0, &mut members);
677 members
678}
679
680fn find_members_recursive(
681 scan_root: &Path,
682 dir: &Path,
683 depth: usize,
684 members: &mut Vec<WorkspaceMember>,
685) {
686 if depth > MAX_TRAVERSE_DEPTH {
687 return;
688 }
689
690 let read_dir = match std::fs::read_dir(dir) {
691 Ok(rd) => rd,
692 Err(_) => return,
693 };
694
695 for entry in read_dir.flatten() {
696 let path = entry.path();
697 if !path.is_dir() {
698 continue;
699 }
700
701 let dir_name = match path.file_name().and_then(|n| n.to_str()) {
702 Some(n) => n,
703 None => continue,
704 };
705
706 if dir_name.starts_with('.') || SKIP_DIRS.contains(&dir_name) {
708 continue;
709 }
710
711 if path == scan_root {
713 continue;
714 }
715
716 if let Some(crate_name) = parse_crate_name(&path) {
718 members.push(WorkspaceMember {
719 crate_name,
720 member_root: path.to_path_buf(),
721 });
722 continue;
725 }
726
727 find_members_recursive(scan_root, &path, depth + 1, members);
729 }
730}
731
732pub fn find_member_by_crate_name<'a>(
736 name: &str,
737 members: &'a [WorkspaceMember],
738) -> Option<&'a WorkspaceMember> {
739 members.iter().find(|m| m.crate_name == name)
740}
741
742pub fn find_member_for_path<'a>(
746 path: &Path,
747 members: &'a [WorkspaceMember],
748) -> Option<&'a WorkspaceMember> {
749 members
750 .iter()
751 .filter(|m| path.starts_with(&m.member_root))
752 .max_by_key(|m| m.member_root.components().count())
753}
754
755pub fn parse_crate_name(scan_root: &Path) -> Option<String> {
759 let cargo_toml = scan_root.join("Cargo.toml");
760 let content = std::fs::read_to_string(&cargo_toml).ok()?;
761
762 let mut in_package = false;
763 for line in content.lines() {
764 let trimmed = line.trim();
765
766 if trimmed.starts_with('[') {
768 if trimmed == "[package]" {
769 in_package = true;
770 } else {
771 if in_package {
773 break;
774 }
775 }
776 continue;
777 }
778
779 if in_package {
780 if let Some(rest) = trimmed.strip_prefix("name") {
782 let rest = rest.trim();
783 if let Some(rest) = rest.strip_prefix('=') {
784 let rest = rest.trim();
785 let name = if let Some(inner) =
787 rest.strip_prefix('"').and_then(|s| s.strip_suffix('"'))
788 {
789 inner
790 } else if let Some(inner) =
791 rest.strip_prefix('\'').and_then(|s| s.strip_suffix('\''))
792 {
793 inner
794 } else {
795 continue;
796 };
797 return Some(name.replace('-', "_"));
798 }
799 }
800 }
801 }
802
803 None
804}
805
806fn parse_use_path(path: &str, result: &mut HashMap<String, Vec<String>>) {
812 if let Some(brace_start) = path.find('{') {
814 let module_part = &path[..brace_start.saturating_sub(2)]; let specifier = module_part.replace("::", "/");
816 if let Some(brace_end) = path.find('}') {
817 let list_content = &path[brace_start + 1..brace_end];
818 let symbols: Vec<String> = list_content
819 .split(',')
820 .map(|s| s.trim().to_string())
821 .filter(|s| !s.is_empty() && s != "*")
822 .collect();
823 result.entry(specifier).or_default().extend(symbols);
824 }
825 return;
826 }
827
828 if let Some(module_part) = path.strip_suffix("::*") {
830 let specifier = module_part.replace("::", "/");
831 if !specifier.is_empty() {
832 result.entry(specifier).or_default();
833 }
834 return;
835 }
836
837 if !path.contains("::") && !path.is_empty() {
839 result.entry(path.to_string()).or_default();
840 return;
841 }
842
843 let parts: Vec<&str> = path.split("::").collect();
845 if parts.len() >= 2 {
846 let module_parts = &parts[..parts.len() - 1];
847 let symbol = parts[parts.len() - 1];
848 let specifier = module_parts.join("/");
849 result
850 .entry(specifier)
851 .or_default()
852 .push(symbol.to_string());
853 }
854}
855
856fn extract_re_exports_from_text(text: &str, result: &mut Vec<BarrelReExport>) {
859 let joined = join_multiline_pub_use(text);
860 for line in joined.lines() {
861 let trimmed = line.trim();
862 if trimmed == "{" || trimmed == "}" {
864 continue;
865 }
866 let trimmed = trimmed
868 .strip_prefix('{')
869 .unwrap_or(trimmed)
870 .strip_suffix('}')
871 .unwrap_or(trimmed)
872 .trim();
873
874 let statements: Vec<&str> = trimmed
879 .split(';')
880 .map(str::trim)
881 .filter(|s| !s.is_empty())
882 .collect();
883 for stmt in statements {
884 extract_single_re_export_stmt(stmt, result);
885 }
886 }
887}
888
889fn extract_single_re_export_stmt(trimmed: &str, result: &mut Vec<BarrelReExport>) {
890 if trimmed.starts_with("pub mod ") || trimmed.starts_with("pub(crate) mod ") {
892 let mod_name = trimmed
893 .trim_start_matches("pub(crate) mod ")
894 .trim_start_matches("pub mod ")
895 .trim_end_matches(';')
896 .trim();
897 if !mod_name.is_empty() && !mod_name.contains(' ') {
898 result.push(BarrelReExport {
899 symbols: Vec::new(),
900 from_specifier: format!("./{mod_name}"),
901 wildcard: true,
902 namespace_wildcard: false,
903 });
904 }
905 }
906
907 if trimmed.starts_with("pub use ") && trimmed.contains("::") {
909 let use_path = trimmed
910 .trim_start_matches("pub use ")
911 .trim_end_matches(';')
912 .trim();
913 let use_path = use_path.strip_prefix("self::").unwrap_or(use_path);
914 if use_path == "*" {
916 result.push(BarrelReExport {
917 symbols: Vec::new(),
918 from_specifier: "./".to_string(),
919 wildcard: true,
920 namespace_wildcard: false,
921 });
922 return;
923 }
924 if use_path.ends_with("::*") {
926 let module_part = use_path.strip_suffix("::*").unwrap_or("");
927 result.push(BarrelReExport {
928 symbols: Vec::new(),
929 from_specifier: format!("./{}", module_part.replace("::", "/")),
930 wildcard: true,
931 namespace_wildcard: false,
932 });
933 } else if let Some(brace_start) = use_path.find('{') {
934 let module_part = &use_path[..brace_start.saturating_sub(2)];
935 if let Some(brace_end) = use_path.find('}') {
936 let list_content = &use_path[brace_start + 1..brace_end];
937 let symbols: Vec<String> = list_content
938 .split(',')
939 .map(|s| s.trim().to_string())
940 .filter(|s| !s.is_empty() && s != "*")
941 .collect();
942 result.push(BarrelReExport {
943 symbols,
944 from_specifier: format!("./{}", module_part.replace("::", "/")),
945 wildcard: false,
946 namespace_wildcard: false,
947 });
948 }
949 } else {
950 let parts: Vec<&str> = use_path.split("::").collect();
952 if parts.len() >= 2 {
953 let module_parts = &parts[..parts.len() - 1];
954 let symbol = parts[parts.len() - 1];
955 result.push(BarrelReExport {
956 symbols: vec![symbol.to_string()],
957 from_specifier: format!("./{}", module_parts.join("/")),
958 wildcard: false,
959 namespace_wildcard: false,
960 });
961 }
962 }
963 }
964}
965
966fn extract_pub_use_re_exports(
968 arg: &tree_sitter::Node,
969 source_bytes: &[u8],
970 result: &mut Vec<BarrelReExport>,
971) {
972 let full_text = arg.utf8_text(source_bytes).unwrap_or("");
973 let full_text = full_text.strip_prefix("self::").unwrap_or(full_text);
975
976 if full_text == "*" {
978 result.push(BarrelReExport {
979 symbols: Vec::new(),
980 from_specifier: "./".to_string(),
981 wildcard: true,
982 namespace_wildcard: false,
983 });
984 return;
985 }
986
987 if full_text.ends_with("::*") {
989 let module_part = full_text.strip_suffix("::*").unwrap_or("");
990 result.push(BarrelReExport {
991 symbols: Vec::new(),
992 from_specifier: format!("./{}", module_part.replace("::", "/")),
993 wildcard: true,
994 namespace_wildcard: false,
995 });
996 return;
997 }
998
999 if let Some(brace_start) = full_text.find('{') {
1001 let module_part = &full_text[..brace_start.saturating_sub(2)]; if let Some(brace_end) = full_text.find('}') {
1003 let list_content = &full_text[brace_start + 1..brace_end];
1004 let symbols: Vec<String> = list_content
1005 .split(',')
1006 .map(|s| s.trim().to_string())
1007 .filter(|s| !s.is_empty())
1008 .collect();
1009 result.push(BarrelReExport {
1010 symbols,
1011 from_specifier: format!("./{}", module_part.replace("::", "/")),
1012 wildcard: false,
1013 namespace_wildcard: false,
1014 });
1015 }
1016 return;
1017 }
1018
1019 let parts: Vec<&str> = full_text.split("::").collect();
1021 if parts.len() >= 2 {
1022 let module_parts = &parts[..parts.len() - 1];
1023 let symbol = parts[parts.len() - 1];
1024 result.push(BarrelReExport {
1025 symbols: vec![symbol.to_string()],
1026 from_specifier: format!("./{}", module_parts.join("/")),
1027 wildcard: false,
1028 namespace_wildcard: false,
1029 });
1030 }
1031}
1032
1033fn extract_test_subdir(path: &str) -> Option<String> {
1044 let parts: Vec<&str> = path.split('/').collect();
1045 for (i, part) in parts.iter().enumerate() {
1046 if *part == "tests" && i + 2 < parts.len() {
1047 return Some(parts[i + 1].to_string());
1048 }
1049 }
1050 None
1051}
1052
1053impl RustExtractor {
1058 pub fn map_test_files_with_imports(
1064 &self,
1065 production_files: &[String],
1066 test_sources: &HashMap<String, String>,
1067 scan_root: &Path,
1068 l1_exclusive: bool,
1069 ) -> Vec<FileMapping> {
1070 let test_file_list: Vec<String> = test_sources.keys().cloned().collect();
1071
1072 let mut mappings =
1074 exspec_core::observe::map_test_files(self, production_files, &test_file_list);
1075
1076 for (idx, prod_file) in production_files.iter().enumerate() {
1078 if production_stem(prod_file).is_none() {
1080 continue;
1081 }
1082 if let Ok(source) = std::fs::read_to_string(prod_file) {
1083 if detect_inline_tests(&source) {
1084 if !mappings[idx].test_files.contains(prod_file) {
1086 mappings[idx].test_files.push(prod_file.clone());
1087 }
1088 }
1089 }
1090 }
1091
1092 let canonical_root = match scan_root.canonicalize() {
1094 Ok(r) => r,
1095 Err(_) => return mappings,
1096 };
1097 let mut canonical_to_idx: HashMap<String, usize> = HashMap::new();
1098 for (idx, prod) in production_files.iter().enumerate() {
1099 if let Ok(canonical) = Path::new(prod).canonicalize() {
1100 canonical_to_idx.insert(canonical.to_string_lossy().into_owned(), idx);
1101 }
1102 }
1103
1104 let layer1_tests_per_prod: Vec<HashSet<String>> = mappings
1106 .iter()
1107 .map(|m| m.test_files.iter().cloned().collect())
1108 .collect();
1109
1110 let mut layer1_matched: HashSet<String> = layer1_tests_per_prod
1112 .iter()
1113 .flat_map(|s| s.iter().cloned())
1114 .collect();
1115
1116 self.apply_l1_5_underscore_path_matching(
1119 &mut mappings,
1120 &test_file_list,
1121 &mut layer1_matched,
1122 );
1123
1124 self.apply_l1_subdir_matching(&mut mappings, &test_file_list, &mut layer1_matched);
1128
1129 let crate_name = parse_crate_name(scan_root);
1131 let members = find_workspace_members(scan_root);
1132
1133 if let Some(ref name) = crate_name {
1135 self.apply_l2_imports(
1137 test_sources,
1138 name,
1139 scan_root,
1140 &canonical_root,
1141 &canonical_to_idx,
1142 &mut mappings,
1143 l1_exclusive,
1144 &layer1_matched,
1145 );
1146 }
1147
1148 if !members.is_empty() {
1149 for member in &members {
1151 let member_test_sources: HashMap<String, String> = test_sources
1153 .iter()
1154 .filter(|(path, _)| {
1155 find_member_for_path(Path::new(path.as_str()), &members)
1156 .map(|m| std::ptr::eq(m, member))
1157 .unwrap_or(false)
1158 })
1159 .map(|(k, v)| (k.clone(), v.clone()))
1160 .collect();
1161
1162 self.apply_l2_imports(
1163 &member_test_sources,
1164 &member.crate_name,
1165 &member.member_root,
1166 &canonical_root,
1167 &canonical_to_idx,
1168 &mut mappings,
1169 l1_exclusive,
1170 &layer1_matched,
1171 );
1172 }
1173
1174 let root_test_sources: HashMap<String, String> = test_sources
1179 .iter()
1180 .filter(|(path, _)| {
1181 find_member_for_path(Path::new(path.as_str()), &members).is_none()
1182 })
1183 .map(|(k, v)| (k.clone(), v.clone()))
1184 .collect();
1185
1186 if !root_test_sources.is_empty() {
1187 for member in &members {
1188 self.apply_l2_imports(
1190 &root_test_sources,
1191 &member.crate_name,
1192 &member.member_root,
1193 &canonical_root,
1194 &canonical_to_idx,
1195 &mut mappings,
1196 l1_exclusive,
1197 &layer1_matched,
1198 );
1199 if let Some(ref root_name) = crate_name {
1202 if *root_name != member.crate_name {
1203 self.apply_l2_imports(
1204 &root_test_sources,
1205 root_name,
1206 &member.member_root,
1207 &canonical_root,
1208 &canonical_to_idx,
1209 &mut mappings,
1210 l1_exclusive,
1211 &layer1_matched,
1212 );
1213 }
1214 }
1215 }
1216 }
1217 } else if crate_name.is_none() {
1218 self.apply_l2_imports(
1221 test_sources,
1222 "crate",
1223 scan_root,
1224 &canonical_root,
1225 &canonical_to_idx,
1226 &mut mappings,
1227 l1_exclusive,
1228 &layer1_matched,
1229 );
1230 }
1231
1232 if let Some(ref root_name) = crate_name {
1236 let root_lib = scan_root.join("src/lib.rs");
1237 if root_lib.exists() && !members.is_empty() {
1238 let root_test_sources: HashMap<String, String> = test_sources
1239 .iter()
1240 .filter(|(path, _)| {
1241 find_member_for_path(Path::new(path.as_str()), &members).is_none()
1242 })
1243 .map(|(k, v)| (k.clone(), v.clone()))
1244 .collect();
1245 if !root_test_sources.is_empty() {
1246 self.apply_l2_cross_crate_barrel(
1247 &root_test_sources,
1248 root_name,
1249 &root_lib,
1250 &members,
1251 &canonical_root,
1252 &canonical_to_idx,
1253 &mut mappings,
1254 l1_exclusive,
1255 &layer1_matched,
1256 );
1257 }
1258 }
1259 }
1260
1261 for (i, mapping) in mappings.iter_mut().enumerate() {
1264 let has_layer1 = !layer1_tests_per_prod[i].is_empty();
1265 if !has_layer1 && !mapping.test_files.is_empty() {
1266 mapping.strategy = MappingStrategy::ImportTracing;
1267 }
1268 }
1269
1270 mappings
1271 }
1272
1273 #[allow(clippy::too_many_arguments)]
1278 fn apply_l2_imports(
1279 &self,
1280 test_sources: &HashMap<String, String>,
1281 crate_name: &str,
1282 crate_root: &Path,
1283 canonical_root: &Path,
1284 canonical_to_idx: &HashMap<String, usize>,
1285 mappings: &mut [FileMapping],
1286 l1_exclusive: bool,
1287 layer1_matched: &HashSet<String>,
1288 ) {
1289 for (test_file, source) in test_sources {
1290 if l1_exclusive && layer1_matched.contains(test_file) {
1291 continue;
1292 }
1293 let imports = extract_import_specifiers_with_crate_name(source, Some(crate_name));
1294 let mut matched_indices = HashSet::<usize>::new();
1295
1296 for (specifier, symbols) in &imports {
1297 let src_relative = crate_root.join("src").join(specifier);
1299
1300 if let Some(resolved) = exspec_core::observe::resolve_absolute_base_to_file(
1301 self,
1302 &src_relative,
1303 canonical_root,
1304 ) {
1305 let mut per_specifier_indices = HashSet::<usize>::new();
1306 exspec_core::observe::collect_import_matches(
1307 self,
1308 &resolved,
1309 symbols,
1310 canonical_to_idx,
1311 &mut per_specifier_indices,
1312 canonical_root,
1313 );
1314 for idx in per_specifier_indices {
1316 let prod_path = Path::new(&mappings[idx].production_file);
1317 if self.file_exports_any_symbol(prod_path, symbols) {
1318 matched_indices.insert(idx);
1319 }
1320 }
1321 }
1322 }
1323
1324 for idx in matched_indices {
1325 if !mappings[idx].test_files.contains(test_file) {
1326 mappings[idx].test_files.push(test_file.clone());
1327 }
1328 }
1329 }
1330 }
1331
1332 #[allow(clippy::too_many_arguments)]
1341 fn apply_l2_cross_crate_barrel(
1342 &self,
1343 test_sources: &HashMap<String, String>,
1344 root_crate_name: &str,
1345 root_lib_path: &Path,
1346 members: &[WorkspaceMember],
1347 canonical_root: &Path,
1348 canonical_to_idx: &HashMap<String, usize>,
1349 mappings: &mut [FileMapping],
1350 l1_exclusive: bool,
1351 layer1_matched: &HashSet<String>,
1352 ) {
1353 let root_lib_source = match std::fs::read_to_string(root_lib_path) {
1355 Ok(s) => s,
1356 Err(_) => return,
1357 };
1358 let root_lib_str = root_lib_path.to_string_lossy();
1359
1360 let barrel_exports = self.extract_barrel_re_exports(&root_lib_source, &root_lib_str);
1362 if barrel_exports.is_empty() {
1363 return;
1364 }
1365
1366 for (test_file, source) in test_sources {
1367 if l1_exclusive && layer1_matched.contains(test_file) {
1368 continue;
1369 }
1370
1371 let imports = extract_import_specifiers_with_crate_name(source, Some(root_crate_name));
1376 let root_symbols: Vec<String> = {
1377 let mut syms = Vec::new();
1378 for (specifier, symbols) in &imports {
1379 if specifier.is_empty() && !symbols.is_empty() {
1380 syms.extend(symbols.clone());
1382 } else if !specifier.is_empty()
1383 && !specifier.contains('/')
1384 && symbols.is_empty()
1385 {
1386 syms.push(specifier.clone());
1388 }
1389 }
1390 syms
1391 };
1392
1393 if root_symbols.is_empty() {
1394 continue;
1395 }
1396
1397 let mut matched_indices = HashSet::<usize>::new();
1398
1399 for barrel in &barrel_exports {
1400 let crate_candidate = barrel
1402 .from_specifier
1403 .strip_prefix("./")
1404 .unwrap_or(&barrel.from_specifier);
1405
1406 if crate_candidate.contains('/') {
1409 continue;
1410 }
1411
1412 let member = match find_member_by_crate_name(crate_candidate, members) {
1414 Some(m) => m,
1415 None => continue,
1416 };
1417
1418 let symbols_matched: Vec<String> = if barrel.wildcard {
1420 root_symbols.clone()
1422 } else {
1423 root_symbols
1425 .iter()
1426 .filter(|sym| barrel.symbols.contains(sym))
1427 .cloned()
1428 .collect()
1429 };
1430
1431 if symbols_matched.is_empty() {
1432 continue;
1433 }
1434
1435 let member_lib = member.member_root.join("src/lib.rs");
1437
1438 if barrel.wildcard {
1442 let canonical_member_root = member
1443 .member_root
1444 .canonicalize()
1445 .unwrap_or_else(|_| member.member_root.clone());
1446 let canonical_member_str = canonical_member_root.to_string_lossy().into_owned();
1447 for (prod_str, &idx) in canonical_to_idx.iter() {
1448 if prod_str.starts_with(&canonical_member_str) {
1449 let prod_path = Path::new(&mappings[idx].production_file);
1450 if self.file_exports_any_symbol(prod_path, &symbols_matched) {
1451 matched_indices.insert(idx);
1452 }
1453 }
1454 }
1455 }
1456
1457 if let Some(resolved) = exspec_core::observe::resolve_absolute_base_to_file(
1458 self,
1459 &member_lib,
1460 canonical_root,
1461 ) {
1462 if let Some(&idx) = canonical_to_idx.get(&resolved) {
1466 let prod_path = Path::new(&mappings[idx].production_file);
1467 if self.file_exports_any_symbol(prod_path, &symbols_matched) {
1468 matched_indices.insert(idx);
1469 }
1470 }
1471
1472 let mut per_member_indices = HashSet::<usize>::new();
1475 exspec_core::observe::collect_import_matches(
1476 self,
1477 &resolved,
1478 &symbols_matched,
1479 canonical_to_idx,
1480 &mut per_member_indices,
1481 canonical_root,
1482 );
1483 for idx in per_member_indices {
1485 let prod_path = Path::new(&mappings[idx].production_file);
1486 if self.file_exports_any_symbol(prod_path, &symbols_matched) {
1487 matched_indices.insert(idx);
1488 }
1489 }
1490 }
1491 }
1492
1493 for idx in matched_indices {
1494 if !mappings[idx].test_files.contains(test_file) {
1495 mappings[idx].test_files.push(test_file.clone());
1496 }
1497 }
1498 }
1499 }
1500
1501 fn apply_l1_subdir_matching(
1510 &self,
1511 mappings: &mut [FileMapping],
1512 test_paths: &[String],
1513 layer1_matched: &mut HashSet<String>,
1514 ) {
1515 for test_path in test_paths {
1516 if layer1_matched.contains(test_path) {
1517 continue;
1518 }
1519
1520 let test_stem = match self.test_stem(test_path) {
1521 Some(s) => s,
1522 None => continue,
1523 };
1524
1525 let normalized = test_path.replace('\\', "/");
1526 let test_subdir = extract_test_subdir(&normalized);
1527 if test_subdir.as_ref().is_none_or(|s| s.len() < 3) {
1528 continue;
1529 }
1530 let test_subdir = test_subdir.unwrap();
1531
1532 let subdir_lower = test_subdir.to_lowercase();
1533 let stem_lower = test_stem.to_lowercase();
1534 let dir_segment = format!("/{subdir_lower}/");
1535
1536 for mapping in mappings.iter_mut() {
1537 let prod_stem = match self.production_stem(&mapping.production_file) {
1538 Some(s) => s,
1539 None => continue,
1540 };
1541
1542 if prod_stem.to_lowercase() != stem_lower {
1543 continue;
1544 }
1545
1546 let prod_path_lower = mapping.production_file.replace('\\', "/").to_lowercase();
1547 if prod_path_lower.contains(&dir_segment) {
1548 if !mapping.test_files.contains(test_path) {
1549 mapping.test_files.push(test_path.clone());
1550 }
1551 layer1_matched.insert(test_path.clone());
1552 break;
1553 }
1554 }
1555 }
1556 }
1557
1558 fn apply_l1_5_underscore_path_matching(
1566 &self,
1567 mappings: &mut [FileMapping],
1568 test_paths: &[String],
1569 layer1_matched: &mut HashSet<String>,
1570 ) {
1571 for test_path in test_paths {
1572 if layer1_matched.contains(test_path) {
1573 continue;
1574 }
1575
1576 let test_stem = match self.test_stem(test_path) {
1577 Some(s) => s,
1578 None => continue,
1579 };
1580
1581 if !test_stem.contains('_') {
1582 continue;
1583 }
1584
1585 let underscore_pos = match test_stem.find('_') {
1586 Some(pos) => pos,
1587 None => continue,
1588 };
1589
1590 let prefix = &test_stem[..underscore_pos];
1591 let suffix = &test_stem[underscore_pos + 1..];
1592
1593 if suffix.len() <= 2 {
1594 continue;
1595 }
1596
1597 let prefix_lower = prefix.to_lowercase();
1598 let suffix_lower = suffix.to_lowercase();
1599 let dir_segment = format!("/{prefix_lower}/");
1600
1601 for mapping in mappings.iter_mut() {
1602 let prod_stem = match self.production_stem(&mapping.production_file) {
1603 Some(s) => s,
1604 None => continue,
1605 };
1606
1607 if prod_stem.to_lowercase() != suffix_lower {
1608 continue;
1609 }
1610
1611 let prod_path_lower = mapping.production_file.replace('\\', "/").to_lowercase();
1612 let test_first = test_path.split('/').next().unwrap_or("");
1615 let prod_first = mapping.production_file.split('/').next().unwrap_or("");
1616 let test_has_crate_prefix = test_first != "tests" && test_first != "src";
1617 let prod_has_crate_prefix = prod_first != "tests" && prod_first != "src";
1618 if test_has_crate_prefix
1619 && prod_has_crate_prefix
1620 && !test_first.eq_ignore_ascii_case(prod_first)
1621 {
1622 continue;
1623 }
1624 if prod_path_lower.contains(&dir_segment) {
1625 mapping.test_files.push(test_path.clone());
1626 layer1_matched.insert(test_path.clone());
1627 break;
1628 }
1629 }
1630 }
1631 }
1632}
1633
1634pub(crate) fn join_multiline_pub_use(text: &str) -> String {
1641 let mut result = String::new();
1642 let mut accumulator: Option<String> = None;
1643 let mut brace_depth: usize = 0;
1644 for line in text.lines() {
1645 let trimmed = line.trim();
1646 if let Some(ref mut acc) = accumulator {
1647 acc.push(' ');
1648 acc.push_str(trimmed);
1649 for ch in trimmed.chars() {
1650 match ch {
1651 '{' => brace_depth += 1,
1652 '}' => brace_depth = brace_depth.saturating_sub(1),
1653 _ => {}
1654 }
1655 }
1656 if brace_depth == 0 {
1657 result.push_str(acc);
1658 result.push('\n');
1659 accumulator = None;
1660 }
1661 } else if trimmed.starts_with("pub use ") && trimmed.contains('{') && !trimmed.contains('}')
1662 {
1663 brace_depth = trimmed.chars().filter(|&c| c == '{').count()
1664 - trimmed.chars().filter(|&c| c == '}').count();
1665 accumulator = Some(trimmed.to_string());
1666 } else {
1667 result.push_str(line);
1668 result.push('\n');
1669 }
1670 }
1671 if let Some(acc) = accumulator {
1672 result.push_str(&acc);
1673 result.push('\n');
1674 }
1675 result
1676}
1677
1678#[cfg(test)]
1683mod tests {
1684 use super::*;
1685 use std::path::PathBuf;
1686
1687 #[test]
1691 fn rs_stem_01_test_prefix() {
1692 let extractor = RustExtractor::new();
1696 assert_eq!(extractor.test_stem("tests/test_foo.rs"), Some("foo"));
1697 }
1698
1699 #[test]
1703 fn rs_stem_02_test_suffix() {
1704 let extractor = RustExtractor::new();
1708 assert_eq!(extractor.test_stem("tests/foo_test.rs"), Some("foo"));
1709 }
1710
1711 #[test]
1715 fn rs_stem_03_tests_dir_integration() {
1716 let extractor = RustExtractor::new();
1720 assert_eq!(
1721 extractor.test_stem("tests/integration.rs"),
1722 Some("integration")
1723 );
1724 }
1725
1726 #[test]
1730 fn rs_stem_04_production_file_no_test_stem() {
1731 let extractor = RustExtractor::new();
1735 assert_eq!(extractor.test_stem("src/user.rs"), None);
1736 }
1737
1738 #[test]
1742 fn rs_stem_05_production_stem_regular() {
1743 let extractor = RustExtractor::new();
1747 assert_eq!(extractor.production_stem("src/user.rs"), Some("user"));
1748 }
1749
1750 #[test]
1754 fn rs_stem_06_production_stem_lib() {
1755 let extractor = RustExtractor::new();
1759 assert_eq!(extractor.production_stem("src/lib.rs"), None);
1760 }
1761
1762 #[test]
1766 fn rs_stem_07_production_stem_mod() {
1767 let extractor = RustExtractor::new();
1771 assert_eq!(extractor.production_stem("src/mod.rs"), None);
1772 }
1773
1774 #[test]
1778 fn rs_stem_08_production_stem_main() {
1779 let extractor = RustExtractor::new();
1783 assert_eq!(extractor.production_stem("src/main.rs"), None);
1784 }
1785
1786 #[test]
1790 fn rs_stem_09_production_stem_test_file() {
1791 let extractor = RustExtractor::new();
1795 assert_eq!(extractor.production_stem("tests/test_foo.rs"), None);
1796 }
1797
1798 #[test]
1802 fn rs_helper_01_build_rs() {
1803 let extractor = RustExtractor::new();
1807 assert!(extractor.is_non_sut_helper("build.rs", false));
1808 }
1809
1810 #[test]
1814 fn rs_helper_02_tests_common() {
1815 let extractor = RustExtractor::new();
1819 assert!(extractor.is_non_sut_helper("tests/common/mod.rs", false));
1820 }
1821
1822 #[test]
1826 fn rs_helper_03_regular_production_file() {
1827 let extractor = RustExtractor::new();
1831 assert!(!extractor.is_non_sut_helper("src/user.rs", false));
1832 }
1833
1834 #[test]
1838 fn rs_helper_04_benches() {
1839 let extractor = RustExtractor::new();
1843 assert!(extractor.is_non_sut_helper("benches/bench.rs", false));
1844 }
1845
1846 #[test]
1850 fn rs_l0_01_cfg_test_present() {
1851 let source = r#"
1853pub fn add(a: i32, b: i32) -> i32 { a + b }
1854
1855#[cfg(test)]
1856mod tests {
1857 use super::*;
1858
1859 #[test]
1860 fn test_add() {
1861 assert_eq!(add(1, 2), 3);
1862 }
1863}
1864"#;
1865 assert!(detect_inline_tests(source));
1868 }
1869
1870 #[test]
1874 fn rs_l0_02_no_cfg_test() {
1875 let source = r#"
1877pub fn add(a: i32, b: i32) -> i32 { a + b }
1878"#;
1879 assert!(!detect_inline_tests(source));
1882 }
1883
1884 #[test]
1888 fn rs_l0_03_cfg_not_test() {
1889 let source = r#"
1891#[cfg(not(test))]
1892mod production_only {
1893 pub fn real_thing() {}
1894}
1895"#;
1896 assert!(!detect_inline_tests(source));
1899 }
1900
1901 #[test]
1905 fn rs_l0_04_cfg_all_test() {
1906 let source = r#"
1907pub(crate) fn do_work() {}
1908
1909#[cfg(all(test, not(loom)))]
1910pub(crate) mod test {
1911 use super::*;
1912
1913 #[test]
1914 fn test_do_work() {
1915 do_work();
1916 }
1917}
1918"#;
1919 assert!(detect_inline_tests(source));
1920 }
1921
1922 #[test]
1926 fn rs_l0_05_cfg_any_test() {
1927 let source = r#"
1928pub struct LinkedList;
1929
1930#[cfg(any(test, fuzzing))]
1931#[cfg(not(loom))]
1932pub(crate) mod tests {
1933 use super::*;
1934
1935 #[test]
1936 fn const_new() {
1937 let _ = LinkedList;
1938 }
1939}
1940"#;
1941 assert!(detect_inline_tests(source));
1942 }
1943
1944 #[test]
1948 fn rs_func_01_pub_function() {
1949 let source = "pub fn create_user() {}\n";
1951
1952 let extractor = RustExtractor::new();
1954 let result = extractor.extract_production_functions(source, "src/user.rs");
1955
1956 let func = result.iter().find(|f| f.name == "create_user");
1958 assert!(func.is_some(), "create_user not found in {:?}", result);
1959 assert!(func.unwrap().is_exported);
1960 }
1961
1962 #[test]
1966 fn rs_func_02_private_function() {
1967 let source = "fn private_fn() {}\n";
1969
1970 let extractor = RustExtractor::new();
1972 let result = extractor.extract_production_functions(source, "src/internal.rs");
1973
1974 let func = result.iter().find(|f| f.name == "private_fn");
1976 assert!(func.is_some(), "private_fn not found in {:?}", result);
1977 assert!(!func.unwrap().is_exported);
1978 }
1979
1980 #[test]
1984 fn rs_func_03_impl_method() {
1985 let source = r#"
1987struct User;
1988
1989impl User {
1990 pub fn save(&self) {}
1991}
1992"#;
1993 let extractor = RustExtractor::new();
1995 let result = extractor.extract_production_functions(source, "src/user.rs");
1996
1997 let method = result.iter().find(|f| f.name == "save");
1999 assert!(method.is_some(), "save not found in {:?}", result);
2000 let method = method.unwrap();
2001 assert_eq!(method.class_name, Some("User".to_string()));
2002 assert!(method.is_exported);
2003 }
2004
2005 #[test]
2009 fn rs_func_04_cfg_test_excluded() {
2010 let source = r#"
2012pub fn real_function() {}
2013
2014#[cfg(test)]
2015mod tests {
2016 use super::*;
2017
2018 #[test]
2019 fn test_real_function() {
2020 assert!(true);
2021 }
2022}
2023"#;
2024 let extractor = RustExtractor::new();
2026 let result = extractor.extract_production_functions(source, "src/lib.rs");
2027
2028 assert_eq!(result.len(), 1);
2030 assert_eq!(result[0].name, "real_function");
2031 }
2032
2033 #[test]
2037 fn rs_imp_01_simple_crate_import() {
2038 let source = "use crate::user::User;\n";
2040
2041 let extractor = RustExtractor::new();
2043 let result = extractor.extract_all_import_specifiers(source);
2044
2045 let entry = result.iter().find(|(spec, _)| spec == "user");
2047 assert!(entry.is_some(), "user not found in {:?}", result);
2048 let (_, symbols) = entry.unwrap();
2049 assert!(symbols.contains(&"User".to_string()));
2050 }
2051
2052 #[test]
2056 fn rs_imp_02_nested_crate_import() {
2057 let source = "use crate::models::user::User;\n";
2059
2060 let extractor = RustExtractor::new();
2062 let result = extractor.extract_all_import_specifiers(source);
2063
2064 let entry = result.iter().find(|(spec, _)| spec == "models/user");
2066 assert!(entry.is_some(), "models/user not found in {:?}", result);
2067 let (_, symbols) = entry.unwrap();
2068 assert!(symbols.contains(&"User".to_string()));
2069 }
2070
2071 #[test]
2075 fn rs_imp_03_use_list() {
2076 let source = "use crate::user::{User, Admin};\n";
2078
2079 let extractor = RustExtractor::new();
2081 let result = extractor.extract_all_import_specifiers(source);
2082
2083 let entry = result.iter().find(|(spec, _)| spec == "user");
2085 assert!(entry.is_some(), "user not found in {:?}", result);
2086 let (_, symbols) = entry.unwrap();
2087 assert!(
2088 symbols.contains(&"User".to_string()),
2089 "User not in {:?}",
2090 symbols
2091 );
2092 assert!(
2093 symbols.contains(&"Admin".to_string()),
2094 "Admin not in {:?}",
2095 symbols
2096 );
2097 }
2098
2099 #[test]
2103 fn rs_imp_04_external_crate_skipped() {
2104 let source = "use std::collections::HashMap;\n";
2106
2107 let extractor = RustExtractor::new();
2109 let result = extractor.extract_all_import_specifiers(source);
2110
2111 assert!(
2113 result.is_empty(),
2114 "external imports should be skipped: {:?}",
2115 result
2116 );
2117 }
2118
2119 #[test]
2123 fn rs_barrel_01_mod_rs() {
2124 let extractor = RustExtractor::new();
2128 assert!(extractor.is_barrel_file("src/models/mod.rs"));
2129 }
2130
2131 #[test]
2135 fn rs_barrel_02_lib_rs() {
2136 let extractor = RustExtractor::new();
2140 assert!(extractor.is_barrel_file("src/lib.rs"));
2141 }
2142
2143 #[test]
2147 fn rs_barrel_03_pub_mod() {
2148 let source = "pub mod user;\n";
2150
2151 let extractor = RustExtractor::new();
2153 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
2154
2155 let entry = result.iter().find(|e| e.from_specifier == "./user");
2157 assert!(entry.is_some(), "./user not found in {:?}", result);
2158 assert!(entry.unwrap().wildcard);
2159 }
2160
2161 #[test]
2165 fn rs_barrel_04_pub_use_wildcard() {
2166 let source = "pub use user::*;\n";
2168
2169 let extractor = RustExtractor::new();
2171 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
2172
2173 let entry = result.iter().find(|e| e.from_specifier == "./user");
2175 assert!(entry.is_some(), "./user not found in {:?}", result);
2176 assert!(entry.unwrap().wildcard);
2177 }
2178
2179 #[test]
2183 fn rs_e2e_01_inline_test_self_map() {
2184 let tmp = tempfile::tempdir().unwrap();
2186 let src_dir = tmp.path().join("src");
2187 std::fs::create_dir_all(&src_dir).unwrap();
2188
2189 let user_rs = src_dir.join("user.rs");
2190 std::fs::write(
2191 &user_rs,
2192 r#"pub fn create_user() {}
2193
2194#[cfg(test)]
2195mod tests {
2196 use super::*;
2197 #[test]
2198 fn test_create_user() { assert!(true); }
2199}
2200"#,
2201 )
2202 .unwrap();
2203
2204 let extractor = RustExtractor::new();
2205 let prod_path = user_rs.to_string_lossy().into_owned();
2206 let production_files = vec![prod_path.clone()];
2207 let test_sources: HashMap<String, String> = HashMap::new();
2208
2209 let result = extractor.map_test_files_with_imports(
2211 &production_files,
2212 &test_sources,
2213 tmp.path(),
2214 false,
2215 );
2216
2217 let mapping = result.iter().find(|m| m.production_file == prod_path);
2219 assert!(mapping.is_some());
2220 assert!(
2221 mapping.unwrap().test_files.contains(&prod_path),
2222 "Expected self-map for inline tests: {:?}",
2223 mapping.unwrap().test_files
2224 );
2225 }
2226
2227 #[test]
2231 fn rs_e2e_02_layer1_stem_match() {
2232 let extractor = RustExtractor::new();
2234 let production_files = vec!["src/user.rs".to_string()];
2235 let test_sources: HashMap<String, String> =
2236 [("tests/test_user.rs".to_string(), String::new())]
2237 .into_iter()
2238 .collect();
2239
2240 let scan_root = PathBuf::from(".");
2242 let result = extractor.map_test_files_with_imports(
2243 &production_files,
2244 &test_sources,
2245 &scan_root,
2246 false,
2247 );
2248
2249 let mapping = result.iter().find(|m| m.production_file == "src/user.rs");
2255 assert!(mapping.is_some());
2256 }
2257
2258 #[test]
2262 fn rs_e2e_03_layer2_import_tracing() {
2263 let tmp = tempfile::tempdir().unwrap();
2265 let src_dir = tmp.path().join("src");
2266 let tests_dir = tmp.path().join("tests");
2267 std::fs::create_dir_all(&src_dir).unwrap();
2268 std::fs::create_dir_all(&tests_dir).unwrap();
2269
2270 let service_rs = src_dir.join("service.rs");
2271 std::fs::write(&service_rs, "pub struct Service;\n").unwrap();
2272
2273 let test_service_rs = tests_dir.join("test_service.rs");
2274 let test_source = "use crate::service::Service;\n\n#[test]\nfn test_it() {}\n";
2275 std::fs::write(&test_service_rs, test_source).unwrap();
2276
2277 let extractor = RustExtractor::new();
2278 let prod_path = service_rs.to_string_lossy().into_owned();
2279 let test_path = test_service_rs.to_string_lossy().into_owned();
2280 let production_files = vec![prod_path.clone()];
2281 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
2282 .into_iter()
2283 .collect();
2284
2285 let result = extractor.map_test_files_with_imports(
2287 &production_files,
2288 &test_sources,
2289 tmp.path(),
2290 false,
2291 );
2292
2293 let mapping = result.iter().find(|m| m.production_file == prod_path);
2295 assert!(mapping.is_some());
2296 assert!(
2297 mapping.unwrap().test_files.contains(&test_path),
2298 "Expected import tracing match: {:?}",
2299 mapping.unwrap().test_files
2300 );
2301 }
2302
2303 #[test]
2307 fn rs_e2e_04_helper_excluded() {
2308 let extractor = RustExtractor::new();
2310 let production_files = vec!["src/user.rs".to_string()];
2311 let test_sources: HashMap<String, String> = [
2312 ("tests/test_user.rs".to_string(), String::new()),
2313 (
2314 "tests/common/mod.rs".to_string(),
2315 "pub fn setup() {}\n".to_string(),
2316 ),
2317 ]
2318 .into_iter()
2319 .collect();
2320
2321 let scan_root = PathBuf::from(".");
2323 let result = extractor.map_test_files_with_imports(
2324 &production_files,
2325 &test_sources,
2326 &scan_root,
2327 false,
2328 );
2329
2330 for mapping in &result {
2332 assert!(
2333 !mapping
2334 .test_files
2335 .iter()
2336 .any(|f| f.contains("common/mod.rs")),
2337 "common/mod.rs should not appear: {:?}",
2338 mapping
2339 );
2340 }
2341 }
2342
2343 #[test]
2347 fn rs_crate_01_parse_crate_name_hyphen() {
2348 let tmp = tempfile::tempdir().unwrap();
2350 std::fs::write(
2351 tmp.path().join("Cargo.toml"),
2352 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\n",
2353 )
2354 .unwrap();
2355
2356 let result = parse_crate_name(tmp.path());
2358
2359 assert_eq!(result, Some("my_crate".to_string()));
2361 }
2362
2363 #[test]
2367 fn rs_crate_02_parse_crate_name_no_hyphen() {
2368 let tmp = tempfile::tempdir().unwrap();
2370 std::fs::write(
2371 tmp.path().join("Cargo.toml"),
2372 "[package]\nname = \"tokio\"\nversion = \"1.0.0\"\n",
2373 )
2374 .unwrap();
2375
2376 let result = parse_crate_name(tmp.path());
2378
2379 assert_eq!(result, Some("tokio".to_string()));
2381 }
2382
2383 #[test]
2387 fn rs_crate_03_parse_crate_name_no_file() {
2388 let tmp = tempfile::tempdir().unwrap();
2390
2391 let result = parse_crate_name(tmp.path());
2393
2394 assert_eq!(result, None);
2396 }
2397
2398 #[test]
2402 fn rs_crate_04_parse_crate_name_workspace() {
2403 let tmp = tempfile::tempdir().unwrap();
2405 std::fs::write(
2406 tmp.path().join("Cargo.toml"),
2407 "[workspace]\nmembers = [\"crate1\"]\n",
2408 )
2409 .unwrap();
2410
2411 let result = parse_crate_name(tmp.path());
2413
2414 assert_eq!(result, None);
2416 }
2417
2418 #[test]
2422 fn rs_imp_05_crate_name_simple_import() {
2423 let source = "use my_crate::user::User;\n";
2425
2426 let result = extract_import_specifiers_with_crate_name(source, Some("my_crate"));
2428
2429 let entry = result.iter().find(|(spec, _)| spec == "user");
2431 assert!(entry.is_some(), "user not found in {:?}", result);
2432 let (_, symbols) = entry.unwrap();
2433 assert!(
2434 symbols.contains(&"User".to_string()),
2435 "User not in {:?}",
2436 symbols
2437 );
2438 }
2439
2440 #[test]
2444 fn rs_imp_06_crate_name_use_list() {
2445 let source = "use my_crate::user::{User, Admin};\n";
2447
2448 let result = extract_import_specifiers_with_crate_name(source, Some("my_crate"));
2450
2451 let entry = result.iter().find(|(spec, _)| spec == "user");
2453 assert!(entry.is_some(), "user not found in {:?}", result);
2454 let (_, symbols) = entry.unwrap();
2455 assert!(
2456 symbols.contains(&"User".to_string()),
2457 "User not in {:?}",
2458 symbols
2459 );
2460 assert!(
2461 symbols.contains(&"Admin".to_string()),
2462 "Admin not in {:?}",
2463 symbols
2464 );
2465 }
2466
2467 #[test]
2471 fn rs_imp_07_crate_name_none_skips() {
2472 let source = "use my_crate::user::User;\n";
2474
2475 let result = extract_import_specifiers_with_crate_name(source, None);
2477
2478 assert!(
2480 result.is_empty(),
2481 "Expected empty result when crate_name=None, got: {:?}",
2482 result
2483 );
2484 }
2485
2486 #[test]
2490 fn rs_imp_08_mixed_crate_and_crate_name() {
2491 let source = "use crate::service::Service;\nuse my_crate::user::User;\n";
2494
2495 let result = extract_import_specifiers_with_crate_name(source, Some("my_crate"));
2497
2498 let service_entry = result.iter().find(|(spec, _)| spec == "service");
2500 assert!(service_entry.is_some(), "service not found in {:?}", result);
2501 let (_, service_symbols) = service_entry.unwrap();
2502 assert!(
2503 service_symbols.contains(&"Service".to_string()),
2504 "Service not in {:?}",
2505 service_symbols
2506 );
2507
2508 let user_entry = result.iter().find(|(spec, _)| spec == "user");
2509 assert!(user_entry.is_some(), "user not found in {:?}", result);
2510 let (_, user_symbols) = user_entry.unwrap();
2511 assert!(
2512 user_symbols.contains(&"User".to_string()),
2513 "User not in {:?}",
2514 user_symbols
2515 );
2516 }
2517
2518 #[test]
2522 fn rs_l2_integ_crate_name_import_layer2() {
2523 let tmp = tempfile::tempdir().unwrap();
2528 let src_dir = tmp.path().join("src");
2529 let tests_dir = tmp.path().join("tests");
2530 std::fs::create_dir_all(&src_dir).unwrap();
2531 std::fs::create_dir_all(&tests_dir).unwrap();
2532
2533 std::fs::write(
2534 tmp.path().join("Cargo.toml"),
2535 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
2536 )
2537 .unwrap();
2538
2539 let user_rs = src_dir.join("user.rs");
2540 std::fs::write(&user_rs, "pub struct User;\n").unwrap();
2541
2542 let test_user_rs = tests_dir.join("test_user.rs");
2543 let test_source = "use my_crate::user::User;\n\n#[test]\nfn test_user() {}\n";
2544 std::fs::write(&test_user_rs, test_source).unwrap();
2545
2546 let extractor = RustExtractor::new();
2547 let prod_path = user_rs.to_string_lossy().into_owned();
2548 let test_path = test_user_rs.to_string_lossy().into_owned();
2549 let production_files = vec![prod_path.clone()];
2550 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
2551 .into_iter()
2552 .collect();
2553
2554 let result = extractor.map_test_files_with_imports(
2556 &production_files,
2557 &test_sources,
2558 tmp.path(),
2559 false,
2560 );
2561
2562 let mapping = result.iter().find(|m| m.production_file == prod_path);
2564 assert!(mapping.is_some(), "production file mapping not found");
2565 let mapping = mapping.unwrap();
2566 assert!(
2567 mapping.test_files.contains(&test_path),
2568 "Expected test_user.rs to map to user.rs via Layer 2, got: {:?}",
2569 mapping.test_files
2570 );
2571 assert_eq!(
2572 mapping.strategy,
2573 MappingStrategy::ImportTracing,
2574 "Expected ImportTracing strategy, got: {:?}",
2575 mapping.strategy
2576 );
2577 }
2578
2579 #[test]
2583 fn rs_deep_reexport_01_two_hop() {
2584 let tmp = tempfile::tempdir().unwrap();
2590 let src_models_dir = tmp.path().join("src").join("models");
2591 let tests_dir = tmp.path().join("tests");
2592 std::fs::create_dir_all(&src_models_dir).unwrap();
2593 std::fs::create_dir_all(&tests_dir).unwrap();
2594
2595 std::fs::write(
2596 tmp.path().join("Cargo.toml"),
2597 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
2598 )
2599 .unwrap();
2600
2601 let mod_rs = src_models_dir.join("mod.rs");
2602 std::fs::write(&mod_rs, "pub mod user;\n").unwrap();
2603
2604 let user_rs = src_models_dir.join("user.rs");
2605 std::fs::write(&user_rs, "pub struct User;\n").unwrap();
2606
2607 let test_models_rs = tests_dir.join("test_models.rs");
2608 let test_source = "use my_crate::models::User;\n\n#[test]\nfn test_user() {}\n";
2609 std::fs::write(&test_models_rs, test_source).unwrap();
2610
2611 let extractor = RustExtractor::new();
2612 let user_path = user_rs.to_string_lossy().into_owned();
2613 let test_path = test_models_rs.to_string_lossy().into_owned();
2614 let production_files = vec![user_path.clone()];
2615 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
2616 .into_iter()
2617 .collect();
2618
2619 let result = extractor.map_test_files_with_imports(
2621 &production_files,
2622 &test_sources,
2623 tmp.path(),
2624 false,
2625 );
2626
2627 let mapping = result.iter().find(|m| m.production_file == user_path);
2629 assert!(mapping.is_some(), "production file mapping not found");
2630 let mapping = mapping.unwrap();
2631 assert!(
2632 mapping.test_files.contains(&test_path),
2633 "Expected test_models.rs to map to user.rs via Layer 2 (pub mod chain), got: {:?}",
2634 mapping.test_files
2635 );
2636 assert_eq!(
2637 mapping.strategy,
2638 MappingStrategy::ImportTracing,
2639 "Expected ImportTracing strategy, got: {:?}",
2640 mapping.strategy
2641 );
2642 }
2643
2644 #[test]
2650 fn rs_deep_reexport_02_three_hop() {
2651 let tmp = tempfile::tempdir().unwrap();
2658 let src_dir = tmp.path().join("src");
2659 let src_models_dir = src_dir.join("models");
2660 let tests_dir = tmp.path().join("tests");
2661 std::fs::create_dir_all(&src_models_dir).unwrap();
2662 std::fs::create_dir_all(&tests_dir).unwrap();
2663
2664 std::fs::write(
2665 tmp.path().join("Cargo.toml"),
2666 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
2667 )
2668 .unwrap();
2669
2670 std::fs::write(src_dir.join("lib.rs"), "pub mod models;\n").unwrap();
2671
2672 let mod_rs = src_models_dir.join("mod.rs");
2673 std::fs::write(&mod_rs, "pub mod user;\n").unwrap();
2674
2675 let user_rs = src_models_dir.join("user.rs");
2676 std::fs::write(&user_rs, "pub struct User;\n").unwrap();
2677
2678 let test_account_rs = tests_dir.join("test_account.rs");
2680 let test_source = "use my_crate::models::User;\n\n#[test]\nfn test_account() {}\n";
2681 std::fs::write(&test_account_rs, test_source).unwrap();
2682
2683 let extractor = RustExtractor::new();
2684 let user_path = user_rs.to_string_lossy().into_owned();
2685 let test_path = test_account_rs.to_string_lossy().into_owned();
2686 let production_files = vec![user_path.clone()];
2687 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
2688 .into_iter()
2689 .collect();
2690
2691 let result = extractor.map_test_files_with_imports(
2693 &production_files,
2694 &test_sources,
2695 tmp.path(),
2696 false,
2697 );
2698
2699 let mapping = result.iter().find(|m| m.production_file == user_path);
2702 assert!(mapping.is_some(), "production file mapping not found");
2703 let mapping = mapping.unwrap();
2704 assert!(
2705 mapping.test_files.contains(&test_path),
2706 "Expected test_account.rs to map to user.rs via Layer 2 (3-hop pub mod chain), got: {:?}",
2707 mapping.test_files
2708 );
2709 assert_eq!(
2710 mapping.strategy,
2711 MappingStrategy::ImportTracing,
2712 "Expected ImportTracing strategy, got: {:?}",
2713 mapping.strategy
2714 );
2715 }
2716
2717 #[test]
2721 fn rs_deep_reexport_03_pub_use_and_pub_mod() {
2722 let source = "pub mod internal;\npub use internal::Exported;\n";
2724
2725 let extractor = RustExtractor::new();
2727 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
2728
2729 let wildcard_entry = result
2733 .iter()
2734 .find(|e| e.from_specifier == "./internal" && e.wildcard);
2735 assert!(
2736 wildcard_entry.is_some(),
2737 "Expected wildcard=true entry for pub mod internal, got: {:?}",
2738 result
2739 );
2740
2741 let symbol_entry = result.iter().find(|e| {
2742 e.from_specifier == "./internal"
2743 && !e.wildcard
2744 && e.symbols.contains(&"Exported".to_string())
2745 });
2746 assert!(
2747 symbol_entry.is_some(),
2748 "Expected symbols=[\"Exported\"] entry for pub use internal::Exported, got: {:?}",
2749 result
2750 );
2751 }
2752
2753 #[test]
2757 fn rs_export_01_pub_fn_match() {
2758 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2760 .join("../../tests/fixtures/rust/observe/exported_pub_symbols.rs");
2761 let extractor = RustExtractor::new();
2762 let symbols = vec!["create_user".to_string()];
2763
2764 let result = extractor.file_exports_any_symbol(&path, &symbols);
2766
2767 assert!(result, "Expected true for pub fn create_user");
2769 }
2770
2771 #[test]
2775 fn rs_export_02_pub_struct_match() {
2776 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2778 .join("../../tests/fixtures/rust/observe/exported_pub_symbols.rs");
2779 let extractor = RustExtractor::new();
2780 let symbols = vec!["User".to_string()];
2781
2782 let result = extractor.file_exports_any_symbol(&path, &symbols);
2784
2785 assert!(result, "Expected true for pub struct User");
2787 }
2788
2789 #[test]
2793 fn rs_export_03_nonexistent_symbol() {
2794 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2796 .join("../../tests/fixtures/rust/observe/exported_pub_symbols.rs");
2797 let extractor = RustExtractor::new();
2798 let symbols = vec!["NonExistent".to_string()];
2799
2800 let result = extractor.file_exports_any_symbol(&path, &symbols);
2802
2803 assert!(!result, "Expected false for NonExistent symbol");
2805 }
2806
2807 #[test]
2811 fn rs_export_04_no_pub_symbols() {
2812 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2814 .join("../../tests/fixtures/rust/observe/no_pub_symbols.rs");
2815 let extractor = RustExtractor::new();
2816 let symbols = vec!["internal_only".to_string()];
2817
2818 let result = extractor.file_exports_any_symbol(&path, &symbols);
2820
2821 assert!(!result, "Expected false for file with no pub symbols");
2823 }
2824
2825 #[test]
2829 fn rs_export_05_pub_use_mod_only() {
2830 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2832 .join("../../tests/fixtures/rust/observe/pub_use_only.rs");
2833 let extractor = RustExtractor::new();
2834 let symbols = vec!["Foo".to_string()];
2835
2836 let result = extractor.file_exports_any_symbol(&path, &symbols);
2838
2839 assert!(
2841 !result,
2842 "Expected false for pub use/mod only file (barrel resolution handles these)"
2843 );
2844 }
2845
2846 #[test]
2850 fn rs_export_06_empty_symbols() {
2851 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2853 .join("../../tests/fixtures/rust/observe/exported_pub_symbols.rs");
2854 let extractor = RustExtractor::new();
2855 let symbols: Vec<String> = vec![];
2856
2857 let result = extractor.file_exports_any_symbol(&path, &symbols);
2859
2860 assert!(result, "Expected true for empty symbol list");
2862 }
2863
2864 #[test]
2868 fn rs_export_07_nonexistent_file() {
2869 let path = PathBuf::from("/nonexistent/path/to/file.rs");
2871 let extractor = RustExtractor::new();
2872 let symbols = vec!["Foo".to_string()];
2873
2874 let result = extractor.file_exports_any_symbol(&path, &symbols);
2877 assert!(
2878 result,
2879 "Expected true for non-existent file (optimistic fallback)"
2880 );
2881 }
2882
2883 #[test]
2887 fn rs_export_pub_only_01_pub_fn_matches() {
2888 let dir = tempfile::tempdir().unwrap();
2890 let file = dir.path().join("service.rs");
2891 std::fs::write(&file, "pub fn create_user() {}").unwrap();
2892 let extractor = RustExtractor::new();
2893 let symbols = vec!["create_user".to_string()];
2894
2895 let result = extractor.file_exports_any_symbol(&file, &symbols);
2897
2898 assert!(result, "Expected true for pub fn create_user");
2900 }
2901
2902 #[test]
2906 fn rs_export_pub_only_02_pub_crate_excluded() {
2907 let dir = tempfile::tempdir().unwrap();
2909 let file = dir.path().join("driver.rs");
2910 std::fs::write(&file, "pub(crate) struct Handle {}").unwrap();
2911 let extractor = RustExtractor::new();
2912 let symbols = vec!["Handle".to_string()];
2913
2914 let result = extractor.file_exports_any_symbol(&file, &symbols);
2916
2917 assert!(!result, "Expected false for pub(crate) struct Handle");
2919 }
2920
2921 #[test]
2925 fn rs_export_pub_only_03_pub_super_excluded() {
2926 let dir = tempfile::tempdir().unwrap();
2928 let file = dir.path().join("internal.rs");
2929 std::fs::write(&file, "pub(super) fn helper() {}").unwrap();
2930 let extractor = RustExtractor::new();
2931 let symbols = vec!["helper".to_string()];
2932
2933 let result = extractor.file_exports_any_symbol(&file, &symbols);
2935
2936 assert!(!result, "Expected false for pub(super) fn helper");
2938 }
2939
2940 #[test]
2944 fn rs_export_pub_only_04_mixed_visibility() {
2945 let dir = tempfile::tempdir().unwrap();
2947 let file = dir.path().join("models.rs");
2948 std::fs::write(&file, "pub struct User {}\npub(crate) struct Inner {}").unwrap();
2949 let extractor = RustExtractor::new();
2950 let symbols = vec!["User".to_string()];
2951
2952 let result = extractor.file_exports_any_symbol(&file, &symbols);
2954
2955 assert!(
2957 result,
2958 "Expected true for pub struct User in mixed visibility file"
2959 );
2960 }
2961
2962 #[test]
2966 fn rs_ws_01_workspace_two_members() {
2967 let tmp = tempfile::tempdir().unwrap();
2969 std::fs::write(
2970 tmp.path().join("Cargo.toml"),
2971 "[workspace]\nmembers = [\"crate_a\", \"crate_b\"]\n",
2972 )
2973 .unwrap();
2974 std::fs::create_dir_all(tmp.path().join("crate_a/src")).unwrap();
2975 std::fs::write(
2976 tmp.path().join("crate_a/Cargo.toml"),
2977 "[package]\nname = \"crate_a\"\nversion = \"0.1.0\"\n",
2978 )
2979 .unwrap();
2980 std::fs::create_dir_all(tmp.path().join("crate_b/src")).unwrap();
2981 std::fs::write(
2982 tmp.path().join("crate_b/Cargo.toml"),
2983 "[package]\nname = \"crate_b\"\nversion = \"0.1.0\"\n",
2984 )
2985 .unwrap();
2986
2987 let members = find_workspace_members(tmp.path());
2989
2990 assert_eq!(members.len(), 2, "Expected 2 members, got: {:?}", members);
2992 let names: Vec<&str> = members.iter().map(|m| m.crate_name.as_str()).collect();
2993 assert!(
2994 names.contains(&"crate_a"),
2995 "crate_a not found in {:?}",
2996 names
2997 );
2998 assert!(
2999 names.contains(&"crate_b"),
3000 "crate_b not found in {:?}",
3001 names
3002 );
3003 }
3004
3005 #[test]
3009 fn rs_ws_02_single_crate_returns_empty() {
3010 let tmp = tempfile::tempdir().unwrap();
3012 std::fs::write(
3013 tmp.path().join("Cargo.toml"),
3014 "[package]\nname = \"my_crate\"\nversion = \"0.1.0\"\n",
3015 )
3016 .unwrap();
3017 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
3018
3019 let members = find_workspace_members(tmp.path());
3021
3022 assert!(members.is_empty(), "Expected empty, got: {:?}", members);
3024 }
3025
3026 #[test]
3030 fn rs_ws_03_target_dir_skipped() {
3031 let tmp = tempfile::tempdir().unwrap();
3033 std::fs::write(
3034 tmp.path().join("Cargo.toml"),
3035 "[workspace]\nmembers = [\"crate_a\"]\n",
3036 )
3037 .unwrap();
3038 std::fs::create_dir_all(tmp.path().join("crate_a/src")).unwrap();
3039 std::fs::write(
3040 tmp.path().join("crate_a/Cargo.toml"),
3041 "[package]\nname = \"crate_a\"\nversion = \"0.1.0\"\n",
3042 )
3043 .unwrap();
3044 std::fs::create_dir_all(tmp.path().join("target/debug/build/fake")).unwrap();
3046 std::fs::write(
3047 tmp.path().join("target/debug/build/fake/Cargo.toml"),
3048 "[package]\nname = \"fake_crate\"\nversion = \"0.1.0\"\n",
3049 )
3050 .unwrap();
3051
3052 let members = find_workspace_members(tmp.path());
3054
3055 assert_eq!(members.len(), 1, "Expected 1 member, got: {:?}", members);
3057 assert_eq!(members[0].crate_name, "crate_a");
3058 }
3059
3060 #[test]
3064 fn rs_ws_04_hyphenated_crate_name_converted() {
3065 let tmp = tempfile::tempdir().unwrap();
3067 std::fs::write(
3068 tmp.path().join("Cargo.toml"),
3069 "[workspace]\nmembers = [\"my-crate\"]\n",
3070 )
3071 .unwrap();
3072 std::fs::create_dir_all(tmp.path().join("my-crate/src")).unwrap();
3073 std::fs::write(
3074 tmp.path().join("my-crate/Cargo.toml"),
3075 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\n",
3076 )
3077 .unwrap();
3078
3079 let members = find_workspace_members(tmp.path());
3081
3082 assert_eq!(members.len(), 1, "Expected 1 member, got: {:?}", members);
3084 assert_eq!(members[0].crate_name, "my_crate");
3085 }
3086
3087 #[test]
3091 fn rs_ws_05_find_member_for_path_in_tests() {
3092 let tmp = tempfile::tempdir().unwrap();
3094 let member_root = tmp.path().join("crate_a");
3095 std::fs::create_dir_all(&member_root).unwrap();
3096 let members = vec![WorkspaceMember {
3097 crate_name: "crate_a".to_string(),
3098 member_root: member_root.clone(),
3099 }];
3100
3101 let test_file = member_root.join("tests").join("integration.rs");
3103 let result = find_member_for_path(&test_file, &members);
3104
3105 assert!(result.is_some(), "Expected Some(crate_a), got None");
3107 assert_eq!(result.unwrap().crate_name, "crate_a");
3108 }
3109
3110 #[test]
3114 fn rs_ws_06_find_member_for_path_not_in_any() {
3115 let tmp = tempfile::tempdir().unwrap();
3117 let member_root = tmp.path().join("crate_a");
3118 std::fs::create_dir_all(&member_root).unwrap();
3119 let members = vec![WorkspaceMember {
3120 crate_name: "crate_a".to_string(),
3121 member_root: member_root.clone(),
3122 }];
3123
3124 let outside_path = tmp.path().join("other").join("test.rs");
3126 let result = find_member_for_path(&outside_path, &members);
3127
3128 assert!(
3130 result.is_none(),
3131 "Expected None, got: {:?}",
3132 result.map(|m| &m.crate_name)
3133 );
3134 }
3135
3136 #[test]
3140 fn rs_ws_07_find_member_longest_prefix() {
3141 let tmp = tempfile::tempdir().unwrap();
3143 let foo_root = tmp.path().join("crates").join("foo");
3144 let foo_extra_root = tmp.path().join("crates").join("foo-extra");
3145 std::fs::create_dir_all(&foo_root).unwrap();
3146 std::fs::create_dir_all(&foo_extra_root).unwrap();
3147 let members = vec![
3148 WorkspaceMember {
3149 crate_name: "foo".to_string(),
3150 member_root: foo_root.clone(),
3151 },
3152 WorkspaceMember {
3153 crate_name: "foo_extra".to_string(),
3154 member_root: foo_extra_root.clone(),
3155 },
3156 ];
3157
3158 let test_file = foo_extra_root.join("tests").join("test_bar.rs");
3160 let result = find_member_for_path(&test_file, &members);
3161
3162 assert!(result.is_some(), "Expected Some(foo_extra), got None");
3164 assert_eq!(result.unwrap().crate_name, "foo_extra");
3165 }
3166
3167 #[test]
3171 fn rs_ws_e2e_01_workspace_l2_import_tracing() {
3172 let tmp = tempfile::tempdir().unwrap();
3175 std::fs::write(
3176 tmp.path().join("Cargo.toml"),
3177 "[workspace]\nmembers = [\"crate_a\"]\n",
3178 )
3179 .unwrap();
3180
3181 let member_dir = tmp.path().join("crate_a");
3182 std::fs::create_dir_all(member_dir.join("src")).unwrap();
3183 std::fs::create_dir_all(member_dir.join("tests")).unwrap();
3184 std::fs::write(
3185 member_dir.join("Cargo.toml"),
3186 "[package]\nname = \"crate_a\"\nversion = \"0.1.0\"\n",
3187 )
3188 .unwrap();
3189
3190 let user_rs = member_dir.join("src").join("user.rs");
3191 std::fs::write(&user_rs, "pub fn create_user() {}\n").unwrap();
3192
3193 let test_rs = member_dir.join("tests").join("test_user.rs");
3194 std::fs::write(
3195 &test_rs,
3196 "use crate_a::user::create_user;\n#[test]\nfn test_create_user() { create_user(); }\n",
3197 )
3198 .unwrap();
3199
3200 let extractor = RustExtractor::new();
3201 let prod_path = user_rs.to_string_lossy().into_owned();
3202 let test_path = test_rs.to_string_lossy().into_owned();
3203 let production_files = vec![prod_path.clone()];
3204 let test_sources: HashMap<String, String> = [(
3205 test_path.clone(),
3206 std::fs::read_to_string(&test_rs).unwrap(),
3207 )]
3208 .into_iter()
3209 .collect();
3210
3211 let result = extractor.map_test_files_with_imports(
3213 &production_files,
3214 &test_sources,
3215 tmp.path(),
3216 false,
3217 );
3218
3219 let mapping = result.iter().find(|m| m.production_file == prod_path);
3221 assert!(mapping.is_some(), "No mapping for user.rs");
3222 let mapping = mapping.unwrap();
3223 assert!(
3224 mapping.test_files.contains(&test_path),
3225 "Expected test_user.rs in test_files, got: {:?}",
3226 mapping.test_files
3227 );
3228 assert_eq!(
3229 mapping.strategy,
3230 MappingStrategy::ImportTracing,
3231 "Expected ImportTracing strategy, got: {:?}",
3232 mapping.strategy
3233 );
3234 }
3235
3236 #[test]
3246 fn rs_ws_e2e_02_l0_l1_still_work_at_workspace_level() {
3247 let tmp = tempfile::tempdir().unwrap();
3250 std::fs::write(
3251 tmp.path().join("Cargo.toml"),
3252 "[workspace]\nmembers = [\"crate_a\"]\n",
3253 )
3254 .unwrap();
3255
3256 let member_dir = tmp.path().join("crate_a");
3257 std::fs::create_dir_all(member_dir.join("src")).unwrap();
3258 std::fs::write(
3259 member_dir.join("Cargo.toml"),
3260 "[package]\nname = \"crate_a\"\nversion = \"0.1.0\"\n",
3261 )
3262 .unwrap();
3263
3264 let service_rs = member_dir.join("src").join("service.rs");
3266 std::fs::write(
3267 &service_rs,
3268 r#"pub fn do_work() {}
3269
3270#[cfg(test)]
3271mod tests {
3272 use super::*;
3273 #[test]
3274 fn test_do_work() { do_work(); }
3275}
3276"#,
3277 )
3278 .unwrap();
3279
3280 let test_service_rs = member_dir.join("src").join("test_service.rs");
3282 std::fs::write(
3283 &test_service_rs,
3284 "#[test]\nfn test_service_smoke() { assert!(true); }\n",
3285 )
3286 .unwrap();
3287
3288 let extractor = RustExtractor::new();
3289 let prod_path = service_rs.to_string_lossy().into_owned();
3290 let test_path = test_service_rs.to_string_lossy().into_owned();
3291 let production_files = vec![prod_path.clone()];
3292 let test_sources: HashMap<String, String> = [(
3293 test_path.clone(),
3294 std::fs::read_to_string(&test_service_rs).unwrap(),
3295 )]
3296 .into_iter()
3297 .collect();
3298
3299 let result = extractor.map_test_files_with_imports(
3301 &production_files,
3302 &test_sources,
3303 tmp.path(),
3304 false,
3305 );
3306
3307 let mapping = result.iter().find(|m| m.production_file == prod_path);
3309 assert!(mapping.is_some(), "No mapping for service.rs");
3310 let mapping = mapping.unwrap();
3311 assert!(
3312 mapping.test_files.contains(&prod_path),
3313 "Expected service.rs self-mapped (Layer 0), got: {:?}",
3314 mapping.test_files
3315 );
3316 assert!(
3317 mapping.test_files.contains(&test_path),
3318 "Expected test_service.rs mapped (Layer 1), got: {:?}",
3319 mapping.test_files
3320 );
3321 }
3322
3323 #[test]
3330 fn rs_ws_e2e_03_non_virtual_workspace_l2() {
3331 let tmp = tempfile::tempdir().unwrap();
3334 std::fs::write(
3335 tmp.path().join("Cargo.toml"),
3336 "[workspace]\nmembers = [\"member_a\"]\n\n[package]\nname = \"root_pkg\"\nversion = \"0.1.0\"\n",
3337 )
3338 .unwrap();
3339
3340 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
3342 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
3343 let root_src = tmp.path().join("src").join("lib.rs");
3344 std::fs::write(&root_src, "pub fn root_fn() {}\n").unwrap();
3345 let root_test = tmp.path().join("tests").join("test_root.rs");
3346 std::fs::write(
3347 &root_test,
3348 "use root_pkg::lib::root_fn;\n#[test]\nfn test_root() { }\n",
3349 )
3350 .unwrap();
3351
3352 let member_dir = tmp.path().join("member_a");
3354 std::fs::create_dir_all(member_dir.join("src")).unwrap();
3355 std::fs::create_dir_all(member_dir.join("tests")).unwrap();
3356 std::fs::write(
3357 member_dir.join("Cargo.toml"),
3358 "[package]\nname = \"member_a\"\nversion = \"0.1.0\"\n",
3359 )
3360 .unwrap();
3361 let member_src = member_dir.join("src").join("handler.rs");
3362 std::fs::write(&member_src, "pub fn handle() {}\n").unwrap();
3363 let member_test = member_dir.join("tests").join("test_handler.rs");
3364 std::fs::write(
3365 &member_test,
3366 "use member_a::handler::handle;\n#[test]\nfn test_handle() { handle(); }\n",
3367 )
3368 .unwrap();
3369
3370 let extractor = RustExtractor::new();
3371 let root_src_path = root_src.to_string_lossy().into_owned();
3372 let member_src_path = member_src.to_string_lossy().into_owned();
3373 let root_test_path = root_test.to_string_lossy().into_owned();
3374 let member_test_path = member_test.to_string_lossy().into_owned();
3375
3376 let production_files = vec![root_src_path.clone(), member_src_path.clone()];
3377 let test_sources: HashMap<String, String> = [
3378 (
3379 root_test_path.clone(),
3380 std::fs::read_to_string(&root_test).unwrap(),
3381 ),
3382 (
3383 member_test_path.clone(),
3384 std::fs::read_to_string(&member_test).unwrap(),
3385 ),
3386 ]
3387 .into_iter()
3388 .collect();
3389
3390 let result = extractor.map_test_files_with_imports(
3392 &production_files,
3393 &test_sources,
3394 tmp.path(),
3395 false,
3396 );
3397
3398 let member_mapping = result.iter().find(|m| m.production_file == member_src_path);
3400 assert!(member_mapping.is_some(), "No mapping for member handler.rs");
3401 let member_mapping = member_mapping.unwrap();
3402 assert!(
3403 member_mapping.test_files.contains(&member_test_path),
3404 "Expected member test mapped via L2, got: {:?}",
3405 member_mapping.test_files
3406 );
3407 assert_eq!(
3408 member_mapping.strategy,
3409 MappingStrategy::ImportTracing,
3410 "Expected ImportTracing for member, got: {:?}",
3411 member_mapping.strategy
3412 );
3413 }
3414
3415 #[test]
3419 fn rs_ws_08_has_workspace_section() {
3420 let tmp = tempfile::tempdir().unwrap();
3421
3422 std::fs::write(
3424 tmp.path().join("Cargo.toml"),
3425 "[workspace]\nmembers = [\"a\"]\n",
3426 )
3427 .unwrap();
3428 assert!(has_workspace_section(tmp.path()));
3429
3430 std::fs::write(
3432 tmp.path().join("Cargo.toml"),
3433 "[workspace]\nmembers = [\"a\"]\n\n[package]\nname = \"root\"\n",
3434 )
3435 .unwrap();
3436 assert!(has_workspace_section(tmp.path()));
3437
3438 std::fs::write(
3440 tmp.path().join("Cargo.toml"),
3441 "[package]\nname = \"single\"\n",
3442 )
3443 .unwrap();
3444 assert!(!has_workspace_section(tmp.path()));
3445
3446 std::fs::remove_file(tmp.path().join("Cargo.toml")).unwrap();
3448 assert!(!has_workspace_section(tmp.path()));
3449 }
3450
3451 #[test]
3455 fn rs_l0_barrel_01_mod_rs_excluded() {
3456 let tmp = tempfile::tempdir().unwrap();
3458 let src_dir = tmp.path().join("src");
3459 std::fs::create_dir_all(&src_dir).unwrap();
3460
3461 let mod_rs = src_dir.join("mod.rs");
3462 std::fs::write(
3463 &mod_rs,
3464 r#"pub mod sub;
3465
3466#[cfg(test)]
3467mod tests {
3468 #[test]
3469 fn test_something() {}
3470}
3471"#,
3472 )
3473 .unwrap();
3474
3475 let extractor = RustExtractor::new();
3476 let prod_path = mod_rs.to_string_lossy().into_owned();
3477 let production_files = vec![prod_path.clone()];
3478 let test_sources: HashMap<String, String> = HashMap::new();
3479
3480 let result = extractor.map_test_files_with_imports(
3482 &production_files,
3483 &test_sources,
3484 tmp.path(),
3485 false,
3486 );
3487
3488 let mapping = result.iter().find(|m| m.production_file == prod_path);
3490 assert!(mapping.is_some());
3491 assert!(
3492 !mapping.unwrap().test_files.contains(&prod_path),
3493 "mod.rs should NOT be self-mapped, but found in: {:?}",
3494 mapping.unwrap().test_files
3495 );
3496 }
3497
3498 #[test]
3502 fn rs_l0_barrel_02_lib_rs_excluded() {
3503 let tmp = tempfile::tempdir().unwrap();
3505 let src_dir = tmp.path().join("src");
3506 std::fs::create_dir_all(&src_dir).unwrap();
3507
3508 let lib_rs = src_dir.join("lib.rs");
3509 std::fs::write(
3510 &lib_rs,
3511 r#"pub mod utils;
3512
3513#[cfg(test)]
3514mod tests {
3515 #[test]
3516 fn test_lib() {}
3517}
3518"#,
3519 )
3520 .unwrap();
3521
3522 let extractor = RustExtractor::new();
3523 let prod_path = lib_rs.to_string_lossy().into_owned();
3524 let production_files = vec![prod_path.clone()];
3525 let test_sources: HashMap<String, String> = HashMap::new();
3526
3527 let result = extractor.map_test_files_with_imports(
3529 &production_files,
3530 &test_sources,
3531 tmp.path(),
3532 false,
3533 );
3534
3535 let mapping = result.iter().find(|m| m.production_file == prod_path);
3537 assert!(mapping.is_some());
3538 assert!(
3539 !mapping.unwrap().test_files.contains(&prod_path),
3540 "lib.rs should NOT be self-mapped, but found in: {:?}",
3541 mapping.unwrap().test_files
3542 );
3543 }
3544
3545 #[test]
3549 fn rs_l0_barrel_03_regular_file_self_mapped() {
3550 let tmp = tempfile::tempdir().unwrap();
3552 let src_dir = tmp.path().join("src");
3553 std::fs::create_dir_all(&src_dir).unwrap();
3554
3555 let service_rs = src_dir.join("service.rs");
3556 std::fs::write(
3557 &service_rs,
3558 r#"pub fn do_work() {}
3559
3560#[cfg(test)]
3561mod tests {
3562 use super::*;
3563 #[test]
3564 fn test_do_work() { assert!(true); }
3565}
3566"#,
3567 )
3568 .unwrap();
3569
3570 let extractor = RustExtractor::new();
3571 let prod_path = service_rs.to_string_lossy().into_owned();
3572 let production_files = vec![prod_path.clone()];
3573 let test_sources: HashMap<String, String> = HashMap::new();
3574
3575 let result = extractor.map_test_files_with_imports(
3577 &production_files,
3578 &test_sources,
3579 tmp.path(),
3580 false,
3581 );
3582
3583 let mapping = result.iter().find(|m| m.production_file == prod_path);
3585 assert!(mapping.is_some());
3586 assert!(
3587 mapping.unwrap().test_files.contains(&prod_path),
3588 "service.rs should be self-mapped, but not found in: {:?}",
3589 mapping.unwrap().test_files
3590 );
3591 }
3592
3593 #[test]
3597 fn rs_l0_barrel_04_main_rs_excluded() {
3598 let tmp = tempfile::tempdir().unwrap();
3600 let src_dir = tmp.path().join("src");
3601 std::fs::create_dir_all(&src_dir).unwrap();
3602
3603 let main_rs = src_dir.join("main.rs");
3604 std::fs::write(
3605 &main_rs,
3606 r#"fn main() {}
3607
3608#[cfg(test)]
3609mod tests {
3610 #[test]
3611 fn test_main() {}
3612}
3613"#,
3614 )
3615 .unwrap();
3616
3617 let extractor = RustExtractor::new();
3618 let prod_path = main_rs.to_string_lossy().into_owned();
3619 let production_files = vec![prod_path.clone()];
3620 let test_sources: HashMap<String, String> = HashMap::new();
3621
3622 let result = extractor.map_test_files_with_imports(
3624 &production_files,
3625 &test_sources,
3626 tmp.path(),
3627 false,
3628 );
3629
3630 let mapping = result.iter().find(|m| m.production_file == prod_path);
3632 assert!(mapping.is_some());
3633 assert!(
3634 !mapping.unwrap().test_files.contains(&prod_path),
3635 "main.rs should NOT be self-mapped, but found in: {:?}",
3636 mapping.unwrap().test_files
3637 );
3638 }
3639
3640 #[test]
3645 fn rs_l0_detect_01_cfg_test_with_mod_block() {
3646 let source = r#"
3648pub fn add(a: i32, b: i32) -> i32 { a + b }
3649
3650#[cfg(test)]
3651mod tests {
3652 use super::*;
3653
3654 #[test]
3655 fn test_add() {
3656 assert_eq!(add(1, 2), 3);
3657 }
3658}
3659"#;
3660 assert!(detect_inline_tests(source));
3663 }
3664
3665 #[test]
3669 fn rs_l0_detect_02_cfg_test_for_helper_method() {
3670 let source = r#"
3672pub struct Connection;
3673
3674impl Connection {
3675 #[cfg(test)]
3676 pub fn test_helper(&self) -> bool {
3677 true
3678 }
3679}
3680"#;
3681 assert!(!detect_inline_tests(source));
3684 }
3685
3686 #[test]
3690 fn rs_l0_detect_03_cfg_test_for_use_statement() {
3691 let source = r#"
3693#[cfg(not(test))]
3694use real_http::Client;
3695
3696#[cfg(test)]
3697use mock_http::Client;
3698
3699pub fn fetch(url: &str) -> String {
3700 Client::get(url)
3701}
3702"#;
3703 assert!(!detect_inline_tests(source));
3706 }
3707
3708 #[test]
3713 fn rs_l0_detect_04_cfg_test_with_external_mod_ref() {
3714 let source = r#"
3716pub fn compute(x: i32) -> i32 { x * 2 }
3717
3718#[cfg(test)]
3719mod tests;
3720"#;
3721 assert!(detect_inline_tests(source));
3724 }
3725
3726 #[test]
3736 fn rs_l2_export_filter_01_no_export_not_mapped() {
3737 let tmp = tempfile::tempdir().unwrap();
3741 let src_runtime = tmp.path().join("src").join("runtime");
3742 let tests_dir = tmp.path().join("tests");
3743 std::fs::create_dir_all(&src_runtime).unwrap();
3744 std::fs::create_dir_all(&tests_dir).unwrap();
3745
3746 std::fs::write(
3748 tmp.path().join("Cargo.toml"),
3749 "[package]\nname = \"myapp\"\nversion = \"0.1.0\"\n",
3750 )
3751 .unwrap();
3752
3753 let driver_rs = src_runtime.join("driver.rs");
3755 std::fs::write(&driver_rs, "pub fn spawn() {}\npub struct Driver;\n").unwrap();
3756
3757 let test_rs = tests_dir.join("test_runtime.rs");
3760 let test_source = "use myapp::runtime::driver::{Builder};\n\n#[test]\nfn test_build() {}\n";
3761 std::fs::write(&test_rs, test_source).unwrap();
3762
3763 let extractor = RustExtractor::new();
3764 let driver_path = driver_rs.to_string_lossy().into_owned();
3765 let test_path = test_rs.to_string_lossy().into_owned();
3766 let production_files = vec![driver_path.clone()];
3767 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
3768 .into_iter()
3769 .collect();
3770
3771 let result = extractor.map_test_files_with_imports(
3773 &production_files,
3774 &test_sources,
3775 tmp.path(),
3776 false,
3777 );
3778
3779 let mapping = result.iter().find(|m| m.production_file == driver_path);
3782 if let Some(m) = mapping {
3783 assert!(
3784 !m.test_files.contains(&test_path),
3785 "driver.rs should NOT be mapped (does not export Builder), but found: {:?}",
3786 m.test_files
3787 );
3788 }
3789 }
3791
3792 #[test]
3798 fn rs_l2_export_filter_02_exports_symbol_is_mapped() {
3799 let tmp = tempfile::tempdir().unwrap();
3804 let src_app = tmp.path().join("src").join("app");
3805 let tests_dir = tmp.path().join("tests");
3806 std::fs::create_dir_all(&src_app).unwrap();
3807 std::fs::create_dir_all(&tests_dir).unwrap();
3808
3809 std::fs::write(
3811 tmp.path().join("Cargo.toml"),
3812 "[package]\nname = \"myapp\"\nversion = \"0.1.0\"\n",
3813 )
3814 .unwrap();
3815
3816 let mod_rs = src_app.join("mod.rs");
3818 std::fs::write(&mod_rs, "pub mod service;\n").unwrap();
3819
3820 let service_rs = src_app.join("service.rs");
3822 std::fs::write(&service_rs, "pub fn service_fn() {}\n").unwrap();
3823
3824 let test_rs = tests_dir.join("test_app.rs");
3826 let test_source = "use myapp::app::{service_fn};\n\n#[test]\nfn test_service() {}\n";
3827 std::fs::write(&test_rs, test_source).unwrap();
3828
3829 let extractor = RustExtractor::new();
3830 let service_path = service_rs.to_string_lossy().into_owned();
3831 let test_path = test_rs.to_string_lossy().into_owned();
3832 let production_files = vec![service_path.clone()];
3833 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
3834 .into_iter()
3835 .collect();
3836
3837 let result = extractor.map_test_files_with_imports(
3839 &production_files,
3840 &test_sources,
3841 tmp.path(),
3842 false,
3843 );
3844
3845 let mapping = result.iter().find(|m| m.production_file == service_path);
3848 assert!(mapping.is_some(), "service.rs should have a mapping entry");
3849 assert!(
3850 mapping.unwrap().test_files.contains(&test_path),
3851 "service.rs should be mapped to test_app.rs, got: {:?}",
3852 mapping.unwrap().test_files
3853 );
3854 }
3855
3856 #[test]
3860 fn rs_barrel_cfg_macro_pub_mod() {
3861 let source = r#"
3863cfg_feat! {
3864 pub mod sub;
3865}
3866"#;
3867
3868 let ext = RustExtractor::new();
3870 let result = ext.extract_barrel_re_exports(source, "src/mod.rs");
3871
3872 assert!(
3874 !result.is_empty(),
3875 "Expected non-empty result, got: {:?}",
3876 result
3877 );
3878 assert!(
3879 result
3880 .iter()
3881 .any(|r| r.from_specifier == "./sub" && r.wildcard),
3882 "./sub with wildcard=true not found in {:?}",
3883 result
3884 );
3885 }
3886
3887 #[test]
3891 fn rs_barrel_cfg_macro_pub_use_braces() {
3892 let source = r#"
3894cfg_feat! {
3895 pub use util::{Symbol};
3896}
3897"#;
3898
3899 let ext = RustExtractor::new();
3901 let result = ext.extract_barrel_re_exports(source, "src/mod.rs");
3902
3903 assert!(
3905 !result.is_empty(),
3906 "Expected non-empty result, got: {:?}",
3907 result
3908 );
3909 assert!(
3910 result.iter().any(|r| r.from_specifier == "./util"
3911 && !r.wildcard
3912 && r.symbols.contains(&"Symbol".to_string())),
3913 "./util with symbols=[\"Symbol\"] not found in {:?}",
3914 result
3915 );
3916 }
3917
3918 #[test]
3922 fn rs_barrel_top_level_regression() {
3923 let source = "pub mod foo;\n";
3925
3926 let ext = RustExtractor::new();
3928 let result = ext.extract_barrel_re_exports(source, "src/mod.rs");
3929
3930 let entry = result.iter().find(|e| e.from_specifier == "./foo");
3932 assert!(
3933 entry.is_some(),
3934 "./foo not found in {:?} (regression: top-level pub mod broken)",
3935 result
3936 );
3937 assert!(entry.unwrap().wildcard);
3938 }
3939
3940 #[test]
3944 fn rs_imp_09_single_segment_module_import() {
3945 let source = "use crate::fs;\n";
3947
3948 let extractor = RustExtractor::new();
3950 let result = extractor.extract_all_import_specifiers(source);
3951
3952 let entry = result.iter().find(|(spec, _)| spec == "fs");
3954 assert!(
3955 entry.is_some(),
3956 "fs not found in {:?} (single-segment module import should be registered)",
3957 result
3958 );
3959 let (_, symbols) = entry.unwrap();
3960 assert!(
3961 symbols.is_empty(),
3962 "Expected empty symbols for module import, got: {:?}",
3963 symbols
3964 );
3965 }
3966
3967 #[test]
3971 fn rs_imp_10_single_segment_with_crate_name() {
3972 let source = "use my_crate::util;\n";
3974
3975 let result = extract_import_specifiers_with_crate_name(source, Some("my_crate"));
3977
3978 let entry = result.iter().find(|(spec, _)| spec == "util");
3980 assert!(
3981 entry.is_some(),
3982 "util not found in {:?} (single-segment with crate_name should be registered)",
3983 result
3984 );
3985 let (_, symbols) = entry.unwrap();
3986 assert!(
3987 symbols.is_empty(),
3988 "Expected empty symbols for module import, got: {:?}",
3989 symbols
3990 );
3991 }
3992
3993 #[test]
3997 fn rs_barrel_self_01_strips_self_from_wildcard() {
3998 let source = "pub use self::sub::*;\n";
4000
4001 let extractor = RustExtractor::new();
4003 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
4004
4005 let entry = result.iter().find(|e| e.from_specifier == "./sub");
4007 assert!(
4008 entry.is_some(),
4009 "./sub not found in {:?} (self:: prefix should be stripped from wildcard)",
4010 result
4011 );
4012 assert!(
4013 entry.unwrap().wildcard,
4014 "Expected wildcard=true for pub use self::sub::*"
4015 );
4016 }
4017
4018 #[test]
4022 fn rs_barrel_self_02_strips_self_from_symbol() {
4023 let source = "pub use self::file::File;\n";
4025
4026 let extractor = RustExtractor::new();
4028 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
4029
4030 let entry = result.iter().find(|e| e.from_specifier == "./file");
4032 assert!(
4033 entry.is_some(),
4034 "./file not found in {:?} (self:: prefix should be stripped from symbol import)",
4035 result
4036 );
4037 let entry = entry.unwrap();
4038 assert!(
4039 entry.symbols.contains(&"File".to_string()),
4040 "Expected symbols=[\"File\"], got: {:?}",
4041 entry.symbols
4042 );
4043 }
4044
4045 #[test]
4049 fn rs_barrel_self_03_strips_self_from_use_list() {
4050 let source = "pub use self::sync::{Mutex, RwLock};\n";
4052
4053 let extractor = RustExtractor::new();
4055 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
4056
4057 let entry = result.iter().find(|e| e.from_specifier == "./sync");
4059 assert!(
4060 entry.is_some(),
4061 "./sync not found in {:?} (self:: prefix should be stripped from use list)",
4062 result
4063 );
4064 let entry = entry.unwrap();
4065 assert!(
4066 entry.symbols.contains(&"Mutex".to_string()),
4067 "Expected Mutex in symbols, got: {:?}",
4068 entry.symbols
4069 );
4070 assert!(
4071 entry.symbols.contains(&"RwLock".to_string()),
4072 "Expected RwLock in symbols, got: {:?}",
4073 entry.symbols
4074 );
4075 }
4076
4077 #[test]
4081 fn rs_barrel_cfg_self_01_strips_self_in_cfg_macro() {
4082 let source = "cfg_feat! { pub use self::inner::Symbol; }\n";
4084
4085 let extractor = RustExtractor::new();
4087 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
4088
4089 let entry = result.iter().find(|e| e.from_specifier == "./inner");
4091 assert!(
4092 entry.is_some(),
4093 "./inner not found in {:?} (self:: prefix should be stripped in cfg macro text path)",
4094 result
4095 );
4096 let entry = entry.unwrap();
4097 assert!(
4098 entry.symbols.contains(&"Symbol".to_string()),
4099 "Expected symbols=[\"Symbol\"], got: {:?}",
4100 entry.symbols
4101 );
4102 }
4103
4104 #[test]
4108 fn rs_l2_self_barrel_e2e_resolves_through_self_barrel() {
4109 let tmp = tempfile::tempdir().unwrap();
4114 let src_fs_dir = tmp.path().join("src").join("fs");
4115 let tests_dir = tmp.path().join("tests");
4116 std::fs::create_dir_all(&src_fs_dir).unwrap();
4117 std::fs::create_dir_all(&tests_dir).unwrap();
4118
4119 std::fs::write(
4120 tmp.path().join("Cargo.toml"),
4121 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
4122 )
4123 .unwrap();
4124
4125 let mod_rs = src_fs_dir.join("mod.rs");
4126 std::fs::write(&mod_rs, "pub use self::file::File;\n").unwrap();
4127
4128 let file_rs = src_fs_dir.join("file.rs");
4129 std::fs::write(&file_rs, "pub struct File;\n").unwrap();
4130
4131 let test_fs_rs = tests_dir.join("test_fs.rs");
4132 let test_source = "use my_crate::fs::File;\n\n#[test]\nfn test_fs() {}\n";
4133 std::fs::write(&test_fs_rs, test_source).unwrap();
4134
4135 let extractor = RustExtractor::new();
4136 let file_path = file_rs.to_string_lossy().into_owned();
4137 let test_path = test_fs_rs.to_string_lossy().into_owned();
4138 let production_files = vec![file_path.clone()];
4139 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
4140 .into_iter()
4141 .collect();
4142
4143 let result = extractor.map_test_files_with_imports(
4145 &production_files,
4146 &test_sources,
4147 tmp.path(),
4148 false,
4149 );
4150
4151 let mapping = result.iter().find(|m| m.production_file == file_path);
4153 assert!(mapping.is_some(), "No mapping for src/fs/file.rs");
4154 let mapping = mapping.unwrap();
4155 assert!(
4156 mapping.test_files.contains(&test_path),
4157 "Expected test_fs.rs to map to file.rs through self:: barrel (L2), got: {:?}",
4158 mapping.test_files
4159 );
4160 }
4161
4162 #[test]
4166 fn rs_l2_single_seg_e2e_resolves_single_segment_module() {
4167 let tmp = tempfile::tempdir().unwrap();
4172 let src_fs_dir = tmp.path().join("src").join("fs");
4173 let tests_dir = tmp.path().join("tests");
4174 std::fs::create_dir_all(&src_fs_dir).unwrap();
4175 std::fs::create_dir_all(&tests_dir).unwrap();
4176
4177 std::fs::write(
4178 tmp.path().join("Cargo.toml"),
4179 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
4180 )
4181 .unwrap();
4182
4183 let mod_rs = src_fs_dir.join("mod.rs");
4184 std::fs::write(&mod_rs, "pub mod copy;\n").unwrap();
4185
4186 let copy_rs = src_fs_dir.join("copy.rs");
4187 std::fs::write(©_rs, "pub fn copy_file() {}\n").unwrap();
4188
4189 let test_fs_rs = tests_dir.join("test_fs.rs");
4190 let test_source = "use my_crate::fs;\n\n#[test]\nfn test_fs() {}\n";
4191 std::fs::write(&test_fs_rs, test_source).unwrap();
4192
4193 let extractor = RustExtractor::new();
4194 let mod_path = mod_rs.to_string_lossy().into_owned();
4195 let copy_path = copy_rs.to_string_lossy().into_owned();
4196 let test_path = test_fs_rs.to_string_lossy().into_owned();
4197 let production_files = vec![mod_path.clone(), copy_path.clone()];
4198 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
4199 .into_iter()
4200 .collect();
4201
4202 let result = extractor.map_test_files_with_imports(
4204 &production_files,
4205 &test_sources,
4206 tmp.path(),
4207 false,
4208 );
4209
4210 let mod_mapping = result.iter().find(|m| m.production_file == mod_path);
4213 let copy_mapping = result.iter().find(|m| m.production_file == copy_path);
4214 let mod_mapped = mod_mapping
4215 .map(|m| m.test_files.contains(&test_path))
4216 .unwrap_or(false);
4217 let copy_mapped = copy_mapping
4218 .map(|m| m.test_files.contains(&test_path))
4219 .unwrap_or(false);
4220 assert!(
4221 mod_mapped || copy_mapped,
4222 "Expected test_fs.rs to map to src/fs/mod.rs or src/fs/copy.rs via single-segment L2, \
4223 mod_mapping: {:?}, copy_mapping: {:?}",
4224 mod_mapping.map(|m| &m.test_files),
4225 copy_mapping.map(|m| &m.test_files)
4226 );
4227 }
4228
4229 #[test]
4233 fn rs_export_cfg_01_finds_pub_struct_inside_cfg_macro() {
4234 use std::io::Write;
4235
4236 let mut tmp = tempfile::NamedTempFile::new().unwrap();
4238 write!(
4239 tmp,
4240 "cfg_net! {{ pub struct TcpListener {{ field: u32 }} }}\n"
4241 )
4242 .unwrap();
4243 let path = tmp.path();
4244
4245 let extractor = RustExtractor::new();
4247 let symbols = vec!["TcpListener".to_string()];
4248 let result = extractor.file_exports_any_symbol(path, &symbols);
4249
4250 assert!(
4252 result,
4253 "Expected file_exports_any_symbol to return true for pub struct inside cfg macro, got false"
4254 );
4255 }
4256
4257 #[test]
4261 fn rs_export_cfg_02_returns_false_for_missing_symbol() {
4262 use std::io::Write;
4263
4264 let mut tmp = tempfile::NamedTempFile::new().unwrap();
4266 write!(
4267 tmp,
4268 "cfg_net! {{ pub struct TcpListener {{ field: u32 }} }}\n"
4269 )
4270 .unwrap();
4271 let path = tmp.path();
4272
4273 let extractor = RustExtractor::new();
4275 let symbols = vec!["NotHere".to_string()];
4276 let result = extractor.file_exports_any_symbol(path, &symbols);
4277
4278 assert!(
4280 !result,
4281 "Expected file_exports_any_symbol to return false for symbol not in file, got true"
4282 );
4283 }
4284
4285 #[test]
4289 fn rs_export_cfg_03_does_not_match_pub_crate() {
4290 use std::io::Write;
4291
4292 let mut tmp = tempfile::NamedTempFile::new().unwrap();
4294 write!(
4295 tmp,
4296 "cfg_net! {{ pub(crate) struct Internal {{ field: u32 }} }}\n"
4297 )
4298 .unwrap();
4299 let path = tmp.path();
4300
4301 let extractor = RustExtractor::new();
4303 let symbols = vec!["Internal".to_string()];
4304 let result = extractor.file_exports_any_symbol(path, &symbols);
4305
4306 assert!(
4308 !result,
4309 "Expected file_exports_any_symbol to return false for pub(crate) struct, got true"
4310 );
4311 }
4312
4313 #[test]
4317 fn rs_multiline_use_01_joins_multiline_pub_use() {
4318 let text = " pub use util::{\n AsyncReadExt,\n AsyncWriteExt,\n };\n";
4320
4321 let result = join_multiline_pub_use(text);
4323
4324 assert!(
4326 result.contains("pub use util::{"),
4327 "Expected result to contain 'pub use util::{{', got: {:?}",
4328 result
4329 );
4330 assert!(
4331 result.contains("AsyncReadExt"),
4332 "Expected result to contain 'AsyncReadExt', got: {:?}",
4333 result
4334 );
4335 assert!(
4336 result.contains('}'),
4337 "Expected result to contain '}}', got: {:?}",
4338 result
4339 );
4340 let joined_line = result.lines().find(|l| l.contains("pub use util::"));
4342 assert!(
4343 joined_line.is_some(),
4344 "Expected a single line containing 'pub use util::', got: {:?}",
4345 result
4346 );
4347 let joined_line = joined_line.unwrap();
4348 assert!(
4349 joined_line.contains("AsyncReadExt") && joined_line.contains('}'),
4350 "Expected joined line to contain both 'AsyncReadExt' and '}}', got: {:?}",
4351 joined_line
4352 );
4353 }
4354
4355 #[test]
4359 fn rs_multiline_use_02_extract_re_exports_parses_multiline_cfg_pub_use() {
4360 let source =
4362 "cfg_io! {\n pub use util::{\n Copy,\n AsyncReadExt,\n };\n}\n";
4363
4364 let extractor = RustExtractor::new();
4366 let result = extractor.extract_barrel_re_exports(source, "src/mod.rs");
4367
4368 let entry = result.iter().find(|e| e.from_specifier == "./util");
4370 assert!(
4371 entry.is_some(),
4372 "Expected entry with from_specifier='./util', got: {:?}",
4373 result
4374 );
4375 let entry = entry.unwrap();
4376 assert!(
4377 entry.symbols.contains(&"Copy".to_string()),
4378 "Expected 'Copy' in symbols, got: {:?}",
4379 entry.symbols
4380 );
4381 assert!(
4382 entry.symbols.contains(&"AsyncReadExt".to_string()),
4383 "Expected 'AsyncReadExt' in symbols, got: {:?}",
4384 entry.symbols
4385 );
4386 }
4387
4388 #[test]
4392 fn rs_l2_cfg_export_e2e_resolves_through_cfg_wrapped_production_file() {
4393 let tmp = tempfile::tempdir().unwrap();
4399 let src_net_dir = tmp.path().join("src").join("net");
4400 let src_tcp_dir = src_net_dir.join("tcp");
4401 let src_listener_dir = src_tcp_dir.join("listener");
4402 let tests_dir = tmp.path().join("tests");
4403 std::fs::create_dir_all(&src_net_dir).unwrap();
4404 std::fs::create_dir_all(&src_tcp_dir).unwrap();
4405 std::fs::create_dir_all(&src_listener_dir).unwrap();
4406 std::fs::create_dir_all(&tests_dir).unwrap();
4407
4408 std::fs::write(
4409 tmp.path().join("Cargo.toml"),
4410 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
4411 )
4412 .unwrap();
4413
4414 let net_mod_rs = src_net_dir.join("mod.rs");
4415 std::fs::write(
4416 &net_mod_rs,
4417 "cfg_net! { pub mod tcp; pub use tcp::listener::TcpListener; }\n",
4418 )
4419 .unwrap();
4420
4421 let tcp_mod_rs = src_tcp_dir.join("mod.rs");
4422 std::fs::write(&tcp_mod_rs, "pub mod listener;\n").unwrap();
4423
4424 let listener_rs = src_tcp_dir.join("listener.rs");
4425 std::fs::write(&listener_rs, "cfg_net! { pub struct TcpListener; }\n").unwrap();
4426
4427 let test_net_rs = tests_dir.join("test_net.rs");
4428 let test_source = "use my_crate::net::TcpListener;\n\n#[test]\nfn test_net() {}\n";
4429 std::fs::write(&test_net_rs, test_source).unwrap();
4430
4431 let extractor = RustExtractor::new();
4432 let listener_path = listener_rs.to_string_lossy().into_owned();
4433 let test_path = test_net_rs.to_string_lossy().into_owned();
4434 let production_files = vec![
4435 net_mod_rs.to_string_lossy().into_owned(),
4436 tcp_mod_rs.to_string_lossy().into_owned(),
4437 listener_path.clone(),
4438 ];
4439 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
4440 .into_iter()
4441 .collect();
4442
4443 let result = extractor.map_test_files_with_imports(
4445 &production_files,
4446 &test_sources,
4447 tmp.path(),
4448 false,
4449 );
4450
4451 let mapping = result.iter().find(|m| m.production_file == listener_path);
4453 assert!(
4454 mapping.is_some(),
4455 "No mapping found for src/net/tcp/listener.rs"
4456 );
4457 let mapping = mapping.unwrap();
4458 assert!(
4459 mapping.test_files.contains(&test_path),
4460 "Expected test_net.rs to map to listener.rs through cfg-wrapped barrel (L2), got: {:?}",
4461 mapping.test_files
4462 );
4463 }
4464
4465 #[test]
4469 fn rs_l2_cfg_multiline_e2e_resolves_through_multiline_cfg_pub_use() {
4470 let tmp = tempfile::tempdir().unwrap();
4475 let src_io_dir = tmp.path().join("src").join("io");
4476 let tests_dir = tmp.path().join("tests");
4477 std::fs::create_dir_all(&src_io_dir).unwrap();
4478 std::fs::create_dir_all(&tests_dir).unwrap();
4479
4480 std::fs::write(
4481 tmp.path().join("Cargo.toml"),
4482 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
4483 )
4484 .unwrap();
4485
4486 let io_mod_rs = src_io_dir.join("mod.rs");
4487 std::fs::write(
4488 &io_mod_rs,
4489 "cfg_io! {\n pub use util::{\n AsyncReadExt,\n Copy,\n };\n}\n",
4490 )
4491 .unwrap();
4492
4493 let util_rs = src_io_dir.join("util.rs");
4494 std::fs::write(&util_rs, "pub trait AsyncReadExt {}\npub fn Copy() {}\n").unwrap();
4495
4496 let test_io_rs = tests_dir.join("test_io.rs");
4497 let test_source = "use my_crate::io::AsyncReadExt;\n\n#[test]\nfn test_io() {}\n";
4498 std::fs::write(&test_io_rs, test_source).unwrap();
4499
4500 let extractor = RustExtractor::new();
4501 let util_path = util_rs.to_string_lossy().into_owned();
4502 let test_path = test_io_rs.to_string_lossy().into_owned();
4503 let production_files = vec![io_mod_rs.to_string_lossy().into_owned(), util_path.clone()];
4504 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
4505 .into_iter()
4506 .collect();
4507
4508 let result = extractor.map_test_files_with_imports(
4510 &production_files,
4511 &test_sources,
4512 tmp.path(),
4513 false,
4514 );
4515
4516 let mapping = result.iter().find(|m| m.production_file == util_path);
4518 assert!(mapping.is_some(), "No mapping found for src/io/util.rs");
4519 let mapping = mapping.unwrap();
4520 assert!(
4521 mapping.test_files.contains(&test_path),
4522 "Expected test_io.rs to map to util.rs through multi-line cfg pub use (L2), got: {:?}",
4523 mapping.test_files
4524 );
4525 }
4526
4527 #[test]
4531 fn us_01_underscore_path_sync_broadcast() {
4532 let extractor = RustExtractor::new();
4534 let production_files = vec!["src/sync/broadcast.rs".to_string()];
4535 let test_sources: HashMap<String, String> =
4536 [("tests/sync_broadcast.rs".to_string(), String::new())]
4537 .into_iter()
4538 .collect();
4539 let scan_root = PathBuf::from(".");
4540
4541 let result = extractor.map_test_files_with_imports(
4543 &production_files,
4544 &test_sources,
4545 &scan_root,
4546 false,
4547 );
4548
4549 let mapping = result
4551 .iter()
4552 .find(|m| m.production_file == "src/sync/broadcast.rs");
4553 assert!(mapping.is_some(), "No mapping for src/sync/broadcast.rs");
4554 assert!(
4555 mapping
4556 .unwrap()
4557 .test_files
4558 .contains(&"tests/sync_broadcast.rs".to_string()),
4559 "Expected tests/sync_broadcast.rs to map to src/sync/broadcast.rs via L1.5, got: {:?}",
4560 mapping.unwrap().test_files
4561 );
4562 }
4563
4564 #[test]
4568 fn us_02_underscore_path_sync_oneshot() {
4569 let extractor = RustExtractor::new();
4571 let production_files = vec!["src/sync/oneshot.rs".to_string()];
4572 let test_sources: HashMap<String, String> =
4573 [("tests/sync_oneshot.rs".to_string(), String::new())]
4574 .into_iter()
4575 .collect();
4576 let scan_root = PathBuf::from(".");
4577
4578 let result = extractor.map_test_files_with_imports(
4580 &production_files,
4581 &test_sources,
4582 &scan_root,
4583 false,
4584 );
4585
4586 let mapping = result
4588 .iter()
4589 .find(|m| m.production_file == "src/sync/oneshot.rs");
4590 assert!(mapping.is_some(), "No mapping for src/sync/oneshot.rs");
4591 assert!(
4592 mapping
4593 .unwrap()
4594 .test_files
4595 .contains(&"tests/sync_oneshot.rs".to_string()),
4596 "Expected tests/sync_oneshot.rs to map to src/sync/oneshot.rs via L1.5, got: {:?}",
4597 mapping.unwrap().test_files
4598 );
4599 }
4600
4601 #[test]
4605 fn us_03_underscore_path_task_blocking() {
4606 let extractor = RustExtractor::new();
4608 let production_files = vec!["src/task/blocking.rs".to_string()];
4609 let test_sources: HashMap<String, String> =
4610 [("tests/task_blocking.rs".to_string(), String::new())]
4611 .into_iter()
4612 .collect();
4613 let scan_root = PathBuf::from(".");
4614
4615 let result = extractor.map_test_files_with_imports(
4617 &production_files,
4618 &test_sources,
4619 &scan_root,
4620 false,
4621 );
4622
4623 let mapping = result
4625 .iter()
4626 .find(|m| m.production_file == "src/task/blocking.rs");
4627 assert!(mapping.is_some(), "No mapping for src/task/blocking.rs");
4628 assert!(
4629 mapping
4630 .unwrap()
4631 .test_files
4632 .contains(&"tests/task_blocking.rs".to_string()),
4633 "Expected tests/task_blocking.rs to map to src/task/blocking.rs via L1.5, got: {:?}",
4634 mapping.unwrap().test_files
4635 );
4636 }
4637
4638 #[test]
4642 fn us_04_underscore_path_macros_select() {
4643 let extractor = RustExtractor::new();
4645 let production_files = vec!["src/macros/select.rs".to_string()];
4646 let test_sources: HashMap<String, String> =
4647 [("tests/macros_select.rs".to_string(), String::new())]
4648 .into_iter()
4649 .collect();
4650 let scan_root = PathBuf::from(".");
4651
4652 let result = extractor.map_test_files_with_imports(
4654 &production_files,
4655 &test_sources,
4656 &scan_root,
4657 false,
4658 );
4659
4660 let mapping = result
4662 .iter()
4663 .find(|m| m.production_file == "src/macros/select.rs");
4664 assert!(mapping.is_some(), "No mapping for src/macros/select.rs");
4665 assert!(
4666 mapping
4667 .unwrap()
4668 .test_files
4669 .contains(&"tests/macros_select.rs".to_string()),
4670 "Expected tests/macros_select.rs to map to src/macros/select.rs via L1.5, got: {:?}",
4671 mapping.unwrap().test_files
4672 );
4673 }
4674
4675 #[test]
4679 fn us_05_underscore_path_no_underscore_unchanged() {
4680 let extractor = RustExtractor::new();
4684 let production_files = vec!["src/abc.rs".to_string()];
4685 let test_sources: HashMap<String, String> = [("tests/abc.rs".to_string(), String::new())]
4686 .into_iter()
4687 .collect();
4688 let scan_root = PathBuf::from(".");
4689
4690 let result = extractor.map_test_files_with_imports(
4692 &production_files,
4693 &test_sources,
4694 &scan_root,
4695 false,
4696 );
4697
4698 let mapping = result.iter().find(|m| m.production_file == "src/abc.rs");
4702 assert!(mapping.is_some(), "No mapping entry for src/abc.rs");
4703 assert!(
4705 !mapping
4706 .unwrap()
4707 .test_files
4708 .contains(&"tests/abc.rs".to_string()),
4709 "L1.5 must not match tests/abc.rs -> src/abc.rs (different dirs, no underscore): {:?}",
4710 mapping.unwrap().test_files
4711 );
4712 }
4713
4714 #[test]
4718 fn us_06_underscore_path_wrong_dir_no_match() {
4719 let extractor = RustExtractor::new();
4721 let production_files = vec!["src/runtime/broadcast.rs".to_string()];
4722 let test_sources: HashMap<String, String> =
4723 [("tests/sync_broadcast.rs".to_string(), String::new())]
4724 .into_iter()
4725 .collect();
4726 let scan_root = PathBuf::from(".");
4727
4728 let result = extractor.map_test_files_with_imports(
4730 &production_files,
4731 &test_sources,
4732 &scan_root,
4733 false,
4734 );
4735
4736 let mapping = result
4738 .iter()
4739 .find(|m| m.production_file == "src/runtime/broadcast.rs");
4740 assert!(
4741 mapping.is_some(),
4742 "No mapping entry for src/runtime/broadcast.rs"
4743 );
4744 assert!(
4745 !mapping.unwrap().test_files.contains(&"tests/sync_broadcast.rs".to_string()),
4746 "L1.5 must NOT match tests/sync_broadcast.rs -> src/runtime/broadcast.rs (wrong dir), got: {:?}",
4747 mapping.unwrap().test_files
4748 );
4749 }
4750
4751 #[test]
4755 fn us_07_underscore_path_short_suffix_no_match() {
4756 let extractor = RustExtractor::new();
4758 let production_files = vec!["src/a/b.rs".to_string()];
4759 let test_sources: HashMap<String, String> = [("tests/a_b.rs".to_string(), String::new())]
4760 .into_iter()
4761 .collect();
4762 let scan_root = PathBuf::from(".");
4763
4764 let result = extractor.map_test_files_with_imports(
4766 &production_files,
4767 &test_sources,
4768 &scan_root,
4769 false,
4770 );
4771
4772 let mapping = result.iter().find(|m| m.production_file == "src/a/b.rs");
4774 assert!(mapping.is_some(), "No mapping entry for src/a/b.rs");
4775 assert!(
4776 !mapping
4777 .unwrap()
4778 .test_files
4779 .contains(&"tests/a_b.rs".to_string()),
4780 "L1.5 must NOT match tests/a_b.rs -> src/a/b.rs (short suffix guard), got: {:?}",
4781 mapping.unwrap().test_files
4782 );
4783 }
4784
4785 #[test]
4789 fn us_08_underscore_path_already_l1_matched_skipped() {
4790 let extractor = RustExtractor::new();
4793 let production_files = vec![
4794 "src/broadcast.rs".to_string(),
4795 "src/sync/broadcast.rs".to_string(),
4796 ];
4797 let test_sources: HashMap<String, String> = [
4798 ("tests/broadcast.rs".to_string(), String::new()),
4799 ("tests/sync_broadcast.rs".to_string(), String::new()),
4800 ]
4801 .into_iter()
4802 .collect();
4803 let scan_root = PathBuf::from(".");
4804
4805 let result = extractor.map_test_files_with_imports(
4807 &production_files,
4808 &test_sources,
4809 &scan_root,
4810 false,
4811 );
4812
4813 let sync_mapping = result
4817 .iter()
4818 .find(|m| m.production_file == "src/sync/broadcast.rs");
4819 assert!(
4820 sync_mapping.is_some(),
4821 "No mapping entry for src/sync/broadcast.rs"
4822 );
4823 assert!(
4824 sync_mapping
4825 .unwrap()
4826 .test_files
4827 .contains(&"tests/sync_broadcast.rs".to_string()),
4828 "Expected tests/sync_broadcast.rs to map to src/sync/broadcast.rs via L1.5, got: {:?}",
4829 sync_mapping.unwrap().test_files
4830 );
4831 }
4832
4833 #[test]
4837 fn xc_01_cross_crate_extract_root_crate_name() {
4838 let source = "use clap::builder::Arg;\n";
4840 let crate_names = ["clap", "clap_builder"];
4841
4842 let result = extract_import_specifiers_with_crate_names(source, &crate_names);
4844
4845 assert!(
4847 !result.is_empty(),
4848 "Expected at least one import entry, got empty"
4849 );
4850 let entry = result
4851 .iter()
4852 .find(|(crate_n, spec, _)| crate_n == "clap" && spec == "builder");
4853 assert!(
4854 entry.is_some(),
4855 "Expected entry (clap, builder, [Arg]), got: {:?}",
4856 result
4857 );
4858 let (_, _, symbols) = entry.unwrap();
4859 assert!(
4860 symbols.contains(&"Arg".to_string()),
4861 "Expected symbols to contain 'Arg', got: {:?}",
4862 symbols
4863 );
4864 }
4865
4866 #[test]
4870 fn xc_02_cross_crate_extract_member_crate_name() {
4871 let source = "use clap_builder::error::ErrorKind;\n";
4874 let crate_names = ["clap", "clap_builder"];
4875
4876 let result = extract_import_specifiers_with_crate_names(source, &crate_names);
4878
4879 assert!(
4882 !result.is_empty(),
4883 "Expected at least one import entry, got empty"
4884 );
4885 let entry = result
4886 .iter()
4887 .find(|(crate_n, spec, _)| crate_n == "clap_builder" && spec == "error");
4888 assert!(
4889 entry.is_some(),
4890 "Expected entry (clap_builder, error, [ErrorKind]), got: {:?}",
4891 result
4892 );
4893 let (_, _, symbols) = entry.unwrap();
4894 assert!(
4895 symbols.contains(&"ErrorKind".to_string()),
4896 "Expected symbols to contain 'ErrorKind', got: {:?}",
4897 symbols
4898 );
4899 }
4900
4901 #[test]
4905 fn xc_03_cross_crate_skips_external() {
4906 let source = "use std::collections::HashMap;\n";
4908 let crate_names = ["clap"];
4909
4910 let result = extract_import_specifiers_with_crate_names(source, &crate_names);
4912
4913 assert!(
4915 result.is_empty(),
4916 "Expected empty result for std:: import not in crate_names, got: {:?}",
4917 result
4918 );
4919 }
4920
4921 #[test]
4925 fn xc_04_cross_crate_crate_prefix_still_works() {
4926 let source = "use crate::utils;\n";
4928 let crate_names = ["clap"];
4929
4930 let result = extract_import_specifiers_with_crate_names(source, &crate_names);
4932
4933 assert!(
4936 !result.is_empty(),
4937 "Expected at least one import entry for `use crate::utils`, got empty"
4938 );
4939 let entry = result
4940 .iter()
4941 .find(|(crate_n, spec, _)| crate_n == "crate" && spec == "utils");
4942 assert!(
4943 entry.is_some(),
4944 "Expected entry (crate, utils, []), got: {:?}",
4945 result
4946 );
4947 }
4948
4949 #[test]
4953 fn xc_05_cross_crate_root_test_maps_to_root_src() {
4954 let tmp = tempfile::tempdir().unwrap();
4959 let src_dir = tmp.path().join("src");
4960 let tests_dir = tmp.path().join("tests");
4961 std::fs::create_dir_all(&src_dir).unwrap();
4962 std::fs::create_dir_all(&tests_dir).unwrap();
4963
4964 std::fs::write(
4965 tmp.path().join("Cargo.toml"),
4966 "[package]\nname = \"my_crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
4967 )
4968 .unwrap();
4969
4970 let builder_rs = src_dir.join("builder.rs");
4971 std::fs::write(&builder_rs, "pub struct Command;\n").unwrap();
4972
4973 let test_builder_rs = tests_dir.join("test_builder.rs");
4974 let test_source = "use my_crate::builder::Command;\n\n#[test]\nfn test_builder() {}\n";
4975 std::fs::write(&test_builder_rs, test_source).unwrap();
4976
4977 let extractor = RustExtractor::new();
4978 let prod_path = builder_rs.to_string_lossy().into_owned();
4979 let test_path = test_builder_rs.to_string_lossy().into_owned();
4980 let production_files = vec![prod_path.clone()];
4981 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
4982 .into_iter()
4983 .collect();
4984
4985 let result = extractor.map_test_files_with_imports(
4987 &production_files,
4988 &test_sources,
4989 tmp.path(),
4990 false,
4991 );
4992
4993 let mapping = result.iter().find(|m| m.production_file == prod_path);
4995 assert!(mapping.is_some(), "No mapping found for src/builder.rs");
4996 assert!(
4997 mapping.unwrap().test_files.contains(&test_path),
4998 "Expected test_builder.rs to map to builder.rs via cross-crate L2, got: {:?}",
4999 mapping.unwrap().test_files
5000 );
5001 }
5002
5003 #[test]
5007 fn xc_06_cross_crate_root_test_maps_to_member() {
5008 let tmp = tempfile::tempdir().unwrap();
5015 let member_dir = tmp.path().join("member_a");
5016 let member_src = member_dir.join("src");
5017 let tests_dir = tmp.path().join("tests");
5018 std::fs::create_dir_all(&member_src).unwrap();
5019 std::fs::create_dir_all(&tests_dir).unwrap();
5020
5021 std::fs::write(
5023 tmp.path().join("Cargo.toml"),
5024 "[workspace]\nmembers = [\"member_a\"]\n",
5025 )
5026 .unwrap();
5027
5028 std::fs::write(
5030 member_dir.join("Cargo.toml"),
5031 "[package]\nname = \"member_a\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
5032 )
5033 .unwrap();
5034
5035 let builder_rs = member_src.join("builder.rs");
5036 std::fs::write(&builder_rs, "pub struct Cmd;\n").unwrap();
5037
5038 let test_builder_rs = tests_dir.join("test_builder.rs");
5039 let test_source = "use member_a::builder::Cmd;\n\n#[test]\nfn test_builder() {}\n";
5040 std::fs::write(&test_builder_rs, test_source).unwrap();
5041
5042 let extractor = RustExtractor::new();
5043 let prod_path = builder_rs.to_string_lossy().into_owned();
5044 let test_path = test_builder_rs.to_string_lossy().into_owned();
5045 let production_files = vec![prod_path.clone()];
5046 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
5047 .into_iter()
5048 .collect();
5049
5050 let result = extractor.map_test_files_with_imports(
5052 &production_files,
5053 &test_sources,
5054 tmp.path(),
5055 false,
5056 );
5057
5058 let mapping = result.iter().find(|m| m.production_file == prod_path);
5060 assert!(
5061 mapping.is_some(),
5062 "No mapping found for member_a/src/builder.rs"
5063 );
5064 assert!(
5065 mapping.unwrap().test_files.contains(&test_path),
5066 "Expected root test_builder.rs to map to member_a/src/builder.rs via cross-crate L2, got: {:?}",
5067 mapping.unwrap().test_files
5068 );
5069 }
5070
5071 #[test]
5080 fn xc_07_cross_crate_member_test_not_affected() {
5081 let tmp = tempfile::tempdir().unwrap();
5088 let member_dir = tmp.path().join("member_a");
5089 let member_src = member_dir.join("src");
5090 let member_tests = member_dir.join("tests");
5091 std::fs::create_dir_all(&member_src).unwrap();
5092 std::fs::create_dir_all(&member_tests).unwrap();
5093
5094 std::fs::write(
5096 tmp.path().join("Cargo.toml"),
5097 "[workspace]\nmembers = [\"member_a\"]\n",
5098 )
5099 .unwrap();
5100
5101 std::fs::write(
5103 member_dir.join("Cargo.toml"),
5104 "[package]\nname = \"member_a\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
5105 )
5106 .unwrap();
5107
5108 let engine_rs = member_src.join("engine.rs");
5109 std::fs::write(&engine_rs, "pub struct Engine;\n").unwrap();
5110
5111 let test_rs = member_tests.join("test_engine.rs");
5112 let test_source = "use member_a::engine::Engine;\n\n#[test]\nfn test_engine() {}\n";
5113 std::fs::write(&test_rs, test_source).unwrap();
5114
5115 let extractor = RustExtractor::new();
5116 let prod_path = engine_rs.to_string_lossy().into_owned();
5117 let test_path = test_rs.to_string_lossy().into_owned();
5118 let production_files = vec![prod_path.clone()];
5119 let test_sources: HashMap<String, String> = [(test_path.clone(), test_source.to_string())]
5120 .into_iter()
5121 .collect();
5122
5123 let result = extractor.map_test_files_with_imports(
5126 &production_files,
5127 &test_sources,
5128 tmp.path(),
5129 false,
5130 );
5131
5132 let mapping = result.iter().find(|m| m.production_file == prod_path);
5136 assert!(
5137 mapping.is_some(),
5138 "No mapping found for member_a/src/engine.rs"
5139 );
5140 assert!(
5141 mapping.unwrap().test_files.contains(&test_path),
5142 "Expected member_a/tests/test_engine.rs to map to member_a/src/engine.rs via per-member L2, got: {:?}",
5143 mapping.unwrap().test_files
5144 );
5145 }
5146
5147 #[test]
5151 fn sd_01_subdir_stem_match_same_crate() {
5152 let extractor = RustExtractor::new();
5154 let production_files = vec!["src/builder/action.rs".to_string()];
5155 let test_sources: HashMap<String, String> =
5156 [("tests/builder/action.rs".to_string(), String::new())]
5157 .into_iter()
5158 .collect();
5159 let scan_root = PathBuf::from(".");
5160
5161 let result = extractor.map_test_files_with_imports(
5163 &production_files,
5164 &test_sources,
5165 &scan_root,
5166 false,
5167 );
5168
5169 let mapping = result
5171 .iter()
5172 .find(|m| m.production_file == "src/builder/action.rs");
5173 assert!(mapping.is_some(), "No mapping for src/builder/action.rs");
5174 assert!(
5175 mapping
5176 .unwrap()
5177 .test_files
5178 .contains(&"tests/builder/action.rs".to_string()),
5179 "Expected tests/builder/action.rs to map to src/builder/action.rs via L1.6 subdir matching, got: {:?}",
5180 mapping.unwrap().test_files
5181 );
5182 }
5183
5184 #[test]
5188 fn sd_02_subdir_stem_match_cross_crate() {
5189 let extractor = RustExtractor::new();
5191 let production_files = vec!["member_a/src/builder/command.rs".to_string()];
5192 let test_sources: HashMap<String, String> =
5193 [("tests/builder/command.rs".to_string(), String::new())]
5194 .into_iter()
5195 .collect();
5196 let scan_root = PathBuf::from(".");
5197
5198 let result = extractor.map_test_files_with_imports(
5200 &production_files,
5201 &test_sources,
5202 &scan_root,
5203 false,
5204 );
5205
5206 let mapping = result
5208 .iter()
5209 .find(|m| m.production_file == "member_a/src/builder/command.rs");
5210 assert!(
5211 mapping.is_some(),
5212 "No mapping for member_a/src/builder/command.rs"
5213 );
5214 assert!(
5215 mapping
5216 .unwrap()
5217 .test_files
5218 .contains(&"tests/builder/command.rs".to_string()),
5219 "Expected tests/builder/command.rs to map to member_a/src/builder/command.rs via L1.6 subdir matching, got: {:?}",
5220 mapping.unwrap().test_files
5221 );
5222 }
5223
5224 #[test]
5228 fn sd_03_subdir_wrong_dir_no_match() {
5229 let extractor = RustExtractor::new();
5231 let production_files = vec!["src/parser/action.rs".to_string()];
5232 let test_sources: HashMap<String, String> =
5233 [("tests/builder/action.rs".to_string(), String::new())]
5234 .into_iter()
5235 .collect();
5236 let scan_root = PathBuf::from(".");
5237
5238 let result = extractor.map_test_files_with_imports(
5240 &production_files,
5241 &test_sources,
5242 &scan_root,
5243 false,
5244 );
5245
5246 let mapping = result
5248 .iter()
5249 .find(|m| m.production_file == "src/parser/action.rs");
5250 assert!(
5251 mapping.is_some(),
5252 "No mapping entry for src/parser/action.rs"
5253 );
5254 assert!(
5255 mapping.unwrap().test_files.is_empty(),
5256 "Expected NO match for src/parser/action.rs (wrong dir), got: {:?}",
5257 mapping.unwrap().test_files
5258 );
5259 }
5260
5261 #[test]
5265 fn sd_04_subdir_no_subdir_skip() {
5266 let extractor = RustExtractor::new();
5268 let production_files = vec!["src/builder/action.rs".to_string()];
5269 let test_sources: HashMap<String, String> =
5270 [("tests/action.rs".to_string(), String::new())]
5271 .into_iter()
5272 .collect();
5273 let scan_root = PathBuf::from(".");
5274
5275 let result = extractor.map_test_files_with_imports(
5277 &production_files,
5278 &test_sources,
5279 &scan_root,
5280 false,
5281 );
5282
5283 let mapping = result
5285 .iter()
5286 .find(|m| m.production_file == "src/builder/action.rs");
5287 assert!(
5288 mapping.is_some(),
5289 "No mapping entry for src/builder/action.rs"
5290 );
5291 assert!(
5292 mapping.unwrap().test_files.is_empty(),
5293 "Expected NO match for src/builder/action.rs (no test subdir), got: {:?}",
5294 mapping.unwrap().test_files
5295 );
5296 }
5297
5298 #[test]
5302 fn sd_05_subdir_main_rs_skip() {
5303 let extractor = RustExtractor::new();
5306 let production_files = vec!["src/builder/action.rs".to_string()];
5307 let test_sources: HashMap<String, String> =
5308 [("tests/builder/main.rs".to_string(), String::new())]
5309 .into_iter()
5310 .collect();
5311 let scan_root = PathBuf::from(".");
5312
5313 let result = extractor.map_test_files_with_imports(
5315 &production_files,
5316 &test_sources,
5317 &scan_root,
5318 false,
5319 );
5320
5321 let mapping = result
5323 .iter()
5324 .find(|m| m.production_file == "src/builder/action.rs");
5325 assert!(
5326 mapping.is_some(),
5327 "No mapping entry for src/builder/action.rs"
5328 );
5329 assert!(
5330 mapping.unwrap().test_files.is_empty(),
5331 "Expected NO match for src/builder/action.rs (main.rs skipped), got: {:?}",
5332 mapping.unwrap().test_files
5333 );
5334 }
5335
5336 #[test]
5343 fn sd_06_subdir_already_matched_skip() {
5344 let extractor = RustExtractor::new();
5348 let production_files = vec![
5349 "src/sync/action.rs".to_string(),
5350 "src/builder/command.rs".to_string(),
5351 ];
5352 let test_sources: HashMap<String, String> = [
5353 ("tests/sync_action.rs".to_string(), String::new()),
5354 ("tests/builder/command.rs".to_string(), String::new()),
5355 ]
5356 .into_iter()
5357 .collect();
5358 let scan_root = PathBuf::from(".");
5359
5360 let result = extractor.map_test_files_with_imports(
5362 &production_files,
5363 &test_sources,
5364 &scan_root,
5365 false,
5366 );
5367
5368 let l15_mapping = result
5370 .iter()
5371 .find(|m| m.production_file == "src/sync/action.rs");
5372 assert!(
5373 l15_mapping.is_some(),
5374 "No mapping entry for src/sync/action.rs"
5375 );
5376 assert!(
5377 l15_mapping
5378 .unwrap()
5379 .test_files
5380 .contains(&"tests/sync_action.rs".to_string()),
5381 "Expected tests/sync_action.rs to L1.5-match to src/sync/action.rs, got: {:?}",
5382 l15_mapping.unwrap().test_files
5383 );
5384
5385 let sd_mapping = result
5388 .iter()
5389 .find(|m| m.production_file == "src/builder/command.rs");
5390 assert!(
5391 sd_mapping.is_some(),
5392 "No mapping entry for src/builder/command.rs"
5393 );
5394 assert!(
5395 sd_mapping
5396 .unwrap()
5397 .test_files
5398 .contains(&"tests/builder/command.rs".to_string()),
5399 "Expected tests/builder/command.rs to L1.6-match to src/builder/command.rs, got: {:?}",
5400 sd_mapping.unwrap().test_files
5401 );
5402 }
5403
5404 #[test]
5414 fn ccb_01_wildcard_cross_crate_barrel_maps_test_to_member() {
5415 let tmp = tempfile::tempdir().unwrap();
5424
5425 std::fs::write(
5427 tmp.path().join("Cargo.toml"),
5428 "[workspace]\nmembers = [\"sibling_crate\"]\n\n[package]\nname = \"root_crate\"\nversion = \"0.1.0\"\n",
5429 )
5430 .unwrap();
5431
5432 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
5434 let root_lib = tmp.path().join("src").join("lib.rs");
5435 std::fs::write(&root_lib, "pub use sibling_crate::*;\n").unwrap();
5436
5437 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
5439 let test_file = tmp.path().join("tests").join("test_symbol.rs");
5440 std::fs::write(
5441 &test_file,
5442 "use root_crate::Symbol;\n#[test]\nfn test_symbol() { let _s = Symbol {}; }\n",
5443 )
5444 .unwrap();
5445
5446 let sib_dir = tmp.path().join("sibling_crate");
5448 std::fs::create_dir_all(sib_dir.join("src")).unwrap();
5449 std::fs::write(
5450 sib_dir.join("Cargo.toml"),
5451 "[package]\nname = \"sibling_crate\"\nversion = \"0.1.0\"\n",
5452 )
5453 .unwrap();
5454
5455 let sib_lib = sib_dir.join("src").join("lib.rs");
5457 std::fs::write(&sib_lib, "pub struct Symbol {}\n").unwrap();
5458
5459 let extractor = RustExtractor::new();
5460 let sib_lib_path = sib_lib.to_string_lossy().into_owned();
5461 let test_file_path = test_file.to_string_lossy().into_owned();
5462
5463 let production_files = vec![sib_lib_path.clone()];
5464 let test_sources: HashMap<String, String> = [(
5465 test_file_path.clone(),
5466 std::fs::read_to_string(&test_file).unwrap(),
5467 )]
5468 .into_iter()
5469 .collect();
5470
5471 let result = extractor.map_test_files_with_imports(
5473 &production_files,
5474 &test_sources,
5475 tmp.path(),
5476 false,
5477 );
5478
5479 let mapping = result.iter().find(|m| m.production_file == sib_lib_path);
5481 assert!(
5482 mapping.is_some(),
5483 "No mapping entry for sibling_crate/src/lib.rs. All mappings: {:#?}",
5484 result
5485 );
5486 assert!(
5487 mapping.unwrap().test_files.contains(&test_file_path),
5488 "Expected test_symbol.rs mapped to sibling_crate/src/lib.rs via CCB, \
5489 but test_files: {:?}",
5490 mapping.unwrap().test_files
5491 );
5492 }
5493
5494 #[test]
5502 fn ccb_02_named_cross_crate_reexport_maps_test_to_member() {
5503 let tmp = tempfile::tempdir().unwrap();
5504
5505 std::fs::write(
5506 tmp.path().join("Cargo.toml"),
5507 "[workspace]\nmembers = [\"sibling_crate\"]\n\n[package]\nname = \"root_crate\"\nversion = \"0.1.0\"\n",
5508 )
5509 .unwrap();
5510
5511 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
5512 let root_lib = tmp.path().join("src").join("lib.rs");
5513 std::fs::write(&root_lib, "pub use sibling_crate::SpecificType;\n").unwrap();
5515
5516 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
5517 let test_file = tmp.path().join("tests").join("test_specific.rs");
5518 std::fs::write(
5519 &test_file,
5520 "use root_crate::SpecificType;\n#[test]\nfn test_it() { let _x: SpecificType = todo!(); }\n",
5521 )
5522 .unwrap();
5523
5524 let sib_dir = tmp.path().join("sibling_crate");
5525 std::fs::create_dir_all(sib_dir.join("src")).unwrap();
5526 std::fs::write(
5527 sib_dir.join("Cargo.toml"),
5528 "[package]\nname = \"sibling_crate\"\nversion = \"0.1.0\"\n",
5529 )
5530 .unwrap();
5531 let sib_lib = sib_dir.join("src").join("lib.rs");
5532 std::fs::write(&sib_lib, "pub struct SpecificType {}\n").unwrap();
5533
5534 let extractor = RustExtractor::new();
5535 let sib_lib_path = sib_lib.to_string_lossy().into_owned();
5536 let test_file_path = test_file.to_string_lossy().into_owned();
5537
5538 let production_files = vec![sib_lib_path.clone()];
5539 let test_sources: HashMap<String, String> = [(
5540 test_file_path.clone(),
5541 std::fs::read_to_string(&test_file).unwrap(),
5542 )]
5543 .into_iter()
5544 .collect();
5545
5546 let result = extractor.map_test_files_with_imports(
5548 &production_files,
5549 &test_sources,
5550 tmp.path(),
5551 false,
5552 );
5553
5554 let mapping = result.iter().find(|m| m.production_file == sib_lib_path);
5556 assert!(
5557 mapping.is_some(),
5558 "No mapping entry for sibling_crate/src/lib.rs. All mappings: {:#?}",
5559 result
5560 );
5561 assert!(
5562 mapping.unwrap().test_files.contains(&test_file_path),
5563 "Expected test_specific.rs mapped via named CCB re-export, \
5564 but test_files: {:?}",
5565 mapping.unwrap().test_files
5566 );
5567 }
5568
5569 #[test]
5578 fn ccb_03_nonexistent_member_produces_empty_mapping_no_panic() {
5579 let tmp = tempfile::tempdir().unwrap();
5580
5581 std::fs::write(
5583 tmp.path().join("Cargo.toml"),
5584 "[workspace]\nmembers = []\n\n[package]\nname = \"root_crate\"\nversion = \"0.1.0\"\n",
5585 )
5586 .unwrap();
5587
5588 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
5589 let root_lib = tmp.path().join("src").join("lib.rs");
5590 std::fs::write(&root_lib, "pub use nonexistent_crate::*;\n").unwrap();
5591
5592 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
5593 let test_file = tmp.path().join("tests").join("test_ghost.rs");
5594 std::fs::write(
5595 &test_file,
5596 "use root_crate::Ghost;\n#[test]\nfn test_ghost() {}\n",
5597 )
5598 .unwrap();
5599
5600 let extractor = RustExtractor::new();
5601 let root_lib_path = root_lib.to_string_lossy().into_owned();
5603 let test_file_path = test_file.to_string_lossy().into_owned();
5604
5605 let production_files = vec![root_lib_path.clone()];
5606 let test_sources: HashMap<String, String> = [(
5607 test_file_path.clone(),
5608 std::fs::read_to_string(&test_file).unwrap(),
5609 )]
5610 .into_iter()
5611 .collect();
5612
5613 let result = extractor.map_test_files_with_imports(
5615 &production_files,
5616 &test_sources,
5617 tmp.path(),
5618 false,
5619 );
5620
5621 let ghost_mapped = result
5624 .iter()
5625 .any(|m| m.test_files.contains(&test_file_path));
5626 assert!(
5627 !ghost_mapped,
5628 "test_ghost.rs should not be mapped (nonexistent_crate is not a member), \
5629 but found in mappings: {:#?}",
5630 result
5631 );
5632 }
5633
5634 #[test]
5642 fn ccb_04_local_pubmod_still_resolved_by_existing_l2() {
5643 let tmp = tempfile::tempdir().unwrap();
5644
5645 std::fs::write(
5646 tmp.path().join("Cargo.toml"),
5647 "[workspace]\nmembers = [\"sibling\"]\n\n[package]\nname = \"root_crate\"\nversion = \"0.1.0\"\n",
5648 )
5649 .unwrap();
5650
5651 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
5652
5653 let root_lib = tmp.path().join("src").join("lib.rs");
5655 std::fs::write(&root_lib, "pub mod local_module;\npub use sibling::*;\n").unwrap();
5656
5657 let local_mod = tmp.path().join("src").join("local_module.rs");
5659 std::fs::write(&local_mod, "pub fn local_fn() {}\n").unwrap();
5660
5661 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
5663 let test_file = tmp.path().join("tests").join("test_local.rs");
5664 std::fs::write(
5665 &test_file,
5666 "use root_crate::local_module::local_fn;\n#[test]\nfn test_local_fn() { local_fn(); }\n",
5667 )
5668 .unwrap();
5669
5670 let sib_dir = tmp.path().join("sibling");
5672 std::fs::create_dir_all(sib_dir.join("src")).unwrap();
5673 std::fs::write(
5674 sib_dir.join("Cargo.toml"),
5675 "[package]\nname = \"sibling\"\nversion = \"0.1.0\"\n",
5676 )
5677 .unwrap();
5678 std::fs::write(sib_dir.join("src").join("lib.rs"), "pub fn sib_fn() {}\n").unwrap();
5679
5680 let extractor = RustExtractor::new();
5681 let local_mod_path = local_mod.to_string_lossy().into_owned();
5682 let test_file_path = test_file.to_string_lossy().into_owned();
5683
5684 let production_files = vec![local_mod_path.clone()];
5685 let test_sources: HashMap<String, String> = [(
5686 test_file_path.clone(),
5687 std::fs::read_to_string(&test_file).unwrap(),
5688 )]
5689 .into_iter()
5690 .collect();
5691
5692 let result = extractor.map_test_files_with_imports(
5694 &production_files,
5695 &test_sources,
5696 tmp.path(),
5697 false,
5698 );
5699
5700 let mapping = result.iter().find(|m| m.production_file == local_mod_path);
5702 assert!(
5703 mapping.is_some(),
5704 "No mapping entry for local_module.rs. All mappings: {:#?}",
5705 result
5706 );
5707 assert!(
5708 mapping.unwrap().test_files.contains(&test_file_path),
5709 "Expected test_local.rs mapped to local_module.rs via existing L2, \
5710 but test_files: {:?}",
5711 mapping.unwrap().test_files
5712 );
5713 }
5714
5715 #[test]
5730 fn ccb_05_two_level_cross_crate_barrel_chain() {
5731 let tmp = tempfile::tempdir().unwrap();
5732
5733 std::fs::write(
5735 tmp.path().join("Cargo.toml"),
5736 "[workspace]\nmembers = [\"mid\"]\n\n[package]\nname = \"root_crate\"\nversion = \"0.1.0\"\n",
5737 )
5738 .unwrap();
5739
5740 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
5741 let root_lib = tmp.path().join("src").join("lib.rs");
5742 std::fs::write(&root_lib, "pub use mid::*;\n").unwrap();
5744
5745 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
5747 let test_file = tmp.path().join("tests").join("test_mid_item.rs");
5748 std::fs::write(
5749 &test_file,
5750 "use root_crate::MidItem;\n#[test]\nfn test_mid_item() { let _ = MidItem {}; }\n",
5751 )
5752 .unwrap();
5753
5754 let mid_dir = tmp.path().join("mid");
5756 std::fs::create_dir_all(mid_dir.join("src")).unwrap();
5757 std::fs::write(
5758 mid_dir.join("Cargo.toml"),
5759 "[package]\nname = \"mid\"\nversion = \"0.1.0\"\n",
5760 )
5761 .unwrap();
5762 std::fs::write(
5764 mid_dir.join("src").join("lib.rs"),
5765 "pub mod sub;\npub use sub::*;\n",
5766 )
5767 .unwrap();
5768
5769 let sub_rs = mid_dir.join("src").join("sub.rs");
5771 std::fs::write(&sub_rs, "pub struct MidItem {}\n").unwrap();
5772
5773 let extractor = RustExtractor::new();
5774 let sub_rs_path = sub_rs.to_string_lossy().into_owned();
5775 let test_file_path = test_file.to_string_lossy().into_owned();
5776
5777 let production_files = vec![sub_rs_path.clone()];
5778 let test_sources: HashMap<String, String> = [(
5779 test_file_path.clone(),
5780 std::fs::read_to_string(&test_file).unwrap(),
5781 )]
5782 .into_iter()
5783 .collect();
5784
5785 let result = extractor.map_test_files_with_imports(
5787 &production_files,
5788 &test_sources,
5789 tmp.path(),
5790 false,
5791 );
5792
5793 let mapping = result.iter().find(|m| m.production_file == sub_rs_path);
5795 assert!(
5796 mapping.is_some(),
5797 "No mapping entry for mid/src/sub.rs. All mappings: {:#?}",
5798 result
5799 );
5800 assert!(
5801 mapping.unwrap().test_files.contains(&test_file_path),
5802 "Expected test_mid_item.rs mapped to mid/src/sub.rs via 2-level CCB chain, \
5803 but test_files: {:?}",
5804 mapping.unwrap().test_files
5805 );
5806 }
5807
5808 #[test]
5817 fn ccb_06_wildcard_with_many_items_filters_to_single_file() {
5818 let tmp = tempfile::tempdir().unwrap();
5819
5820 std::fs::write(
5821 tmp.path().join("Cargo.toml"),
5822 "[workspace]\nmembers = [\"big_crate\"]\n\n[package]\nname = \"root_crate\"\nversion = \"0.1.0\"\n",
5823 )
5824 .unwrap();
5825
5826 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
5827 let root_lib = tmp.path().join("src").join("lib.rs");
5828 std::fs::write(&root_lib, "pub use big_crate::*;\n").unwrap();
5829
5830 std::fs::create_dir_all(tmp.path().join("tests")).unwrap();
5831 let test_file = tmp.path().join("tests").join("test_specific_fn.rs");
5832 std::fs::write(
5833 &test_file,
5834 "use root_crate::SpecificFn;\n#[test]\nfn test_specific() { SpecificFn::run(); }\n",
5835 )
5836 .unwrap();
5837
5838 let big_dir = tmp.path().join("big_crate");
5840 std::fs::create_dir_all(big_dir.join("src")).unwrap();
5841 std::fs::write(
5842 big_dir.join("Cargo.toml"),
5843 "[package]\nname = \"big_crate\"\nversion = \"0.1.0\"\n",
5844 )
5845 .unwrap();
5846
5847 let mut lib_content = String::new();
5849 for i in 0..50 {
5851 lib_content.push_str(&format!("pub struct Item{i} {{}}\n"));
5852 }
5853 std::fs::write(big_dir.join("src").join("lib.rs"), &lib_content).unwrap();
5854
5855 let specific_rs = big_dir.join("src").join("specific.rs");
5857 std::fs::write(
5858 &specific_rs,
5859 "pub struct SpecificFn;\nimpl SpecificFn { pub fn run() {} }\n",
5860 )
5861 .unwrap();
5862
5863 let extractor = RustExtractor::new();
5864 let specific_rs_path = specific_rs.to_string_lossy().into_owned();
5865 let test_file_path = test_file.to_string_lossy().into_owned();
5866
5867 let big_lib_path = big_dir
5869 .join("src")
5870 .join("lib.rs")
5871 .to_string_lossy()
5872 .into_owned();
5873 let production_files = vec![big_lib_path.clone(), specific_rs_path.clone()];
5874 let test_sources: HashMap<String, String> = [(
5875 test_file_path.clone(),
5876 std::fs::read_to_string(&test_file).unwrap(),
5877 )]
5878 .into_iter()
5879 .collect();
5880
5881 let result = extractor.map_test_files_with_imports(
5883 &production_files,
5884 &test_sources,
5885 tmp.path(),
5886 false,
5887 );
5888
5889 let specific_mapping = result
5891 .iter()
5892 .find(|m| m.production_file == specific_rs_path);
5893 assert!(
5894 specific_mapping.is_some(),
5895 "No mapping entry for big_crate/src/specific.rs. All mappings: {:#?}",
5896 result
5897 );
5898 assert!(
5899 specific_mapping
5900 .unwrap()
5901 .test_files
5902 .contains(&test_file_path),
5903 "Expected test_specific_fn.rs mapped to specific.rs via CCB + symbol filter, \
5904 but test_files: {:?}",
5905 specific_mapping.unwrap().test_files
5906 );
5907
5908 let lib_mapping = result.iter().find(|m| m.production_file == big_lib_path);
5911 if let Some(lib_m) = lib_mapping {
5912 assert!(
5913 !lib_m.test_files.contains(&test_file_path),
5914 "test_specific_fn.rs should NOT be fan-out mapped to big_crate/src/lib.rs \
5915 (which does not export SpecificFn), but found in: {:?}",
5916 lib_m.test_files
5917 );
5918 }
5919 }
5920}