1use dashmap::DashMap;
2use rustpython_parser::ast::{Expr, Stmt};
3use rustpython_parser::{parse, Mode};
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use tracing::{debug, info, warn};
7use walkdir::WalkDir;
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct FixtureDefinition {
11 pub name: String,
12 pub file_path: PathBuf,
13 pub line: usize,
14 pub docstring: Option<String>,
15}
16
17#[derive(Debug, Clone)]
18pub struct FixtureUsage {
19 pub name: String,
20 pub file_path: PathBuf,
21 pub line: usize,
22 pub start_char: usize, pub end_char: usize, }
25
26#[derive(Debug)]
27pub struct FixtureDatabase {
28 definitions: Arc<DashMap<String, Vec<FixtureDefinition>>>,
30 usages: Arc<DashMap<PathBuf, Vec<FixtureUsage>>>,
32 file_cache: Arc<DashMap<PathBuf, String>>,
34}
35
36impl Default for FixtureDatabase {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl FixtureDatabase {
43 pub fn new() -> Self {
44 Self {
45 definitions: Arc::new(DashMap::new()),
46 usages: Arc::new(DashMap::new()),
47 file_cache: Arc::new(DashMap::new()),
48 }
49 }
50
51 pub fn scan_workspace(&self, root_path: &Path) {
53 info!("Scanning workspace: {:?}", root_path);
54 let mut file_count = 0;
55
56 for entry in WalkDir::new(root_path).into_iter().filter_map(|e| e.ok()) {
57 let path = entry.path();
58
59 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
61 if filename == "conftest.py"
62 || filename.starts_with("test_") && filename.ends_with(".py")
63 || filename.ends_with("_test.py")
64 {
65 debug!("Found test/conftest file: {:?}", path);
66 if let Ok(content) = std::fs::read_to_string(path) {
67 self.analyze_file(path.to_path_buf(), &content);
68 file_count += 1;
69 }
70 }
71 }
72 }
73
74 info!("Workspace scan complete. Processed {} files", file_count);
75
76 self.scan_venv_fixtures(root_path);
78
79 info!("Total fixtures defined: {}", self.definitions.len());
80 info!("Total files with fixture usages: {}", self.usages.len());
81 }
82
83 fn scan_venv_fixtures(&self, root_path: &Path) {
85 info!("Scanning for pytest plugins in virtual environment");
86
87 let venv_paths = vec![
89 root_path.join(".venv"),
90 root_path.join("venv"),
91 root_path.join("env"),
92 ];
93
94 info!("Checking for venv in: {:?}", root_path);
95 for venv_path in &venv_paths {
96 debug!("Checking venv path: {:?}", venv_path);
97 if venv_path.exists() {
98 info!("Found virtual environment at: {:?}", venv_path);
99 self.scan_venv_site_packages(venv_path);
100 return;
101 } else {
102 debug!(" Does not exist: {:?}", venv_path);
103 }
104 }
105
106 if let Ok(venv) = std::env::var("VIRTUAL_ENV") {
108 info!("Found VIRTUAL_ENV environment variable: {}", venv);
109 let venv_path = PathBuf::from(venv);
110 if venv_path.exists() {
111 info!("Using VIRTUAL_ENV: {:?}", venv_path);
112 self.scan_venv_site_packages(&venv_path);
113 return;
114 } else {
115 warn!("VIRTUAL_ENV path does not exist: {:?}", venv_path);
116 }
117 } else {
118 debug!("No VIRTUAL_ENV environment variable set");
119 }
120
121 warn!("No virtual environment found - third-party fixtures will not be available");
122 }
123
124 fn scan_venv_site_packages(&self, venv_path: &Path) {
125 info!("Scanning venv site-packages in: {:?}", venv_path);
126
127 let lib_path = venv_path.join("lib");
129 debug!("Checking lib path: {:?}", lib_path);
130
131 if lib_path.exists() {
132 if let Ok(entries) = std::fs::read_dir(&lib_path) {
134 for entry in entries.flatten() {
135 let path = entry.path();
136 let dirname = path.file_name().unwrap_or_default().to_string_lossy();
137 debug!("Found in lib: {:?}", dirname);
138
139 if path.is_dir() && dirname.starts_with("python") {
140 let site_packages = path.join("site-packages");
141 debug!("Checking site-packages: {:?}", site_packages);
142
143 if site_packages.exists() {
144 info!("Found site-packages: {:?}", site_packages);
145 self.scan_pytest_plugins(&site_packages);
146 return;
147 }
148 }
149 }
150 }
151 }
152
153 let windows_site_packages = venv_path.join("Lib/site-packages");
155 debug!("Checking Windows path: {:?}", windows_site_packages);
156 if windows_site_packages.exists() {
157 info!("Found site-packages (Windows): {:?}", windows_site_packages);
158 self.scan_pytest_plugins(&windows_site_packages);
159 return;
160 }
161
162 warn!("Could not find site-packages in venv: {:?}", venv_path);
163 }
164
165 fn scan_pytest_plugins(&self, site_packages: &Path) {
166 info!("Scanning pytest plugins in: {:?}", site_packages);
167
168 let pytest_packages = vec![
170 "pytest_mock",
171 "pytest-mock",
172 "pytest_asyncio",
173 "pytest-asyncio",
174 "pytest_django",
175 "pytest-django",
176 "pytest_cov",
177 "pytest-cov",
178 "pytest_xdist",
179 "pytest-xdist",
180 "pytest_fixtures",
181 ];
182
183 let mut plugin_count = 0;
184
185 for entry in std::fs::read_dir(site_packages).into_iter().flatten() {
186 let entry = match entry {
187 Ok(e) => e,
188 Err(_) => continue,
189 };
190
191 let path = entry.path();
192 let filename = path.file_name().unwrap_or_default().to_string_lossy();
193
194 let is_pytest_package = pytest_packages.iter().any(|pkg| filename.contains(pkg))
196 || filename.starts_with("pytest")
197 || filename.contains("_pytest");
198
199 if is_pytest_package && path.is_dir() {
200 if filename.ends_with(".dist-info") || filename.ends_with(".egg-info") {
202 debug!("Skipping dist-info directory: {:?}", filename);
203 continue;
204 }
205
206 info!("Scanning pytest plugin: {:?}", path);
207 plugin_count += 1;
208 self.scan_plugin_directory(&path);
209 } else {
210 if filename.contains("mock") {
212 debug!("Found mock-related package (not scanning): {:?}", filename);
213 }
214 }
215 }
216
217 info!("Scanned {} pytest plugin packages", plugin_count);
218 }
219
220 fn scan_plugin_directory(&self, plugin_dir: &Path) {
221 for entry in WalkDir::new(plugin_dir)
223 .max_depth(3) .into_iter()
225 .filter_map(|e| e.ok())
226 {
227 let path = entry.path();
228
229 if path.extension().and_then(|s| s.to_str()) == Some("py") {
230 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
232 if filename.starts_with("test_") || filename.contains("__pycache__") {
234 continue;
235 }
236
237 debug!("Scanning plugin file: {:?}", path);
238 if let Ok(content) = std::fs::read_to_string(path) {
239 self.analyze_file(path.to_path_buf(), &content);
240 }
241 }
242 }
243 }
244 }
245
246 pub fn analyze_file(&self, file_path: PathBuf, content: &str) {
248 debug!("Analyzing file: {:?}", file_path);
249
250 self.file_cache
252 .insert(file_path.clone(), content.to_string());
253
254 let parsed = match parse(content, Mode::Module, "") {
256 Ok(ast) => ast,
257 Err(e) => {
258 warn!("Failed to parse {:?}: {:?}", file_path, e);
259 return;
260 }
261 };
262
263 self.usages.remove(&file_path);
265
266 for mut entry in self.definitions.iter_mut() {
269 entry.value_mut().retain(|def| def.file_path != file_path);
270 }
271 self.definitions.retain(|_, defs| !defs.is_empty());
273
274 let is_conftest = file_path
276 .file_name()
277 .map(|n| n == "conftest.py")
278 .unwrap_or(false);
279 debug!("is_conftest: {}", is_conftest);
280
281 if let rustpython_parser::ast::Mod::Module(module) = parsed {
283 debug!("Module has {} statements", module.body.len());
284 for stmt in &module.body {
285 self.visit_stmt(stmt, &file_path, is_conftest, content);
286 }
287 }
288
289 debug!("Analysis complete for {:?}", file_path);
290 }
291
292 fn visit_stmt(&self, stmt: &Stmt, file_path: &PathBuf, _is_conftest: bool, content: &str) {
293 if let Stmt::Assign(assign) = stmt {
295 self.visit_assignment_fixture(assign, file_path, content);
296 }
297
298 let (func_name, decorator_list, args, range, body) = match stmt {
300 Stmt::FunctionDef(func_def) => (
301 func_def.name.as_str(),
302 &func_def.decorator_list,
303 &func_def.args,
304 func_def.range,
305 &func_def.body,
306 ),
307 Stmt::AsyncFunctionDef(func_def) => (
308 func_def.name.as_str(),
309 &func_def.decorator_list,
310 &func_def.args,
311 func_def.range,
312 &func_def.body,
313 ),
314 _ => return,
315 };
316
317 debug!("Found function: {}", func_name);
318
319 debug!(
321 "Function {} has {} decorators",
322 func_name,
323 decorator_list.len()
324 );
325 let is_fixture = decorator_list.iter().any(|dec| {
326 let result = Self::is_fixture_decorator(dec);
327 if result {
328 debug!(" Decorator matched as fixture!");
329 }
330 result
331 });
332
333 if is_fixture {
334 let line = self.get_line_from_offset(range.start().to_usize(), content);
336
337 let docstring = self.extract_docstring(body);
339
340 info!(
341 "Found fixture definition: {} at {:?}:{}",
342 func_name, file_path, line
343 );
344 if let Some(ref doc) = docstring {
345 debug!(" Docstring: {}", doc);
346 }
347
348 let definition = FixtureDefinition {
349 name: func_name.to_string(),
350 file_path: file_path.clone(),
351 line,
352 docstring,
353 };
354
355 self.definitions
356 .entry(func_name.to_string())
357 .or_default()
358 .push(definition);
359
360 for arg in &args.args {
362 let arg_name = arg.def.arg.as_str();
363 if arg_name != "self" && arg_name != "request" {
364 let arg_line =
367 self.get_line_from_offset(arg.def.range.start().to_usize(), content);
368 let start_char = self
369 .get_char_position_from_offset(arg.def.range.start().to_usize(), content);
370 let end_char =
371 self.get_char_position_from_offset(arg.def.range.end().to_usize(), content);
372
373 info!(
374 "Found fixture dependency: {} at {:?}:{}:{}",
375 arg_name, file_path, arg_line, start_char
376 );
377
378 let usage = FixtureUsage {
379 name: arg_name.to_string(),
380 file_path: file_path.clone(),
381 line: arg_line, start_char,
383 end_char,
384 };
385
386 self.usages
387 .entry(file_path.clone())
388 .or_default()
389 .push(usage);
390 }
391 }
392 }
393
394 if func_name.starts_with("test_") {
396 debug!("Found test function: {}", func_name);
397
398 for arg in &args.args {
400 let arg_name = arg.def.arg.as_str();
401 if arg_name != "self" {
402 let arg_offset = arg.def.range.start().to_usize();
406 let arg_line = self.get_line_from_offset(arg_offset, content);
407 let start_char = self.get_char_position_from_offset(arg_offset, content);
408 let end_char =
409 self.get_char_position_from_offset(arg.def.range.end().to_usize(), content);
410
411 debug!(
412 "Parameter {} at offset {}, calculated line {}, char {}",
413 arg_name, arg_offset, arg_line, start_char
414 );
415 info!(
416 "Found fixture usage: {} at {:?}:{}:{}",
417 arg_name, file_path, arg_line, start_char
418 );
419
420 let usage = FixtureUsage {
421 name: arg_name.to_string(),
422 file_path: file_path.clone(),
423 line: arg_line, start_char,
425 end_char,
426 };
427
428 self.usages
430 .entry(file_path.clone())
431 .or_default()
432 .push(usage);
433 }
434 }
435 }
436 }
437
438 fn visit_assignment_fixture(
439 &self,
440 assign: &rustpython_parser::ast::StmtAssign,
441 file_path: &PathBuf,
442 content: &str,
443 ) {
444 if let Expr::Call(outer_call) = &*assign.value {
448 if let Expr::Call(inner_call) = &*outer_call.func {
450 if Self::is_fixture_decorator(&inner_call.func) {
451 for target in &assign.targets {
454 if let Expr::Name(name) = target {
455 let fixture_name = name.id.as_str();
456 let line =
457 self.get_line_from_offset(assign.range.start().to_usize(), content);
458
459 info!(
460 "Found fixture assignment: {} at {:?}:{}",
461 fixture_name, file_path, line
462 );
463
464 let definition = FixtureDefinition {
466 name: fixture_name.to_string(),
467 file_path: file_path.clone(),
468 line,
469 docstring: None,
470 };
471
472 self.definitions
473 .entry(fixture_name.to_string())
474 .or_default()
475 .push(definition);
476 }
477 }
478 }
479 }
480 }
481 }
482
483 fn is_fixture_decorator(expr: &Expr) -> bool {
484 match expr {
485 Expr::Name(name) => name.id.as_str() == "fixture",
486 Expr::Attribute(attr) => {
487 if let Expr::Name(value) = &*attr.value {
489 value.id.as_str() == "pytest" && attr.attr.as_str() == "fixture"
490 } else {
491 false
492 }
493 }
494 Expr::Call(call) => {
495 Self::is_fixture_decorator(&call.func)
497 }
498 _ => false,
499 }
500 }
501
502 fn extract_docstring(&self, body: &[Stmt]) -> Option<String> {
503 if let Some(Stmt::Expr(expr_stmt)) = body.first() {
505 if let Expr::Constant(constant) = &*expr_stmt.value {
506 if let rustpython_parser::ast::Constant::Str(s) = &constant.value {
508 return Some(self.format_docstring(s.to_string()));
509 }
510 }
511 }
512 None
513 }
514
515 fn format_docstring(&self, docstring: String) -> String {
516 let lines: Vec<&str> = docstring.lines().collect();
519
520 if lines.is_empty() {
521 return String::new();
522 }
523
524 let mut start = 0;
526 let mut end = lines.len();
527
528 while start < lines.len() && lines[start].trim().is_empty() {
529 start += 1;
530 }
531
532 while end > start && lines[end - 1].trim().is_empty() {
533 end -= 1;
534 }
535
536 if start >= end {
537 return String::new();
538 }
539
540 let lines = &lines[start..end];
541
542 let mut min_indent = usize::MAX;
544 for (i, line) in lines.iter().enumerate() {
545 if i == 0 && !line.trim().is_empty() {
546 continue;
548 }
549
550 if !line.trim().is_empty() {
551 let indent = line.len() - line.trim_start().len();
552 min_indent = min_indent.min(indent);
553 }
554 }
555
556 if min_indent == usize::MAX {
557 min_indent = 0;
558 }
559
560 let mut result = Vec::new();
562 for (i, line) in lines.iter().enumerate() {
563 if i == 0 {
564 result.push(line.trim().to_string());
566 } else if line.trim().is_empty() {
567 result.push(String::new());
569 } else {
570 let dedented = if line.len() > min_indent {
572 &line[min_indent..]
573 } else {
574 line.trim_start()
575 };
576 result.push(dedented.to_string());
577 }
578 }
579
580 result.join("\n")
582 }
583
584 fn get_line_from_offset(&self, offset: usize, content: &str) -> usize {
585 content[..offset].matches('\n').count() + 1
587 }
588
589 fn get_char_position_from_offset(&self, offset: usize, content: &str) -> usize {
590 if let Some(line_start) = content[..offset].rfind('\n') {
592 offset - line_start - 1
594 } else {
595 offset
597 }
598 }
599
600 pub fn find_fixture_definition(
602 &self,
603 file_path: &Path,
604 line: u32,
605 character: u32,
606 ) -> Option<FixtureDefinition> {
607 debug!(
608 "find_fixture_definition: file={:?}, line={}, char={}",
609 file_path, line, character
610 );
611
612 let target_line = (line + 1) as usize; let content = if let Some(cached) = self.file_cache.get(file_path) {
616 cached.clone()
617 } else {
618 std::fs::read_to_string(file_path).ok()?
619 };
620 let lines: Vec<&str> = content.lines().collect();
621
622 if target_line == 0 || target_line > lines.len() {
623 return None;
624 }
625
626 let line_content = lines[target_line - 1];
627 debug!("Line content: {}", line_content);
628
629 let word_at_cursor = self.extract_word_at_position(line_content, character as usize)?;
631 debug!("Word at cursor: {:?}", word_at_cursor);
632
633 let current_fixture_def = self.get_fixture_definition_at_line(file_path, target_line);
636
637 if let Some(usages) = self.usages.get(file_path) {
640 for usage in usages.iter() {
641 if usage.line == target_line && usage.name == word_at_cursor {
642 let cursor_pos = character as usize;
644 if cursor_pos >= usage.start_char && cursor_pos < usage.end_char {
645 debug!(
646 "Cursor at {} is within usage range {}-{}: {}",
647 cursor_pos, usage.start_char, usage.end_char, usage.name
648 );
649 info!("Found fixture usage at cursor position: {}", usage.name);
650
651 if let Some(ref current_def) = current_fixture_def {
653 if current_def.name == word_at_cursor {
654 info!(
655 "Self-referencing fixture detected, finding parent definition"
656 );
657 return self.find_closest_definition_excluding(
658 file_path,
659 &usage.name,
660 Some(current_def),
661 );
662 }
663 }
664
665 return self.find_closest_definition(file_path, &usage.name);
667 }
668 }
669 }
670 }
671
672 debug!("Word at cursor '{}' is not a fixture usage", word_at_cursor);
673 None
674 }
675
676 fn get_fixture_definition_at_line(
678 &self,
679 file_path: &Path,
680 line: usize,
681 ) -> Option<FixtureDefinition> {
682 for entry in self.definitions.iter() {
683 for def in entry.value().iter() {
684 if def.file_path == file_path && def.line == line {
685 return Some(def.clone());
686 }
687 }
688 }
689 None
690 }
691
692 pub fn get_definition_at_line(
695 &self,
696 file_path: &Path,
697 line: usize,
698 fixture_name: &str,
699 ) -> Option<FixtureDefinition> {
700 if let Some(definitions) = self.definitions.get(fixture_name) {
701 for def in definitions.iter() {
702 if def.file_path == file_path && def.line == line {
703 return Some(def.clone());
704 }
705 }
706 }
707 None
708 }
709
710 fn find_closest_definition(
711 &self,
712 file_path: &Path,
713 fixture_name: &str,
714 ) -> Option<FixtureDefinition> {
715 let definitions = self.definitions.get(fixture_name)?;
716
717 debug!(
720 "Checking for fixture {} in same file: {:?}",
721 fixture_name, file_path
722 );
723 let same_file_defs: Vec<_> = definitions
724 .iter()
725 .filter(|def| def.file_path == file_path)
726 .collect();
727
728 if !same_file_defs.is_empty() {
729 let last_def = same_file_defs.iter().max_by_key(|def| def.line).unwrap();
731 info!(
732 "Found fixture {} in same file at line {} (using last definition)",
733 fixture_name, last_def.line
734 );
735 return Some((*last_def).clone());
736 }
737
738 let mut current_dir = file_path.parent()?;
741
742 debug!(
743 "Searching for fixture {} in conftest.py files starting from {:?}",
744 fixture_name, current_dir
745 );
746 loop {
747 let conftest_path = current_dir.join("conftest.py");
749 debug!(" Checking conftest.py at: {:?}", conftest_path);
750
751 for def in definitions.iter() {
752 if def.file_path == conftest_path {
753 info!(
754 "Found fixture {} in conftest.py: {:?}",
755 fixture_name, conftest_path
756 );
757 return Some(def.clone());
758 }
759 }
760
761 match current_dir.parent() {
763 Some(parent) => current_dir = parent,
764 None => break,
765 }
766 }
767
768 warn!(
770 "No fixture {} found following priority rules, returning first available",
771 fixture_name
772 );
773 definitions.iter().next().cloned()
774 }
775
776 fn find_closest_definition_excluding(
779 &self,
780 file_path: &Path,
781 fixture_name: &str,
782 exclude: Option<&FixtureDefinition>,
783 ) -> Option<FixtureDefinition> {
784 let definitions = self.definitions.get(fixture_name)?;
785
786 debug!(
790 "Checking for fixture {} in same file: {:?} (excluding: {:?})",
791 fixture_name, file_path, exclude
792 );
793 let same_file_defs: Vec<_> = definitions
794 .iter()
795 .filter(|def| {
796 if def.file_path != file_path {
797 return false;
798 }
799 if let Some(excluded) = exclude {
801 if def == &excluded {
802 debug!("Skipping excluded definition at line {}", def.line);
803 return false;
804 }
805 }
806 true
807 })
808 .collect();
809
810 if !same_file_defs.is_empty() {
811 let last_def = same_file_defs.iter().max_by_key(|def| def.line).unwrap();
813 info!(
814 "Found fixture {} in same file at line {} (using last definition, excluding specified)",
815 fixture_name, last_def.line
816 );
817 return Some((*last_def).clone());
818 }
819
820 let mut current_dir = file_path.parent()?;
822
823 debug!(
824 "Searching for fixture {} in conftest.py files starting from {:?}",
825 fixture_name, current_dir
826 );
827 loop {
828 let conftest_path = current_dir.join("conftest.py");
829 debug!(" Checking conftest.py at: {:?}", conftest_path);
830
831 for def in definitions.iter() {
832 if def.file_path == conftest_path {
833 if let Some(excluded) = exclude {
835 if def == excluded {
836 debug!("Skipping excluded definition at line {}", def.line);
837 continue;
838 }
839 }
840 info!(
841 "Found fixture {} in conftest.py: {:?}",
842 fixture_name, conftest_path
843 );
844 return Some(def.clone());
845 }
846 }
847
848 match current_dir.parent() {
850 Some(parent) => current_dir = parent,
851 None => break,
852 }
853 }
854
855 warn!(
857 "No fixture {} found following priority rules, returning first available (excluding specified)",
858 fixture_name
859 );
860 definitions
861 .iter()
862 .find(|def| {
863 if let Some(excluded) = exclude {
864 def != &excluded
865 } else {
866 true
867 }
868 })
869 .cloned()
870 }
871
872 pub fn find_fixture_at_position(
874 &self,
875 file_path: &Path,
876 line: u32,
877 character: u32,
878 ) -> Option<String> {
879 let target_line = (line + 1) as usize; debug!(
882 "find_fixture_at_position: file={:?}, line={}, char={}",
883 file_path, target_line, character
884 );
885
886 let content = if let Some(cached) = self.file_cache.get(file_path) {
888 cached.clone()
889 } else {
890 std::fs::read_to_string(file_path).ok()?
891 };
892 let lines: Vec<&str> = content.lines().collect();
893
894 if target_line == 0 || target_line > lines.len() {
895 return None;
896 }
897
898 let line_content = lines[target_line - 1];
899 debug!("Line content: {}", line_content);
900
901 let word_at_cursor = self.extract_word_at_position(line_content, character as usize);
903 debug!("Word at cursor: {:?}", word_at_cursor);
904
905 if let Some(usages) = self.usages.get(file_path) {
908 for usage in usages.iter() {
909 if usage.line == target_line {
910 let cursor_pos = character as usize;
912 if cursor_pos >= usage.start_char && cursor_pos < usage.end_char {
913 debug!(
914 "Cursor at {} is within usage range {}-{}: {}",
915 cursor_pos, usage.start_char, usage.end_char, usage.name
916 );
917 info!("Found fixture usage at cursor position: {}", usage.name);
918 return Some(usage.name.clone());
919 }
920 }
921 }
922 }
923
924 for entry in self.definitions.iter() {
927 for def in entry.value().iter() {
928 if def.file_path == file_path && def.line == target_line {
929 if let Some(ref word) = word_at_cursor {
931 if word == &def.name {
932 info!(
933 "Found fixture definition name at cursor position: {}",
934 def.name
935 );
936 return Some(def.name.clone());
937 }
938 }
939 }
942 }
943 }
944
945 debug!("No fixture found at cursor position");
946 None
947 }
948
949 fn extract_word_at_position(&self, line: &str, character: usize) -> Option<String> {
950 let chars: Vec<char> = line.chars().collect();
951
952 if character > chars.len() {
954 return None;
955 }
956
957 if character < chars.len() {
959 let c = chars[character];
960 if c.is_alphanumeric() || c == '_' {
961 let mut start = character;
963 while start > 0 {
964 let prev_c = chars[start - 1];
965 if !prev_c.is_alphanumeric() && prev_c != '_' {
966 break;
967 }
968 start -= 1;
969 }
970
971 let mut end = character;
972 while end < chars.len() {
973 let curr_c = chars[end];
974 if !curr_c.is_alphanumeric() && curr_c != '_' {
975 break;
976 }
977 end += 1;
978 }
979
980 if start < end {
981 return Some(chars[start..end].iter().collect());
982 }
983 }
984 }
985
986 None
987 }
988
989 pub fn find_fixture_references(&self, fixture_name: &str) -> Vec<FixtureUsage> {
991 info!("Finding all references for fixture: {}", fixture_name);
992
993 let mut all_references = Vec::new();
994
995 for entry in self.usages.iter() {
997 let file_path = entry.key();
998 let usages = entry.value();
999
1000 for usage in usages.iter() {
1002 if usage.name == fixture_name {
1003 debug!(
1004 "Found reference to {} in {:?} at line {}",
1005 fixture_name, file_path, usage.line
1006 );
1007 all_references.push(usage.clone());
1008 }
1009 }
1010 }
1011
1012 info!(
1013 "Found {} total references for fixture: {}",
1014 all_references.len(),
1015 fixture_name
1016 );
1017 all_references
1018 }
1019
1020 pub fn find_references_for_definition(
1027 &self,
1028 definition: &FixtureDefinition,
1029 ) -> Vec<FixtureUsage> {
1030 info!(
1031 "Finding references for specific definition: {} at {:?}:{}",
1032 definition.name, definition.file_path, definition.line
1033 );
1034
1035 let mut matching_references = Vec::new();
1036
1037 for entry in self.usages.iter() {
1039 let file_path = entry.key();
1040 let usages = entry.value();
1041
1042 for usage in usages.iter() {
1043 if usage.name == definition.name {
1044 let fixture_def_at_line =
1047 self.get_fixture_definition_at_line(file_path, usage.line);
1048
1049 let resolved_def = if let Some(ref current_def) = fixture_def_at_line {
1050 if current_def.name == usage.name {
1051 debug!(
1053 "Usage at {:?}:{} is self-referencing, excluding definition at line {}",
1054 file_path, usage.line, current_def.line
1055 );
1056 self.find_closest_definition_excluding(
1057 file_path,
1058 &usage.name,
1059 Some(current_def),
1060 )
1061 } else {
1062 self.find_closest_definition(file_path, &usage.name)
1064 }
1065 } else {
1066 self.find_closest_definition(file_path, &usage.name)
1068 };
1069
1070 if let Some(resolved_def) = resolved_def {
1071 if resolved_def == *definition {
1072 debug!(
1073 "Usage at {:?}:{} resolves to our definition",
1074 file_path, usage.line
1075 );
1076 matching_references.push(usage.clone());
1077 } else {
1078 debug!(
1079 "Usage at {:?}:{} resolves to different definition at {:?}:{}",
1080 file_path, usage.line, resolved_def.file_path, resolved_def.line
1081 );
1082 }
1083 }
1084 }
1085 }
1086 }
1087
1088 info!(
1089 "Found {} references that resolve to this specific definition",
1090 matching_references.len()
1091 );
1092 matching_references
1093 }
1094}
1095
1096#[cfg(test)]
1097mod tests {
1098 use super::*;
1099 use std::path::PathBuf;
1100
1101 #[test]
1102 fn test_fixture_definition_detection() {
1103 let db = FixtureDatabase::new();
1104
1105 let conftest_content = r#"
1106import pytest
1107
1108@pytest.fixture
1109def my_fixture():
1110 return 42
1111
1112@fixture
1113def another_fixture():
1114 return "hello"
1115"#;
1116
1117 let conftest_path = PathBuf::from("/tmp/test/conftest.py");
1118 db.analyze_file(conftest_path.clone(), conftest_content);
1119
1120 assert!(db.definitions.contains_key("my_fixture"));
1122 assert!(db.definitions.contains_key("another_fixture"));
1123
1124 let my_fixture_defs = db.definitions.get("my_fixture").unwrap();
1126 assert_eq!(my_fixture_defs.len(), 1);
1127 assert_eq!(my_fixture_defs[0].name, "my_fixture");
1128 assert_eq!(my_fixture_defs[0].file_path, conftest_path);
1129 }
1130
1131 #[test]
1132 fn test_fixture_usage_detection() {
1133 let db = FixtureDatabase::new();
1134
1135 let test_content = r#"
1136def test_something(my_fixture, another_fixture):
1137 assert my_fixture == 42
1138 assert another_fixture == "hello"
1139
1140def test_other(my_fixture):
1141 assert my_fixture > 0
1142"#;
1143
1144 let test_path = PathBuf::from("/tmp/test/test_example.py");
1145 db.analyze_file(test_path.clone(), test_content);
1146
1147 assert!(db.usages.contains_key(&test_path));
1149
1150 let usages = db.usages.get(&test_path).unwrap();
1151 assert!(usages.iter().any(|u| u.name == "my_fixture"));
1153 assert!(usages.iter().any(|u| u.name == "another_fixture"));
1154 }
1155
1156 #[test]
1157 fn test_go_to_definition() {
1158 let db = FixtureDatabase::new();
1159
1160 let conftest_content = r#"
1162import pytest
1163
1164@pytest.fixture
1165def my_fixture():
1166 return 42
1167"#;
1168
1169 let conftest_path = PathBuf::from("/tmp/test/conftest.py");
1170 db.analyze_file(conftest_path.clone(), conftest_content);
1171
1172 let test_content = r#"
1174def test_something(my_fixture):
1175 assert my_fixture == 42
1176"#;
1177
1178 let test_path = PathBuf::from("/tmp/test/test_example.py");
1179 db.analyze_file(test_path.clone(), test_content);
1180
1181 let definition = db.find_fixture_definition(&test_path, 1, 19);
1186
1187 assert!(definition.is_some(), "Definition should be found");
1188 let def = definition.unwrap();
1189 assert_eq!(def.name, "my_fixture");
1190 assert_eq!(def.file_path, conftest_path);
1191 }
1192
1193 #[test]
1194 fn test_fixture_decorator_variations() {
1195 let db = FixtureDatabase::new();
1196
1197 let conftest_content = r#"
1198import pytest
1199from pytest import fixture
1200
1201@pytest.fixture
1202def fixture1():
1203 pass
1204
1205@pytest.fixture()
1206def fixture2():
1207 pass
1208
1209@fixture
1210def fixture3():
1211 pass
1212
1213@fixture()
1214def fixture4():
1215 pass
1216"#;
1217
1218 let conftest_path = PathBuf::from("/tmp/test/conftest.py");
1219 db.analyze_file(conftest_path, conftest_content);
1220
1221 assert!(db.definitions.contains_key("fixture1"));
1223 assert!(db.definitions.contains_key("fixture2"));
1224 assert!(db.definitions.contains_key("fixture3"));
1225 assert!(db.definitions.contains_key("fixture4"));
1226 }
1227
1228 #[test]
1229 fn test_fixture_in_test_file() {
1230 let db = FixtureDatabase::new();
1231
1232 let test_content = r#"
1234import pytest
1235
1236@pytest.fixture
1237def local_fixture():
1238 return 42
1239
1240def test_something(local_fixture):
1241 assert local_fixture == 42
1242"#;
1243
1244 let test_path = PathBuf::from("/tmp/test/test_example.py");
1245 db.analyze_file(test_path.clone(), test_content);
1246
1247 assert!(db.definitions.contains_key("local_fixture"));
1249
1250 let local_fixture_defs = db.definitions.get("local_fixture").unwrap();
1251 assert_eq!(local_fixture_defs.len(), 1);
1252 assert_eq!(local_fixture_defs[0].name, "local_fixture");
1253 assert_eq!(local_fixture_defs[0].file_path, test_path);
1254
1255 assert!(db.usages.contains_key(&test_path));
1257 let usages = db.usages.get(&test_path).unwrap();
1258 assert!(usages.iter().any(|u| u.name == "local_fixture"));
1259
1260 let usage_line = usages
1262 .iter()
1263 .find(|u| u.name == "local_fixture")
1264 .map(|u| u.line)
1265 .unwrap();
1266
1267 let definition = db.find_fixture_definition(&test_path, (usage_line - 1) as u32, 19);
1269 assert!(
1270 definition.is_some(),
1271 "Should find definition for fixture in same file. Line: {}, char: 19",
1272 usage_line
1273 );
1274 let def = definition.unwrap();
1275 assert_eq!(def.name, "local_fixture");
1276 assert_eq!(def.file_path, test_path);
1277 }
1278
1279 #[test]
1280 fn test_async_test_functions() {
1281 let db = FixtureDatabase::new();
1282
1283 let test_content = r#"
1285import pytest
1286
1287@pytest.fixture
1288def my_fixture():
1289 return 42
1290
1291async def test_async_function(my_fixture):
1292 assert my_fixture == 42
1293
1294def test_sync_function(my_fixture):
1295 assert my_fixture == 42
1296"#;
1297
1298 let test_path = PathBuf::from("/tmp/test/test_async.py");
1299 db.analyze_file(test_path.clone(), test_content);
1300
1301 assert!(db.definitions.contains_key("my_fixture"));
1303
1304 assert!(db.usages.contains_key(&test_path));
1306 let usages = db.usages.get(&test_path).unwrap();
1307
1308 let fixture_usages: Vec<_> = usages.iter().filter(|u| u.name == "my_fixture").collect();
1310 assert_eq!(
1311 fixture_usages.len(),
1312 2,
1313 "Should detect fixture usage in both async and sync tests"
1314 );
1315 }
1316
1317 #[test]
1318 fn test_extract_word_at_position() {
1319 let db = FixtureDatabase::new();
1320
1321 let line = "def test_something(my_fixture):";
1323
1324 assert_eq!(
1326 db.extract_word_at_position(line, 19),
1327 Some("my_fixture".to_string())
1328 );
1329
1330 assert_eq!(
1332 db.extract_word_at_position(line, 20),
1333 Some("my_fixture".to_string())
1334 );
1335
1336 assert_eq!(
1338 db.extract_word_at_position(line, 28),
1339 Some("my_fixture".to_string())
1340 );
1341
1342 assert_eq!(
1344 db.extract_word_at_position(line, 0),
1345 Some("def".to_string())
1346 );
1347
1348 assert_eq!(db.extract_word_at_position(line, 3), None);
1350
1351 assert_eq!(
1353 db.extract_word_at_position(line, 4),
1354 Some("test_something".to_string())
1355 );
1356
1357 assert_eq!(db.extract_word_at_position(line, 18), None);
1359
1360 assert_eq!(db.extract_word_at_position(line, 29), None);
1362
1363 assert_eq!(db.extract_word_at_position(line, 31), None);
1365 }
1366
1367 #[test]
1368 fn test_extract_word_at_position_fixture_definition() {
1369 let db = FixtureDatabase::new();
1370
1371 let line = "@pytest.fixture";
1372
1373 assert_eq!(db.extract_word_at_position(line, 0), None);
1375
1376 assert_eq!(
1378 db.extract_word_at_position(line, 1),
1379 Some("pytest".to_string())
1380 );
1381
1382 assert_eq!(db.extract_word_at_position(line, 7), None);
1384
1385 assert_eq!(
1387 db.extract_word_at_position(line, 8),
1388 Some("fixture".to_string())
1389 );
1390
1391 let line2 = "def foo(other_fixture):";
1392
1393 assert_eq!(
1395 db.extract_word_at_position(line2, 0),
1396 Some("def".to_string())
1397 );
1398
1399 assert_eq!(db.extract_word_at_position(line2, 3), None);
1401
1402 assert_eq!(
1404 db.extract_word_at_position(line2, 4),
1405 Some("foo".to_string())
1406 );
1407
1408 assert_eq!(
1410 db.extract_word_at_position(line2, 8),
1411 Some("other_fixture".to_string())
1412 );
1413
1414 assert_eq!(db.extract_word_at_position(line2, 7), None);
1416 }
1417
1418 #[test]
1419 fn test_word_detection_only_on_fixtures() {
1420 let db = FixtureDatabase::new();
1421
1422 let conftest_content = r#"
1424import pytest
1425
1426@pytest.fixture
1427def my_fixture():
1428 return 42
1429"#;
1430 let conftest_path = PathBuf::from("/tmp/test/conftest.py");
1431 db.analyze_file(conftest_path.clone(), conftest_content);
1432
1433 let test_content = r#"
1435def test_something(my_fixture, regular_param):
1436 assert my_fixture == 42
1437"#;
1438 let test_path = PathBuf::from("/tmp/test/test_example.py");
1439 db.analyze_file(test_path.clone(), test_content);
1440
1441 assert_eq!(db.find_fixture_definition(&test_path, 1, 0), None);
1450
1451 assert_eq!(db.find_fixture_definition(&test_path, 1, 4), None);
1453
1454 let result = db.find_fixture_definition(&test_path, 1, 19);
1456 assert!(result.is_some());
1457 let def = result.unwrap();
1458 assert_eq!(def.name, "my_fixture");
1459
1460 assert_eq!(db.find_fixture_definition(&test_path, 1, 31), None);
1462
1463 assert_eq!(db.find_fixture_definition(&test_path, 1, 18), None); assert_eq!(db.find_fixture_definition(&test_path, 1, 29), None); }
1467
1468 #[test]
1469 fn test_self_referencing_fixture() {
1470 let db = FixtureDatabase::new();
1471
1472 let parent_conftest_content = r#"
1474import pytest
1475
1476@pytest.fixture
1477def foo():
1478 return "parent"
1479"#;
1480 let parent_conftest_path = PathBuf::from("/tmp/test/conftest.py");
1481 db.analyze_file(parent_conftest_path.clone(), parent_conftest_content);
1482
1483 let child_conftest_content = r#"
1485import pytest
1486
1487@pytest.fixture
1488def foo(foo):
1489 return foo + " child"
1490"#;
1491 let child_conftest_path = PathBuf::from("/tmp/test/subdir/conftest.py");
1492 db.analyze_file(child_conftest_path.clone(), child_conftest_content);
1493
1494 let result = db.find_fixture_definition(&child_conftest_path, 4, 8);
1500
1501 assert!(
1502 result.is_some(),
1503 "Should find parent definition for self-referencing fixture"
1504 );
1505 let def = result.unwrap();
1506 assert_eq!(def.name, "foo");
1507 assert_eq!(
1508 def.file_path, parent_conftest_path,
1509 "Should resolve to parent conftest.py, not the child"
1510 );
1511 assert_eq!(def.line, 5, "Should point to line 5 of parent conftest.py");
1512 }
1513
1514 #[test]
1515 fn test_fixture_overriding_same_file() {
1516 let db = FixtureDatabase::new();
1517
1518 let test_content = r#"
1520import pytest
1521
1522@pytest.fixture
1523def my_fixture():
1524 return "first"
1525
1526@pytest.fixture
1527def my_fixture():
1528 return "second"
1529
1530def test_something(my_fixture):
1531 assert my_fixture == "second"
1532"#;
1533 let test_path = PathBuf::from("/tmp/test/test_example.py");
1534 db.analyze_file(test_path.clone(), test_content);
1535
1536 let result = db.find_fixture_definition(&test_path, 11, 19);
1545
1546 assert!(result.is_some(), "Should find fixture definition");
1547 let def = result.unwrap();
1548 assert_eq!(def.name, "my_fixture");
1549 assert_eq!(def.file_path, test_path);
1550 }
1554
1555 #[test]
1556 fn test_fixture_overriding_conftest_hierarchy() {
1557 let db = FixtureDatabase::new();
1558
1559 let root_conftest_content = r#"
1561import pytest
1562
1563@pytest.fixture
1564def shared_fixture():
1565 return "root"
1566"#;
1567 let root_conftest_path = PathBuf::from("/tmp/test/conftest.py");
1568 db.analyze_file(root_conftest_path.clone(), root_conftest_content);
1569
1570 let sub_conftest_content = r#"
1572import pytest
1573
1574@pytest.fixture
1575def shared_fixture():
1576 return "subdir"
1577"#;
1578 let sub_conftest_path = PathBuf::from("/tmp/test/subdir/conftest.py");
1579 db.analyze_file(sub_conftest_path.clone(), sub_conftest_content);
1580
1581 let test_content = r#"
1583def test_something(shared_fixture):
1584 assert shared_fixture == "subdir"
1585"#;
1586 let test_path = PathBuf::from("/tmp/test/subdir/test_example.py");
1587 db.analyze_file(test_path.clone(), test_content);
1588
1589 let result = db.find_fixture_definition(&test_path, 1, 19);
1595
1596 assert!(result.is_some(), "Should find fixture definition");
1597 let def = result.unwrap();
1598 assert_eq!(def.name, "shared_fixture");
1599 assert_eq!(
1600 def.file_path, sub_conftest_path,
1601 "Should resolve to closest conftest.py"
1602 );
1603
1604 let parent_test_content = r#"
1606def test_parent(shared_fixture):
1607 assert shared_fixture == "root"
1608"#;
1609 let parent_test_path = PathBuf::from("/tmp/test/test_parent.py");
1610 db.analyze_file(parent_test_path.clone(), parent_test_content);
1611
1612 let result = db.find_fixture_definition(&parent_test_path, 1, 16);
1613
1614 assert!(result.is_some(), "Should find fixture definition");
1615 let def = result.unwrap();
1616 assert_eq!(def.name, "shared_fixture");
1617 assert_eq!(
1618 def.file_path, root_conftest_path,
1619 "Should resolve to root conftest.py"
1620 );
1621 }
1622
1623 #[test]
1624 fn test_scoped_references() {
1625 let db = FixtureDatabase::new();
1626
1627 let root_conftest_content = r#"
1629import pytest
1630
1631@pytest.fixture
1632def shared_fixture():
1633 return "root"
1634"#;
1635 let root_conftest_path = PathBuf::from("/tmp/test/conftest.py");
1636 db.analyze_file(root_conftest_path.clone(), root_conftest_content);
1637
1638 let sub_conftest_content = r#"
1640import pytest
1641
1642@pytest.fixture
1643def shared_fixture():
1644 return "subdir"
1645"#;
1646 let sub_conftest_path = PathBuf::from("/tmp/test/subdir/conftest.py");
1647 db.analyze_file(sub_conftest_path.clone(), sub_conftest_content);
1648
1649 let root_test_content = r#"
1651def test_root(shared_fixture):
1652 assert shared_fixture == "root"
1653"#;
1654 let root_test_path = PathBuf::from("/tmp/test/test_root.py");
1655 db.analyze_file(root_test_path.clone(), root_test_content);
1656
1657 let sub_test_content = r#"
1659def test_sub(shared_fixture):
1660 assert shared_fixture == "subdir"
1661"#;
1662 let sub_test_path = PathBuf::from("/tmp/test/subdir/test_sub.py");
1663 db.analyze_file(sub_test_path.clone(), sub_test_content);
1664
1665 let sub_test2_content = r#"
1667def test_sub2(shared_fixture):
1668 assert shared_fixture == "subdir"
1669"#;
1670 let sub_test2_path = PathBuf::from("/tmp/test/subdir/test_sub2.py");
1671 db.analyze_file(sub_test2_path.clone(), sub_test2_content);
1672
1673 let root_definitions = db.definitions.get("shared_fixture").unwrap();
1675 let root_definition = root_definitions
1676 .iter()
1677 .find(|d| d.file_path == root_conftest_path)
1678 .unwrap();
1679
1680 let sub_definition = root_definitions
1682 .iter()
1683 .find(|d| d.file_path == sub_conftest_path)
1684 .unwrap();
1685
1686 let root_refs = db.find_references_for_definition(root_definition);
1688
1689 assert_eq!(
1691 root_refs.len(),
1692 1,
1693 "Root definition should have 1 reference (from root test)"
1694 );
1695 assert_eq!(root_refs[0].file_path, root_test_path);
1696
1697 let sub_refs = db.find_references_for_definition(sub_definition);
1699
1700 assert_eq!(
1702 sub_refs.len(),
1703 2,
1704 "Subdir definition should have 2 references (from subdir tests)"
1705 );
1706
1707 let sub_ref_paths: Vec<_> = sub_refs.iter().map(|r| &r.file_path).collect();
1708 assert!(sub_ref_paths.contains(&&sub_test_path));
1709 assert!(sub_ref_paths.contains(&&sub_test2_path));
1710
1711 let all_refs = db.find_fixture_references("shared_fixture");
1713 assert_eq!(
1714 all_refs.len(),
1715 3,
1716 "Should find 3 total references across all scopes"
1717 );
1718 }
1719
1720 #[test]
1721 fn test_multiline_parameters() {
1722 let db = FixtureDatabase::new();
1723
1724 let conftest_content = r#"
1726import pytest
1727
1728@pytest.fixture
1729def foo():
1730 return 42
1731"#;
1732 let conftest_path = PathBuf::from("/tmp/test/conftest.py");
1733 db.analyze_file(conftest_path.clone(), conftest_content);
1734
1735 let test_content = r#"
1737def test_xxx(
1738 foo,
1739):
1740 assert foo == 42
1741"#;
1742 let test_path = PathBuf::from("/tmp/test/test_example.py");
1743 db.analyze_file(test_path.clone(), test_content);
1744
1745 if let Some(usages) = db.usages.get(&test_path) {
1751 println!("Usages recorded:");
1752 for usage in usages.iter() {
1753 println!(" {} at line {} (1-indexed)", usage.name, usage.line);
1754 }
1755 } else {
1756 println!("No usages recorded for test file");
1757 }
1758
1759 let result = db.find_fixture_definition(&test_path, 2, 4);
1768
1769 assert!(
1770 result.is_some(),
1771 "Should find fixture definition when cursor is on parameter line"
1772 );
1773 let def = result.unwrap();
1774 assert_eq!(def.name, "foo");
1775 }
1776
1777 #[test]
1778 fn test_find_references_from_usage() {
1779 let db = FixtureDatabase::new();
1780
1781 let test_content = r#"
1783import pytest
1784
1785@pytest.fixture
1786def foo(): ...
1787
1788
1789def test_xxx(foo):
1790 pass
1791"#;
1792 let test_path = PathBuf::from("/tmp/test/test_example.py");
1793 db.analyze_file(test_path.clone(), test_content);
1794
1795 let foo_defs = db.definitions.get("foo").unwrap();
1797 assert_eq!(foo_defs.len(), 1, "Should have exactly one foo definition");
1798 let foo_def = &foo_defs[0];
1799 assert_eq!(foo_def.line, 5, "foo definition should be on line 5");
1800
1801 let refs_from_def = db.find_references_for_definition(foo_def);
1803 println!("References from definition:");
1804 for r in &refs_from_def {
1805 println!(" {} at line {}", r.name, r.line);
1806 }
1807
1808 assert_eq!(
1809 refs_from_def.len(),
1810 1,
1811 "Should find 1 usage reference (test_xxx parameter)"
1812 );
1813 assert_eq!(refs_from_def[0].line, 8, "Usage should be on line 8");
1814
1815 let fixture_name = db.find_fixture_at_position(&test_path, 7, 13);
1818 println!(
1819 "\nfind_fixture_at_position(line 7, char 13): {:?}",
1820 fixture_name
1821 );
1822
1823 assert_eq!(
1824 fixture_name,
1825 Some("foo".to_string()),
1826 "Should find fixture name at usage position"
1827 );
1828
1829 let resolved_def = db.find_fixture_definition(&test_path, 7, 13);
1830 println!(
1831 "\nfind_fixture_definition(line 7, char 13): {:?}",
1832 resolved_def.as_ref().map(|d| (d.line, &d.file_path))
1833 );
1834
1835 assert!(resolved_def.is_some(), "Should resolve usage to definition");
1836 assert_eq!(
1837 resolved_def.unwrap(),
1838 *foo_def,
1839 "Should resolve to the correct definition"
1840 );
1841 }
1842
1843 #[test]
1844 fn test_find_references_with_ellipsis_body() {
1845 let db = FixtureDatabase::new();
1847
1848 let test_content = r#"@pytest.fixture
1849def foo(): ...
1850
1851
1852def test_xxx(foo):
1853 pass
1854"#;
1855 let test_path = PathBuf::from("/tmp/test/test_codegen.py");
1856 db.analyze_file(test_path.clone(), test_content);
1857
1858 let foo_defs = db.definitions.get("foo");
1860 println!(
1861 "foo definitions: {:?}",
1862 foo_defs
1863 .as_ref()
1864 .map(|defs| defs.iter().map(|d| d.line).collect::<Vec<_>>())
1865 );
1866
1867 if let Some(usages) = db.usages.get(&test_path) {
1869 println!("usages:");
1870 for u in usages.iter() {
1871 println!(" {} at line {}", u.name, u.line);
1872 }
1873 }
1874
1875 assert!(foo_defs.is_some(), "Should find foo definition");
1876 let foo_def = &foo_defs.unwrap()[0];
1877
1878 let usages = db.usages.get(&test_path).unwrap();
1880 let foo_usage = usages.iter().find(|u| u.name == "foo").unwrap();
1881
1882 let usage_lsp_line = (foo_usage.line - 1) as u32;
1884 println!("\nTesting from usage at LSP line {}", usage_lsp_line);
1885
1886 let fixture_name = db.find_fixture_at_position(&test_path, usage_lsp_line, 13);
1887 assert_eq!(
1888 fixture_name,
1889 Some("foo".to_string()),
1890 "Should find foo at usage"
1891 );
1892
1893 let def_from_usage = db.find_fixture_definition(&test_path, usage_lsp_line, 13);
1894 assert!(
1895 def_from_usage.is_some(),
1896 "Should resolve usage to definition"
1897 );
1898 assert_eq!(def_from_usage.unwrap(), *foo_def);
1899 }
1900
1901 #[test]
1902 fn test_fixture_hierarchy_parent_references() {
1903 let db = FixtureDatabase::new();
1906
1907 let parent_content = r#"
1909import pytest
1910
1911@pytest.fixture
1912def cli_runner():
1913 """Parent fixture"""
1914 return "parent"
1915"#;
1916 let parent_conftest = PathBuf::from("/tmp/project/conftest.py");
1917 db.analyze_file(parent_conftest.clone(), parent_content);
1918
1919 let child_content = r#"
1921import pytest
1922
1923@pytest.fixture
1924def cli_runner(cli_runner):
1925 """Child override that uses parent"""
1926 return cli_runner
1927"#;
1928 let child_conftest = PathBuf::from("/tmp/project/subdir/conftest.py");
1929 db.analyze_file(child_conftest.clone(), child_content);
1930
1931 let test_content = r#"
1933def test_one(cli_runner):
1934 pass
1935
1936def test_two(cli_runner):
1937 pass
1938"#;
1939 let test_path = PathBuf::from("/tmp/project/subdir/test_example.py");
1940 db.analyze_file(test_path.clone(), test_content);
1941
1942 let parent_defs = db.definitions.get("cli_runner").unwrap();
1944 let parent_def = parent_defs
1945 .iter()
1946 .find(|d| d.file_path == parent_conftest)
1947 .unwrap();
1948
1949 println!(
1950 "\nParent definition: {:?}:{}",
1951 parent_def.file_path, parent_def.line
1952 );
1953
1954 let refs = db.find_references_for_definition(parent_def);
1956
1957 println!("\nReferences for parent definition:");
1958 for r in &refs {
1959 println!(" {} at {:?}:{}", r.name, r.file_path, r.line);
1960 }
1961
1962 assert!(
1969 refs.len() <= 2,
1970 "Parent should have at most 2 references: child definition and its parameter, got {}",
1971 refs.len()
1972 );
1973
1974 let child_refs: Vec<_> = refs
1976 .iter()
1977 .filter(|r| r.file_path == child_conftest)
1978 .collect();
1979 assert!(
1980 !child_refs.is_empty(),
1981 "Parent references should include child fixture definition"
1982 );
1983
1984 let test_refs: Vec<_> = refs.iter().filter(|r| r.file_path == test_path).collect();
1986 assert!(
1987 test_refs.is_empty(),
1988 "Parent references should NOT include child's test file usages"
1989 );
1990 }
1991
1992 #[test]
1993 fn test_fixture_hierarchy_child_references() {
1994 let db = FixtureDatabase::new();
1997
1998 let parent_content = r#"
2000import pytest
2001
2002@pytest.fixture
2003def cli_runner():
2004 return "parent"
2005"#;
2006 let parent_conftest = PathBuf::from("/tmp/project/conftest.py");
2007 db.analyze_file(parent_conftest.clone(), parent_content);
2008
2009 let child_content = r#"
2011import pytest
2012
2013@pytest.fixture
2014def cli_runner(cli_runner):
2015 return cli_runner
2016"#;
2017 let child_conftest = PathBuf::from("/tmp/project/subdir/conftest.py");
2018 db.analyze_file(child_conftest.clone(), child_content);
2019
2020 let test_content = r#"
2022def test_one(cli_runner):
2023 pass
2024
2025def test_two(cli_runner):
2026 pass
2027"#;
2028 let test_path = PathBuf::from("/tmp/project/subdir/test_example.py");
2029 db.analyze_file(test_path.clone(), test_content);
2030
2031 let child_defs = db.definitions.get("cli_runner").unwrap();
2033 let child_def = child_defs
2034 .iter()
2035 .find(|d| d.file_path == child_conftest)
2036 .unwrap();
2037
2038 println!(
2039 "\nChild definition: {:?}:{}",
2040 child_def.file_path, child_def.line
2041 );
2042
2043 let refs = db.find_references_for_definition(child_def);
2045
2046 println!("\nReferences for child definition:");
2047 for r in &refs {
2048 println!(" {} at {:?}:{}", r.name, r.file_path, r.line);
2049 }
2050
2051 assert!(
2053 refs.len() >= 2,
2054 "Child should have at least 2 references from test file, got {}",
2055 refs.len()
2056 );
2057
2058 let test_refs: Vec<_> = refs.iter().filter(|r| r.file_path == test_path).collect();
2059 assert_eq!(
2060 test_refs.len(),
2061 2,
2062 "Should have 2 references from test file"
2063 );
2064 }
2065
2066 #[test]
2067 fn test_fixture_hierarchy_child_parameter_references() {
2068 let db = FixtureDatabase::new();
2071
2072 let parent_content = r#"
2074import pytest
2075
2076@pytest.fixture
2077def cli_runner():
2078 return "parent"
2079"#;
2080 let parent_conftest = PathBuf::from("/tmp/project/conftest.py");
2081 db.analyze_file(parent_conftest.clone(), parent_content);
2082
2083 let child_content = r#"
2085import pytest
2086
2087@pytest.fixture
2088def cli_runner(cli_runner):
2089 return cli_runner
2090"#;
2091 let child_conftest = PathBuf::from("/tmp/project/subdir/conftest.py");
2092 db.analyze_file(child_conftest.clone(), child_content);
2093
2094 let resolved_def = db.find_fixture_definition(&child_conftest, 4, 15);
2098
2099 assert!(
2100 resolved_def.is_some(),
2101 "Child parameter should resolve to parent definition"
2102 );
2103
2104 let def = resolved_def.unwrap();
2105 assert_eq!(
2106 def.file_path, parent_conftest,
2107 "Should resolve to parent conftest"
2108 );
2109
2110 let refs = db.find_references_for_definition(&def);
2112
2113 println!("\nReferences for parent (from child parameter):");
2114 for r in &refs {
2115 println!(" {} at {:?}:{}", r.name, r.file_path, r.line);
2116 }
2117
2118 let child_refs: Vec<_> = refs
2120 .iter()
2121 .filter(|r| r.file_path == child_conftest)
2122 .collect();
2123 assert!(
2124 !child_refs.is_empty(),
2125 "Parent references should include child fixture parameter"
2126 );
2127 }
2128
2129 #[test]
2130 fn test_fixture_hierarchy_usage_from_test() {
2131 let db = FixtureDatabase::new();
2134
2135 let parent_content = r#"
2137import pytest
2138
2139@pytest.fixture
2140def cli_runner():
2141 return "parent"
2142"#;
2143 let parent_conftest = PathBuf::from("/tmp/project/conftest.py");
2144 db.analyze_file(parent_conftest.clone(), parent_content);
2145
2146 let child_content = r#"
2148import pytest
2149
2150@pytest.fixture
2151def cli_runner(cli_runner):
2152 return cli_runner
2153"#;
2154 let child_conftest = PathBuf::from("/tmp/project/subdir/conftest.py");
2155 db.analyze_file(child_conftest.clone(), child_content);
2156
2157 let test_content = r#"
2159def test_one(cli_runner):
2160 pass
2161
2162def test_two(cli_runner):
2163 pass
2164
2165def test_three(cli_runner):
2166 pass
2167"#;
2168 let test_path = PathBuf::from("/tmp/project/subdir/test_example.py");
2169 db.analyze_file(test_path.clone(), test_content);
2170
2171 let resolved_def = db.find_fixture_definition(&test_path, 1, 13);
2173
2174 assert!(
2175 resolved_def.is_some(),
2176 "Usage should resolve to child definition"
2177 );
2178
2179 let def = resolved_def.unwrap();
2180 assert_eq!(
2181 def.file_path, child_conftest,
2182 "Should resolve to child conftest (not parent)"
2183 );
2184
2185 let refs = db.find_references_for_definition(&def);
2187
2188 println!("\nReferences for child (from test usage):");
2189 for r in &refs {
2190 println!(" {} at {:?}:{}", r.name, r.file_path, r.line);
2191 }
2192
2193 let test_refs: Vec<_> = refs.iter().filter(|r| r.file_path == test_path).collect();
2195 assert_eq!(test_refs.len(), 3, "Should find all 3 usages in test file");
2196 }
2197
2198 #[test]
2199 fn test_fixture_hierarchy_multiple_levels() {
2200 let db = FixtureDatabase::new();
2202
2203 let grandparent_content = r#"
2205import pytest
2206
2207@pytest.fixture
2208def db():
2209 return "grandparent_db"
2210"#;
2211 let grandparent_conftest = PathBuf::from("/tmp/project/conftest.py");
2212 db.analyze_file(grandparent_conftest.clone(), grandparent_content);
2213
2214 let parent_content = r#"
2216import pytest
2217
2218@pytest.fixture
2219def db(db):
2220 return f"parent_{db}"
2221"#;
2222 let parent_conftest = PathBuf::from("/tmp/project/api/conftest.py");
2223 db.analyze_file(parent_conftest.clone(), parent_content);
2224
2225 let child_content = r#"
2227import pytest
2228
2229@pytest.fixture
2230def db(db):
2231 return f"child_{db}"
2232"#;
2233 let child_conftest = PathBuf::from("/tmp/project/api/tests/conftest.py");
2234 db.analyze_file(child_conftest.clone(), child_content);
2235
2236 let test_content = r#"
2238def test_db(db):
2239 pass
2240"#;
2241 let test_path = PathBuf::from("/tmp/project/api/tests/test_example.py");
2242 db.analyze_file(test_path.clone(), test_content);
2243
2244 let all_defs = db.definitions.get("db").unwrap();
2246 assert_eq!(all_defs.len(), 3, "Should have 3 definitions");
2247
2248 let grandparent_def = all_defs
2249 .iter()
2250 .find(|d| d.file_path == grandparent_conftest)
2251 .unwrap();
2252 let parent_def = all_defs
2253 .iter()
2254 .find(|d| d.file_path == parent_conftest)
2255 .unwrap();
2256 let child_def = all_defs
2257 .iter()
2258 .find(|d| d.file_path == child_conftest)
2259 .unwrap();
2260
2261 let resolved = db.find_fixture_definition(&test_path, 1, 12);
2263 assert_eq!(
2264 resolved.as_ref(),
2265 Some(child_def),
2266 "Test should use child definition"
2267 );
2268
2269 let child_refs = db.find_references_for_definition(child_def);
2271 let test_refs: Vec<_> = child_refs
2272 .iter()
2273 .filter(|r| r.file_path == test_path)
2274 .collect();
2275 assert!(
2276 !test_refs.is_empty(),
2277 "Child should have test file references"
2278 );
2279
2280 let parent_refs = db.find_references_for_definition(parent_def);
2282 let child_param_refs: Vec<_> = parent_refs
2283 .iter()
2284 .filter(|r| r.file_path == child_conftest)
2285 .collect();
2286 let test_refs_in_parent: Vec<_> = parent_refs
2287 .iter()
2288 .filter(|r| r.file_path == test_path)
2289 .collect();
2290
2291 assert!(
2292 !child_param_refs.is_empty(),
2293 "Parent should have child parameter reference"
2294 );
2295 assert!(
2296 test_refs_in_parent.is_empty(),
2297 "Parent should NOT have test file references"
2298 );
2299
2300 let grandparent_refs = db.find_references_for_definition(grandparent_def);
2302 let parent_param_refs: Vec<_> = grandparent_refs
2303 .iter()
2304 .filter(|r| r.file_path == parent_conftest)
2305 .collect();
2306 let child_refs_in_gp: Vec<_> = grandparent_refs
2307 .iter()
2308 .filter(|r| r.file_path == child_conftest)
2309 .collect();
2310
2311 assert!(
2312 !parent_param_refs.is_empty(),
2313 "Grandparent should have parent parameter reference"
2314 );
2315 assert!(
2316 child_refs_in_gp.is_empty(),
2317 "Grandparent should NOT have child references"
2318 );
2319 }
2320
2321 #[test]
2322 fn test_fixture_hierarchy_same_file_override() {
2323 let db = FixtureDatabase::new();
2326
2327 let content = r#"
2328import pytest
2329
2330@pytest.fixture
2331def base():
2332 return "base"
2333
2334@pytest.fixture
2335def base(base):
2336 return f"override_{base}"
2337
2338def test_uses_override(base):
2339 pass
2340"#;
2341 let test_path = PathBuf::from("/tmp/test/test_example.py");
2342 db.analyze_file(test_path.clone(), content);
2343
2344 let defs = db.definitions.get("base").unwrap();
2345 assert_eq!(defs.len(), 2, "Should have 2 definitions in same file");
2346
2347 println!("\nDefinitions found:");
2348 for d in defs.iter() {
2349 println!(" base at line {}", d.line);
2350 }
2351
2352 if let Some(usages) = db.usages.get(&test_path) {
2354 println!("\nUsages found:");
2355 for u in usages.iter() {
2356 println!(" {} at line {}", u.name, u.line);
2357 }
2358 } else {
2359 println!("\nNo usages found!");
2360 }
2361
2362 let resolved = db.find_fixture_definition(&test_path, 11, 23);
2366
2367 println!("\nResolved: {:?}", resolved.as_ref().map(|d| d.line));
2368
2369 assert!(resolved.is_some(), "Should resolve to override definition");
2370
2371 let override_def = defs.iter().find(|d| d.line == 9).unwrap();
2373 println!("Override def at line: {}", override_def.line);
2374 assert_eq!(resolved.as_ref(), Some(override_def));
2375 }
2376
2377 #[test]
2378 fn test_cursor_position_on_definition_line() {
2379 let db = FixtureDatabase::new();
2382
2383 let parent_content = r#"
2385import pytest
2386
2387@pytest.fixture
2388def cli_runner():
2389 return "parent"
2390"#;
2391 let parent_conftest = PathBuf::from("/tmp/conftest.py");
2392 db.analyze_file(parent_conftest.clone(), parent_content);
2393
2394 let content = r#"
2395import pytest
2396
2397@pytest.fixture
2398def cli_runner(cli_runner):
2399 return cli_runner
2400"#;
2401 let test_path = PathBuf::from("/tmp/test/test_example.py");
2402 db.analyze_file(test_path.clone(), content);
2403
2404 println!("\n=== Testing character positions on line 5 ===");
2411
2412 if let Some(usages) = db.usages.get(&test_path) {
2414 println!("\nUsages found:");
2415 for u in usages.iter() {
2416 println!(
2417 " {} at line {}, chars {}-{}",
2418 u.name, u.line, u.start_char, u.end_char
2419 );
2420 }
2421 } else {
2422 println!("\nNo usages found!");
2423 }
2424
2425 let line_content = "def cli_runner(cli_runner):";
2427 println!("\nLine content: '{}'", line_content);
2428
2429 println!("\nPosition 4 (function name):");
2431 let word_at_4 = db.extract_word_at_position(line_content, 4);
2432 println!(" Word at cursor: {:?}", word_at_4);
2433 let fixture_name_at_4 = db.find_fixture_at_position(&test_path, 4, 4);
2434 println!(" find_fixture_at_position: {:?}", fixture_name_at_4);
2435 let resolved_4 = db.find_fixture_definition(&test_path, 4, 4); println!(
2437 " Resolved: {:?}",
2438 resolved_4.as_ref().map(|d| (d.name.as_str(), d.line))
2439 );
2440
2441 println!("\nPosition 16 (parameter name):");
2443 let word_at_16 = db.extract_word_at_position(line_content, 16);
2444 println!(" Word at cursor: {:?}", word_at_16);
2445
2446 if let Some(usages) = db.usages.get(&test_path) {
2448 for usage in usages.iter() {
2449 println!(" Checking usage: {} at line {}", usage.name, usage.line);
2450 if usage.line == 5 && usage.name == "cli_runner" {
2451 println!(" MATCH! Usage matches our position");
2452 }
2453 }
2454 }
2455
2456 let fixture_name_at_16 = db.find_fixture_at_position(&test_path, 4, 16);
2457 println!(" find_fixture_at_position: {:?}", fixture_name_at_16);
2458 let resolved_16 = db.find_fixture_definition(&test_path, 4, 16); println!(
2460 " Resolved: {:?}",
2461 resolved_16.as_ref().map(|d| (d.name.as_str(), d.line))
2462 );
2463
2464 assert_eq!(word_at_4, Some("cli_runner".to_string()));
2469 assert_eq!(word_at_16, Some("cli_runner".to_string()));
2470
2471 println!("\n=== ACTUAL vs EXPECTED ===");
2473 println!("Position 4 (function name):");
2474 println!(
2475 " Actual: {:?}",
2476 resolved_4.as_ref().map(|d| (&d.file_path, d.line))
2477 );
2478 println!(" Expected: test file, line 5 (the child definition itself)");
2479
2480 println!("\nPosition 16 (parameter):");
2481 println!(
2482 " Actual: {:?}",
2483 resolved_16.as_ref().map(|d| (&d.file_path, d.line))
2484 );
2485 println!(" Expected: conftest, line 5 (the parent definition)");
2486
2487 if let Some(ref def) = resolved_16 {
2493 assert_eq!(
2494 def.file_path, parent_conftest,
2495 "Parameter should resolve to parent definition"
2496 );
2497 } else {
2498 panic!("Position 16 (parameter) should resolve to parent definition");
2499 }
2500 }
2501}