1use super::types::TypeImportSpec;
11use super::FixtureDatabase;
12use once_cell::sync::Lazy;
13use rustpython_parser::ast::{Expr, Stmt};
14use std::collections::{HashMap, HashSet};
15use std::path::{Path, PathBuf};
16use std::sync::{Arc, OnceLock};
17use tracing::{debug, info, warn};
18
19static RUNTIME_STDLIB_MODULES: OnceLock<HashSet<String>> = OnceLock::new();
26
27static STDLIB_MODULES: Lazy<HashSet<&'static str>> = Lazy::new(|| {
35 [
36 "os",
37 "sys",
38 "re",
39 "json",
40 "typing",
41 "collections",
42 "functools",
43 "itertools",
44 "pathlib",
45 "datetime",
46 "time",
47 "math",
48 "random",
49 "copy",
50 "io",
51 "abc",
52 "contextlib",
53 "dataclasses",
54 "enum",
55 "logging",
56 "unittest",
57 "asyncio",
58 "concurrent",
59 "multiprocessing",
60 "threading",
61 "subprocess",
62 "shutil",
63 "tempfile",
64 "glob",
65 "fnmatch",
66 "pickle",
67 "sqlite3",
68 "urllib",
69 "http",
70 "email",
71 "html",
72 "xml",
73 "socket",
74 "ssl",
75 "select",
76 "signal",
77 "struct",
78 "codecs",
79 "textwrap",
80 "string",
81 "difflib",
82 "inspect",
83 "dis",
84 "traceback",
85 "warnings",
86 "weakref",
87 "types",
88 "importlib",
89 "pkgutil",
90 "pprint",
91 "reprlib",
92 "numbers",
93 "decimal",
94 "fractions",
95 "statistics",
96 "hashlib",
97 "hmac",
98 "secrets",
99 "base64",
100 "binascii",
101 "zlib",
102 "gzip",
103 "bz2",
104 "lzma",
105 "zipfile",
106 "tarfile",
107 "csv",
108 "configparser",
109 "argparse",
110 "getopt",
111 "getpass",
112 "platform",
113 "errno",
114 "ctypes",
115 "__future__",
116 ]
117 .into_iter()
118 .collect()
119});
120
121#[derive(Debug, Clone)]
123#[allow(dead_code)] pub struct FixtureImport {
125 pub module_path: String,
127 pub is_star_import: bool,
129 pub imported_names: Vec<String>,
131 pub importing_file: PathBuf,
133 pub line: usize,
135}
136
137impl FixtureDatabase {
138 pub(crate) fn extract_fixture_imports(
141 &self,
142 stmts: &[Stmt],
143 file_path: &Path,
144 line_index: &[usize],
145 ) -> Vec<FixtureImport> {
146 let mut imports = Vec::new();
147
148 for stmt in stmts {
149 if let Stmt::ImportFrom(import_from) = stmt {
150 let mut module = import_from
152 .module
153 .as_ref()
154 .map(|m| m.to_string())
155 .unwrap_or_default();
156
157 if let Some(ref level) = import_from.level {
162 let dots = ".".repeat(level.to_usize());
163 module = dots + &module;
164 }
165
166 if self.is_standard_library_module(&module) {
168 continue;
169 }
170
171 let line =
172 self.get_line_from_offset(import_from.range.start().to_usize(), line_index);
173
174 let is_star = import_from
176 .names
177 .iter()
178 .any(|alias| alias.name.as_str() == "*");
179
180 if is_star {
181 imports.push(FixtureImport {
182 module_path: module,
183 is_star_import: true,
184 imported_names: Vec::new(),
185 importing_file: file_path.to_path_buf(),
186 line,
187 });
188 } else {
189 let names: Vec<String> = import_from
191 .names
192 .iter()
193 .map(|alias| alias.asname.as_ref().unwrap_or(&alias.name).to_string())
194 .collect();
195
196 if !names.is_empty() {
197 imports.push(FixtureImport {
198 module_path: module,
199 is_star_import: false,
200 imported_names: names,
201 importing_file: file_path.to_path_buf(),
202 line,
203 });
204 }
205 }
206 }
207 }
208
209 imports
210 }
211
212 pub(crate) fn extract_pytest_plugins(&self, stmts: &[Stmt]) -> Vec<String> {
222 let mut modules = Vec::new();
223
224 for stmt in stmts {
225 let value = match stmt {
226 Stmt::Assign(assign) => {
227 let is_pytest_plugins = assign.targets.iter().any(|target| {
228 matches!(target, Expr::Name(name) if name.id.as_str() == "pytest_plugins")
229 });
230 if !is_pytest_plugins {
231 continue;
232 }
233 assign.value.as_ref()
234 }
235 Stmt::AnnAssign(ann_assign) => {
236 let is_pytest_plugins = matches!(
237 ann_assign.target.as_ref(),
238 Expr::Name(name) if name.id.as_str() == "pytest_plugins"
239 );
240 if !is_pytest_plugins {
241 continue;
242 }
243 match ann_assign.value.as_ref() {
244 Some(v) => v.as_ref(),
245 None => continue,
246 }
247 }
248 _ => continue,
249 };
250
251 modules.clear();
253
254 match value {
255 Expr::Constant(c) => {
256 if let rustpython_parser::ast::Constant::Str(s) = &c.value {
257 modules.push(s.to_string());
258 }
259 }
260 Expr::List(list) => {
261 for elt in &list.elts {
262 if let Expr::Constant(c) = elt {
263 if let rustpython_parser::ast::Constant::Str(s) = &c.value {
264 modules.push(s.to_string());
265 }
266 }
267 }
268 }
269 Expr::Tuple(tuple) => {
270 for elt in &tuple.elts {
271 if let Expr::Constant(c) = elt {
272 if let rustpython_parser::ast::Constant::Str(s) = &c.value {
273 modules.push(s.to_string());
274 }
275 }
276 }
277 }
278 _ => {
279 debug!("Ignoring dynamic pytest_plugins value (not a string/list/tuple)");
280 }
281 }
282 }
283
284 modules
285 }
286
287 fn is_standard_library_module(&self, module: &str) -> bool {
290 is_stdlib_module(module)
291 }
292
293 pub(crate) fn resolve_module_to_file(
296 &self,
297 module_path: &str,
298 importing_file: &Path,
299 ) -> Option<PathBuf> {
300 debug!(
301 "Resolving module '{}' from file {:?}",
302 module_path, importing_file
303 );
304
305 let parent_dir = importing_file.parent()?;
306
307 if module_path.starts_with('.') {
308 self.resolve_relative_import(module_path, parent_dir)
310 } else {
311 self.resolve_absolute_import(module_path, parent_dir)
313 }
314 }
315
316 fn resolve_relative_import(&self, module_path: &str, base_dir: &Path) -> Option<PathBuf> {
318 let mut current_dir = base_dir.to_path_buf();
319 let mut chars = module_path.chars().peekable();
320
321 while chars.peek() == Some(&'.') {
323 chars.next();
324 if chars.peek() != Some(&'.') {
325 break;
327 }
328 current_dir = current_dir.parent()?.to_path_buf();
330 }
331
332 let remaining: String = chars.collect();
333 if remaining.is_empty() {
334 let init_path = current_dir.join("__init__.py");
336 if init_path.exists() {
337 return Some(init_path);
338 }
339 return None;
340 }
341
342 self.find_module_file(&remaining, ¤t_dir)
343 }
344
345 fn resolve_absolute_import(&self, module_path: &str, start_dir: &Path) -> Option<PathBuf> {
348 let mut current_dir = start_dir.to_path_buf();
349
350 loop {
351 if let Some(path) = self.find_module_file(module_path, ¤t_dir) {
352 return Some(path);
353 }
354
355 match current_dir.parent() {
357 Some(parent) => current_dir = parent.to_path_buf(),
358 None => break,
359 }
360 }
361
362 for sp in self.site_packages_paths.lock().unwrap().iter() {
364 if let Some(path) = self.find_module_file(module_path, sp) {
365 return Some(path);
366 }
367 }
368
369 for install in self.editable_install_roots.lock().unwrap().iter() {
371 if let Some(path) = self.find_module_file(module_path, &install.source_root) {
372 return Some(path);
373 }
374 }
375
376 None
377 }
378
379 fn find_module_file(&self, module_path: &str, base_dir: &Path) -> Option<PathBuf> {
381 let parts: Vec<&str> = module_path.split('.').collect();
382 let mut current_path = base_dir.to_path_buf();
383
384 for (i, part) in parts.iter().enumerate() {
385 let is_last = i == parts.len() - 1;
386
387 if is_last {
388 let py_file = current_path.join(format!("{}.py", part));
390 if py_file.exists() {
391 return Some(py_file);
392 }
393
394 let canonical_py_file = self.get_canonical_path(py_file.clone());
396 if self.file_cache.contains_key(&canonical_py_file) {
397 return Some(py_file);
398 }
399
400 let package_init = current_path.join(part).join("__init__.py");
402 if package_init.exists() {
403 return Some(package_init);
404 }
405
406 let canonical_package_init = self.get_canonical_path(package_init.clone());
408 if self.file_cache.contains_key(&canonical_package_init) {
409 return Some(package_init);
410 }
411 } else {
412 current_path = current_path.join(part);
414 if !current_path.is_dir() {
415 return None;
416 }
417 }
418 }
419
420 None
421 }
422
423 pub fn get_imported_fixtures(
429 &self,
430 file_path: &Path,
431 visited: &mut HashSet<PathBuf>,
432 ) -> HashSet<String> {
433 let canonical_path = self.get_canonical_path(file_path.to_path_buf());
434
435 if visited.contains(&canonical_path) {
437 debug!("Circular import detected for {:?}, skipping", file_path);
438 return HashSet::new();
439 }
440 visited.insert(canonical_path.clone());
441
442 let Some(content) = self.get_file_content(&canonical_path) else {
444 return HashSet::new();
445 };
446
447 let content_hash = Self::hash_content(&content);
448 let current_version = self
449 .definitions_version
450 .load(std::sync::atomic::Ordering::SeqCst);
451
452 if let Some(cached) = self.imported_fixtures_cache.get(&canonical_path) {
454 let (cached_content_hash, cached_version, cached_fixtures) = cached.value();
455 if *cached_content_hash == content_hash && *cached_version == current_version {
456 debug!("Cache hit for imported fixtures in {:?}", canonical_path);
457 return cached_fixtures.as_ref().clone();
458 }
459 }
460
461 let imported_fixtures = self.compute_imported_fixtures(&canonical_path, &content, visited);
463
464 self.imported_fixtures_cache.insert(
466 canonical_path.clone(),
467 (
468 content_hash,
469 current_version,
470 Arc::new(imported_fixtures.clone()),
471 ),
472 );
473
474 info!(
475 "Found {} imported fixtures for {:?}: {:?}",
476 imported_fixtures.len(),
477 file_path,
478 imported_fixtures
479 );
480
481 imported_fixtures
482 }
483
484 fn compute_imported_fixtures(
486 &self,
487 canonical_path: &Path,
488 content: &str,
489 visited: &mut HashSet<PathBuf>,
490 ) -> HashSet<String> {
491 let mut imported_fixtures = HashSet::new();
492
493 let Some(parsed) = self.get_parsed_ast(canonical_path, content) else {
494 return imported_fixtures;
495 };
496
497 let line_index = self.get_line_index(canonical_path, content);
498
499 if let rustpython_parser::ast::Mod::Module(module) = parsed.as_ref() {
500 let imports = self.extract_fixture_imports(&module.body, canonical_path, &line_index);
501
502 for import in imports {
503 let Some(resolved_path) =
505 self.resolve_module_to_file(&import.module_path, canonical_path)
506 else {
507 debug!(
508 "Could not resolve module '{}' from {:?}",
509 import.module_path, canonical_path
510 );
511 continue;
512 };
513
514 let resolved_canonical = self.get_canonical_path(resolved_path);
515
516 debug!(
517 "Resolved import '{}' to {:?}",
518 import.module_path, resolved_canonical
519 );
520
521 if import.is_star_import {
522 if let Some(file_fixtures) = self.file_definitions.get(&resolved_canonical) {
525 for fixture_name in file_fixtures.iter() {
526 imported_fixtures.insert(fixture_name.clone());
527 }
528 }
529
530 let transitive = self.get_imported_fixtures(&resolved_canonical, visited);
532 imported_fixtures.extend(transitive);
533 } else {
534 for name in &import.imported_names {
536 if self.definitions.contains_key(name) {
537 imported_fixtures.insert(name.clone());
538 }
539 }
540 }
541 }
542
543 let plugin_modules = self.extract_pytest_plugins(&module.body);
545 for module_path in plugin_modules {
546 let Some(resolved_path) = self.resolve_module_to_file(&module_path, canonical_path)
547 else {
548 debug!(
549 "Could not resolve pytest_plugins module '{}' from {:?}",
550 module_path, canonical_path
551 );
552 continue;
553 };
554
555 let resolved_canonical = self.get_canonical_path(resolved_path);
556
557 debug!(
558 "Resolved pytest_plugins '{}' to {:?}",
559 module_path, resolved_canonical
560 );
561
562 if let Some(file_fixtures) = self.file_definitions.get(&resolved_canonical) {
563 for fixture_name in file_fixtures.iter() {
564 imported_fixtures.insert(fixture_name.clone());
565 }
566 }
567
568 let transitive = self.get_imported_fixtures(&resolved_canonical, visited);
569 imported_fixtures.extend(transitive);
570 }
571 }
572
573 imported_fixtures
574 }
575
576 pub fn is_fixture_imported_in_file(&self, fixture_name: &str, file_path: &Path) -> bool {
579 let mut visited = HashSet::new();
580 let imported = self.get_imported_fixtures(file_path, &mut visited);
581 imported.contains(fixture_name)
582 }
583}
584
585pub(crate) fn is_stdlib_module(module: &str) -> bool {
595 let first_part = module.split('.').next().unwrap_or(module);
596 if let Some(runtime) = RUNTIME_STDLIB_MODULES.get() {
597 runtime.contains(first_part)
598 } else {
599 STDLIB_MODULES.contains(first_part)
600 }
601}
602
603fn find_venv_python(venv_path: &Path) -> Option<PathBuf> {
610 for name in &["python3", "python"] {
612 let candidate = venv_path.join("bin").join(name);
613 if candidate.is_file() {
614 return Some(candidate);
615 }
616 }
617 for name in &["python3.exe", "python.exe"] {
619 let candidate = venv_path.join("Scripts").join(name);
620 if candidate.is_file() {
621 return Some(candidate);
622 }
623 }
624 None
625}
626
627pub(crate) fn try_init_stdlib_from_python(venv_path: &Path) -> bool {
646 if RUNTIME_STDLIB_MODULES.get().is_some() {
648 return true;
649 }
650
651 let Some(python) = find_venv_python(venv_path) else {
652 debug!(
653 "try_init_stdlib_from_python: no Python binary found in {:?}",
654 venv_path
655 );
656 return false;
657 };
658
659 debug!(
660 "try_init_stdlib_from_python: querying stdlib module names via {:?}",
661 python
662 );
663
664 let output = match std::process::Command::new(&python)
667 .args([
668 "-I",
669 "-c",
670 "import sys; print('\\n'.join(sorted(sys.stdlib_module_names)))",
671 ])
672 .output()
673 {
674 Ok(o) => o,
675 Err(e) => {
676 warn!(
677 "try_init_stdlib_from_python: failed to run {:?}: {}",
678 python, e
679 );
680 return false;
681 }
682 };
683
684 if !output.status.success() {
685 debug!(
687 "try_init_stdlib_from_python: Python exited with {:?} \
688 (Python < 3.10 or other error) — using built-in stdlib list",
689 output.status.code()
690 );
691 return false;
692 }
693
694 let stdout = match std::str::from_utf8(&output.stdout) {
695 Ok(s) => s,
696 Err(e) => {
697 warn!(
698 "try_init_stdlib_from_python: Python output is not valid UTF-8: {}",
699 e
700 );
701 return false;
702 }
703 };
704
705 let modules: HashSet<String> = stdout
706 .lines()
707 .map(str::trim)
708 .filter(|l| !l.is_empty())
709 .map(str::to_owned)
710 .collect();
711
712 if modules.is_empty() {
713 warn!("try_init_stdlib_from_python: Python returned an empty module list");
714 return false;
715 }
716
717 info!(
718 "try_init_stdlib_from_python: loaded {} stdlib module names from {:?}",
719 modules.len(),
720 python
721 );
722
723 let _ = RUNTIME_STDLIB_MODULES.set(modules);
726 true
727}
728
729impl FixtureDatabase {
730 pub(crate) fn file_path_to_module_path(file_path: &Path) -> Option<String> {
748 let stem = file_path.file_stem()?.to_str()?;
749 let mut components = if stem == "__init__" {
755 vec![]
756 } else {
757 vec![stem.to_string()]
758 };
759 let mut current = file_path.parent()?;
760
761 loop {
762 if current.join("__init__.py").exists() {
763 let name = current.file_name().and_then(|n| n.to_str())?;
764 components.push(name.to_string());
765 match current.parent() {
766 Some(parent) => current = parent,
767 None => break,
768 }
769 } else {
770 break;
771 }
772 }
773
774 if components.is_empty() {
775 return None;
776 }
777
778 components.reverse();
779 Some(components.join("."))
780 }
781
782 fn resolve_relative_module_to_string(
789 &self,
790 module: &str,
791 level: usize,
792 fixture_file: &Path,
793 ) -> Option<String> {
794 let mut base = fixture_file.parent()?;
797 for _ in 1..level {
798 base = base.parent()?;
799 }
800
801 let target = if module.is_empty() {
803 base.join("__init__.py")
805 } else {
806 let rel_path = module.replace('.', "/");
808 base.join(format!("{}.py", rel_path))
809 };
810
811 Self::file_path_to_module_path(&target)
813 }
814
815 pub(crate) fn build_name_to_import_map(
833 &self,
834 stmts: &[Stmt],
835 fixture_file: &Path,
836 ) -> HashMap<String, TypeImportSpec> {
837 let mut map = HashMap::new();
838
839 for stmt in stmts {
840 match stmt {
841 Stmt::Import(import_stmt) => {
842 for alias in &import_stmt.names {
843 let module = alias.name.to_string();
844 let (check_name, import_statement) = if let Some(ref asname) = alias.asname
845 {
846 let asname_str = asname.to_string();
847 (
848 asname_str.clone(),
849 format!("import {} as {}", module, asname_str),
850 )
851 } else {
852 let top_level = module.split('.').next().unwrap_or(&module).to_string();
853 (top_level, format!("import {}", module))
854 };
855 map.insert(
856 check_name.clone(),
857 TypeImportSpec {
858 check_name,
859 import_statement,
860 },
861 );
862 }
863 }
864
865 Stmt::ImportFrom(import_from) => {
866 let level = import_from
867 .level
868 .as_ref()
869 .map(|l| l.to_usize())
870 .unwrap_or(0);
871 let raw_module = import_from
872 .module
873 .as_ref()
874 .map(|m| m.to_string())
875 .unwrap_or_default();
876
877 let abs_module = if level > 0 {
879 match self.resolve_relative_module_to_string(
880 &raw_module,
881 level,
882 fixture_file,
883 ) {
884 Some(m) => m,
885 None => {
886 debug!(
887 "Could not resolve relative import '.{}' from {:?}, skipping",
888 raw_module, fixture_file
889 );
890 continue;
891 }
892 }
893 } else {
894 raw_module
895 };
896
897 for alias in &import_from.names {
898 if alias.name.as_str() == "*" {
899 continue; }
901 let name = alias.name.to_string();
902 let (check_name, import_statement) = if let Some(ref asname) = alias.asname
903 {
904 let asname_str = asname.to_string();
905 (
906 asname_str.clone(),
907 format!("from {} import {} as {}", abs_module, name, asname_str),
908 )
909 } else {
910 (name.clone(), format!("from {} import {}", abs_module, name))
911 };
912 map.insert(
913 check_name.clone(),
914 TypeImportSpec {
915 check_name,
916 import_statement,
917 },
918 );
919 }
920 }
921
922 _ => {}
923 }
924 }
925
926 map
927 }
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933 use std::fs;
934
935 struct TempDir(std::path::PathBuf);
937
938 impl TempDir {
939 fn new(name: &str) -> Self {
940 let path = std::env::temp_dir().join(name);
941 fs::create_dir_all(&path).unwrap();
942 Self(path)
943 }
944
945 fn path(&self) -> &std::path::Path {
946 &self.0
947 }
948 }
949
950 impl Drop for TempDir {
951 fn drop(&mut self) {
952 let _ = fs::remove_dir_all(&self.0);
953 }
954 }
955
956 fn touch(path: &std::path::Path) {
960 fs::create_dir_all(path.parent().unwrap()).unwrap();
961 fs::write(path, b"").unwrap();
962 }
963
964 #[test]
965 fn test_find_venv_python_unix_python3() {
966 let dir = TempDir::new("fvp_unix_py3");
967 touch(&dir.path().join("bin/python3"));
968 let result = find_venv_python(dir.path());
969 assert_eq!(result, Some(dir.path().join("bin/python3")));
970 }
971
972 #[test]
973 fn test_find_venv_python_unix_python_fallback() {
974 let dir = TempDir::new("fvp_unix_py");
976 touch(&dir.path().join("bin/python"));
977 let result = find_venv_python(dir.path());
978 assert_eq!(result, Some(dir.path().join("bin/python")));
979 }
980
981 #[test]
982 fn test_find_venv_python_unix_prefers_python3_over_python() {
983 let dir = TempDir::new("fvp_unix_prefer");
984 touch(&dir.path().join("bin/python3"));
985 touch(&dir.path().join("bin/python"));
986 let result = find_venv_python(dir.path());
987 assert_eq!(
988 result,
989 Some(dir.path().join("bin/python3")),
990 "python3 should be preferred over python"
991 );
992 }
993
994 #[test]
995 fn test_find_venv_python_windows_style() {
996 let dir = TempDir::new("fvp_win_py");
997 touch(&dir.path().join("Scripts/python.exe"));
998 let result = find_venv_python(dir.path());
999 assert_eq!(result, Some(dir.path().join("Scripts/python.exe")));
1000 }
1001
1002 #[test]
1003 fn test_find_venv_python_windows_prefers_python3_exe() {
1004 let dir = TempDir::new("fvp_win_prefer");
1005 touch(&dir.path().join("Scripts/python3.exe"));
1006 touch(&dir.path().join("Scripts/python.exe"));
1007 let result = find_venv_python(dir.path());
1008 assert_eq!(
1009 result,
1010 Some(dir.path().join("Scripts/python3.exe")),
1011 "python3.exe should be preferred over python.exe"
1012 );
1013 }
1014
1015 #[test]
1016 fn test_find_venv_python_not_found() {
1017 let dir = TempDir::new("fvp_empty");
1018 assert_eq!(find_venv_python(dir.path()), None);
1019 }
1020
1021 #[test]
1022 fn test_find_venv_python_wrong_layout() {
1023 let dir = TempDir::new("fvp_wrong_layout");
1025 touch(&dir.path().join("python3"));
1026 assert_eq!(find_venv_python(dir.path()), None);
1027 }
1028
1029 #[test]
1030 fn test_try_init_stdlib_no_python_returns_false_or_already_set() {
1031 let dir = TempDir::new("fvp_no_python");
1036 let _ = try_init_stdlib_from_python(dir.path());
1037 assert!(is_stdlib_module("os"), "os must always be stdlib");
1038 assert!(is_stdlib_module("sys"), "sys must always be stdlib");
1039 assert!(!is_stdlib_module("pytest"), "pytest is not stdlib");
1040 assert!(!is_stdlib_module("flask"), "flask is not stdlib");
1041 }
1042
1043 #[test]
1046 fn test_module_path_regular_file_no_package() {
1047 let dir = TempDir::new("fptmp_plain");
1049 let file = dir.path().join("conftest.py");
1050 fs::write(&file, "").unwrap();
1051 assert_eq!(
1053 FixtureDatabase::file_path_to_module_path(&file),
1054 Some("conftest".to_string())
1055 );
1056 }
1057
1058 #[test]
1059 fn test_module_path_regular_file_in_package() {
1060 let dir = TempDir::new("fptmp_pkg");
1062 let pkg = dir.path().join("pkg");
1063 fs::create_dir_all(&pkg).unwrap();
1064 fs::write(pkg.join("__init__.py"), "").unwrap();
1065 let file = pkg.join("module.py");
1066 fs::write(&file, "").unwrap();
1067 assert_eq!(
1068 FixtureDatabase::file_path_to_module_path(&file),
1069 Some("pkg.module".to_string())
1070 );
1071 }
1072
1073 #[test]
1074 fn test_module_path_init_file_is_package_root() {
1075 let dir = TempDir::new("fptmp_init");
1078 let pkg = dir.path().join("pkg");
1079 fs::create_dir_all(&pkg).unwrap();
1080 let init = pkg.join("__init__.py");
1081 fs::write(&init, "").unwrap();
1082 assert_eq!(
1083 FixtureDatabase::file_path_to_module_path(&init),
1084 Some("pkg".to_string())
1085 );
1086 }
1087
1088 #[test]
1089 fn test_module_path_nested_init_file() {
1090 let dir = TempDir::new("fptmp_nested_init");
1092 let pkg = dir.path().join("pkg");
1093 let sub = pkg.join("sub");
1094 fs::create_dir_all(&sub).unwrap();
1095 fs::write(pkg.join("__init__.py"), "").unwrap();
1096 let init = sub.join("__init__.py");
1097 fs::write(&init, "").unwrap();
1098 assert_eq!(
1099 FixtureDatabase::file_path_to_module_path(&init),
1100 Some("pkg.sub".to_string())
1101 );
1102 }
1103
1104 #[test]
1105 fn test_module_path_nested_package() {
1106 let dir = TempDir::new("fptmp_nested");
1108 let pkg = dir.path().join("pkg");
1109 let sub = pkg.join("sub");
1110 fs::create_dir_all(&sub).unwrap();
1111 fs::write(pkg.join("__init__.py"), "").unwrap();
1112 fs::write(sub.join("__init__.py"), "").unwrap();
1113 let file = sub.join("module.py");
1114 fs::write(&file, "").unwrap();
1115 assert_eq!(
1116 FixtureDatabase::file_path_to_module_path(&file),
1117 Some("pkg.sub.module".to_string())
1118 );
1119 }
1120
1121 #[test]
1122 fn test_module_path_conftest_in_package() {
1123 let dir = TempDir::new("fptmp_conftest_pkg");
1125 let pkg = dir.path().join("mypkg");
1126 fs::create_dir_all(&pkg).unwrap();
1127 fs::write(pkg.join("__init__.py"), "").unwrap();
1128 let file = pkg.join("conftest.py");
1129 fs::write(&file, "").unwrap();
1130 assert_eq!(
1131 FixtureDatabase::file_path_to_module_path(&file),
1132 Some("mypkg.conftest".to_string())
1133 );
1134 }
1135
1136 #[test]
1144 fn test_build_map_dotted_import_keyed_by_top_level() {
1145 let db = FixtureDatabase::new();
1150 let map = db.get_name_to_import_map(
1151 &PathBuf::from("/tmp/test_bm_dotted.py"),
1152 "import collections.abc\n",
1153 );
1154 let spec = map
1155 .get("collections")
1156 .expect("key 'collections' must be present");
1157 assert_eq!(spec.check_name, "collections");
1158 assert_eq!(spec.import_statement, "import collections.abc");
1159 assert!(
1160 !map.contains_key("collections.abc"),
1161 "full dotted path must not be a key; only the top-level bound name is"
1162 );
1163 }
1164
1165 #[test]
1166 fn test_build_map_two_level_dotted_import_keyed_by_top_level() {
1167 let db = FixtureDatabase::new();
1170 let map = db.get_name_to_import_map(
1171 &PathBuf::from("/tmp/test_bm_two_level.py"),
1172 "import xml.etree.ElementTree\n",
1173 );
1174 let spec = map.get("xml").expect("key 'xml' must be present");
1175 assert_eq!(spec.check_name, "xml");
1176 assert_eq!(spec.import_statement, "import xml.etree.ElementTree");
1177 assert!(
1178 !map.contains_key("xml.etree.ElementTree"),
1179 "full dotted path must not be a key"
1180 );
1181 assert!(
1182 !map.contains_key("xml.etree"),
1183 "partial dotted path must not be a key"
1184 );
1185 }
1186
1187 #[test]
1188 fn test_build_map_simple_import_unaffected() {
1189 let db = FixtureDatabase::new();
1192 let map =
1193 db.get_name_to_import_map(&PathBuf::from("/tmp/test_bm_simple.py"), "import pathlib\n");
1194 let spec = map.get("pathlib").expect("key 'pathlib' must be present");
1195 assert_eq!(spec.check_name, "pathlib");
1196 assert_eq!(spec.import_statement, "import pathlib");
1197 }
1198
1199 #[test]
1200 fn test_build_map_aliased_dotted_import_unaffected() {
1201 let db = FixtureDatabase::new();
1204 let map = db.get_name_to_import_map(
1205 &PathBuf::from("/tmp/test_bm_aliased.py"),
1206 "import collections.abc as abc_mod\n",
1207 );
1208 let spec = map.get("abc_mod").expect("key 'abc_mod' must be present");
1209 assert_eq!(spec.check_name, "abc_mod");
1210 assert_eq!(spec.import_statement, "import collections.abc as abc_mod");
1211 assert!(
1212 !map.contains_key("collections"),
1213 "top-level name must not be keyed when alias present"
1214 );
1215 assert!(
1216 !map.contains_key("collections.abc"),
1217 "dotted path must not be keyed when alias present"
1218 );
1219 }
1220}