1use super::Backend;
4use crate::fixtures::types::FixtureScope;
5use crate::fixtures::CompletionContext;
6use crate::fixtures::FixtureDefinition;
7use std::path::PathBuf;
8use tower_lsp_server::jsonrpc::Result;
9use tower_lsp_server::ls_types::*;
10use tracing::info;
11
12const EXCLUDED_PARAM_NAMES: &[&str] = &["self", "cls"];
14
15pub(crate) struct CompletionOpts<'a> {
19 fixture_scope: Option<FixtureScope>,
22 current_fixture_name: Option<&'a str>,
25 insert_prefix: &'a str,
28}
29
30fn should_exclude_fixture(
33 fixture: &FixtureDefinition,
34 current_scope: Option<FixtureScope>,
35) -> bool {
36 let Some(scope) = current_scope else {
38 return false;
39 };
40 fixture.scope < scope
43}
44
45fn is_fixture_excluded(
49 fixture: &FixtureDefinition,
50 declared_params: Option<&[String]>,
51 opts: &CompletionOpts<'_>,
52) -> bool {
53 if EXCLUDED_PARAM_NAMES.contains(&fixture.name.as_str()) {
55 return true;
56 }
57
58 if let Some(name) = opts.current_fixture_name {
60 if fixture.name == name {
61 return true;
62 }
63 }
64
65 if let Some(params) = declared_params {
67 if params.contains(&fixture.name) {
68 return true;
69 }
70 }
71
72 if should_exclude_fixture(fixture, opts.fixture_scope) {
74 return true;
75 }
76
77 false
78}
79
80fn fixture_sort_priority(fixture: &FixtureDefinition, current_file: &std::path::Path) -> u8 {
83 if fixture.file_path == current_file {
84 0 } else if fixture.is_third_party {
86 3 } else if fixture.is_plugin {
88 2 } else {
90 1 }
92}
93
94fn make_sort_text(priority: u8, fixture_name: &str) -> String {
97 format!("{}_{}", priority, fixture_name)
98}
99
100fn make_fixture_detail(fixture: &FixtureDefinition) -> String {
105 let mut parts = Vec::new();
106
107 if fixture.scope != FixtureScope::Function {
109 parts.push(format!("({})", fixture.scope.as_str()));
110 }
111
112 if fixture.is_third_party {
114 parts.push("[third-party]".to_string());
115 } else if fixture.is_plugin {
116 parts.push("[plugin]".to_string());
117 }
118
119 parts.join(" ")
120}
121
122struct EnrichedFixture {
124 fixture: FixtureDefinition,
125 detail: String,
126 sort_text: String,
127}
128
129fn filter_and_enrich_fixtures(
132 available: Vec<FixtureDefinition>,
133 file_path: &std::path::Path,
134 declared_params: Option<&[String]>,
135 opts: &CompletionOpts<'_>,
136) -> Vec<EnrichedFixture> {
137 available
138 .into_iter()
139 .filter(|f| !is_fixture_excluded(f, declared_params, opts))
140 .map(|f| {
141 let detail = make_fixture_detail(&f);
142 let priority = fixture_sort_priority(&f, file_path);
143 let sort_text = make_sort_text(priority, &f.name);
144 EnrichedFixture {
145 fixture: f,
146 detail,
147 sort_text,
148 }
149 })
150 .collect()
151}
152
153impl Backend {
154 pub async fn handle_completion(
156 &self,
157 params: CompletionParams,
158 ) -> Result<Option<CompletionResponse>> {
159 let uri = params.text_document_position.text_document.uri;
160 let position = params.text_document_position.position;
161
162 let triggered_by_comma = params
165 .context
166 .as_ref()
167 .and_then(|ctx| ctx.trigger_character.as_deref())
168 == Some(",");
169 let insert_prefix = if triggered_by_comma { " " } else { "" };
170
171 info!(
172 "completion request: uri={:?}, line={}, char={}",
173 uri, position.line, position.character
174 );
175
176 if let Some(file_path) = self.uri_to_path(&uri) {
177 if let Some(ctx) = self.fixture_db.get_completion_context(
179 &file_path,
180 position.line,
181 position.character,
182 ) {
183 info!("Completion context: {:?}", ctx);
184
185 let workspace_root = self.workspace_root.read().await.clone();
187
188 match ctx {
189 CompletionContext::FunctionSignature {
190 function_name,
191 is_fixture,
192 declared_params,
193 fixture_scope,
194 ..
195 } => {
196 let opts = CompletionOpts {
199 fixture_scope,
200 current_fixture_name: if is_fixture {
201 Some(function_name.as_str())
202 } else {
203 None
204 },
205 insert_prefix,
206 };
207 return Ok(Some(self.create_fixture_completions(
208 &file_path,
209 &declared_params,
210 workspace_root.as_ref(),
211 &opts,
212 )));
213 }
214 CompletionContext::FunctionBody {
215 function_name,
216 function_line,
217 is_fixture,
218 declared_params,
219 fixture_scope,
220 ..
221 } => {
222 let opts = CompletionOpts {
224 fixture_scope,
225 current_fixture_name: if is_fixture {
226 Some(function_name.as_str())
227 } else {
228 None
229 },
230 insert_prefix,
231 };
232 return Ok(Some(self.create_fixture_completions_with_auto_add(
233 &file_path,
234 &declared_params,
235 function_line,
236 workspace_root.as_ref(),
237 &opts,
238 )));
239 }
240 CompletionContext::UsefixturesDecorator
241 | CompletionContext::ParametrizeIndirect => {
242 return Ok(Some(self.create_string_fixture_completions(
244 &file_path,
245 workspace_root.as_ref(),
246 insert_prefix,
247 )));
248 }
249 }
250 } else {
251 info!("No completion context found");
252 }
253 }
254
255 Ok(None)
256 }
257
258 pub(crate) fn create_fixture_completions(
261 &self,
262 file_path: &std::path::Path,
263 declared_params: &[String],
264 workspace_root: Option<&PathBuf>,
265 opts: &CompletionOpts<'_>,
266 ) -> CompletionResponse {
267 let available = self.fixture_db.get_available_fixtures(file_path);
268 let enriched =
269 filter_and_enrich_fixtures(available, file_path, Some(declared_params), opts);
270
271 let items = enriched
272 .into_iter()
273 .map(|ef| {
274 let documentation = Some(Documentation::MarkupContent(MarkupContent {
275 kind: MarkupKind::Markdown,
276 value: Self::format_fixture_documentation(&ef.fixture, workspace_root),
277 }));
278
279 CompletionItem {
280 label: ef.fixture.name.clone(),
281 kind: Some(CompletionItemKind::VARIABLE),
282 detail: Some(ef.detail),
283 documentation,
284 insert_text: Some(format!("{}{}", opts.insert_prefix, ef.fixture.name)),
285 insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
286 sort_text: Some(ef.sort_text),
287 ..Default::default()
288 }
289 })
290 .collect();
291
292 CompletionResponse::Array(items)
293 }
294
295 pub(crate) fn create_fixture_completions_with_auto_add(
298 &self,
299 file_path: &std::path::Path,
300 declared_params: &[String],
301 function_line: usize,
302 workspace_root: Option<&PathBuf>,
303 opts: &CompletionOpts<'_>,
304 ) -> CompletionResponse {
305 let available = self.fixture_db.get_available_fixtures(file_path);
306 let enriched =
307 filter_and_enrich_fixtures(available, file_path, Some(declared_params), opts);
308
309 let insertion_info = self
311 .fixture_db
312 .get_function_param_insertion_info(file_path, function_line);
313
314 let items = enriched
315 .into_iter()
316 .map(|ef| {
317 let documentation = Some(Documentation::MarkupContent(MarkupContent {
318 kind: MarkupKind::Markdown,
319 value: Self::format_fixture_documentation(&ef.fixture, workspace_root),
320 }));
321
322 let additional_text_edits = insertion_info.as_ref().map(|info| {
324 let text = match &info.multiline_indent {
325 Some(indent) => {
326 if info.needs_comma {
327 format!(",\n{}{}", indent, ef.fixture.name)
330 } else {
331 format!("\n{}{},", indent, ef.fixture.name)
334 }
335 }
336 None => {
337 if info.needs_comma {
338 format!(", {}", ef.fixture.name)
339 } else {
340 ef.fixture.name.clone()
341 }
342 }
343 };
344 let lsp_line = Self::internal_line_to_lsp(info.line);
345 vec![TextEdit {
346 range: Self::create_point_range(lsp_line, info.char_pos as u32),
347 new_text: text,
348 }]
349 });
350
351 CompletionItem {
352 label: ef.fixture.name.clone(),
353 kind: Some(CompletionItemKind::VARIABLE),
354 detail: Some(ef.detail),
355 documentation,
356 insert_text: Some(format!("{}{}", opts.insert_prefix, ef.fixture.name)),
357 insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
358 additional_text_edits,
359 sort_text: Some(ef.sort_text),
360 ..Default::default()
361 }
362 })
363 .collect();
364
365 CompletionResponse::Array(items)
366 }
367
368 pub(crate) fn create_string_fixture_completions(
372 &self,
373 file_path: &std::path::Path,
374 workspace_root: Option<&PathBuf>,
375 insert_prefix: &str,
376 ) -> CompletionResponse {
377 let available = self.fixture_db.get_available_fixtures(file_path);
378 let no_filter_opts = CompletionOpts {
379 fixture_scope: None,
380 current_fixture_name: None,
381 insert_prefix,
382 };
383 let enriched = filter_and_enrich_fixtures(available, file_path, None, &no_filter_opts);
384
385 let items = enriched
386 .into_iter()
387 .map(|ef| {
388 let documentation = Some(Documentation::MarkupContent(MarkupContent {
389 kind: MarkupKind::Markdown,
390 value: Self::format_fixture_documentation(&ef.fixture, workspace_root),
391 }));
392
393 CompletionItem {
394 label: ef.fixture.name.clone(),
395 kind: Some(CompletionItemKind::TEXT),
396 detail: Some(ef.detail),
397 documentation,
398 insert_text: Some(format!("{}{}", insert_prefix, ef.fixture.name)),
399 insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
400 sort_text: Some(ef.sort_text),
401 ..Default::default()
402 }
403 })
404 .collect();
405
406 CompletionResponse::Array(items)
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use crate::fixtures::types::FixtureScope;
414 use crate::fixtures::FixtureDatabase;
415 use std::path::PathBuf;
416 use std::sync::Arc;
417
418 fn make_fixture(name: &str, scope: FixtureScope) -> FixtureDefinition {
419 FixtureDefinition {
420 name: name.to_string(),
421 file_path: PathBuf::from("/tmp/test/conftest.py"),
422 line: 1,
423 end_line: 5,
424 start_char: 4,
425 end_char: 10,
426 docstring: None,
427 return_type: None,
428 return_type_imports: vec![],
429 is_third_party: false,
430 is_plugin: false,
431 dependencies: vec![],
432 scope,
433 yield_line: None,
434 autouse: false,
435 }
436 }
437
438 #[test]
443 fn test_should_exclude_fixture_test_function_allows_all() {
444 let func = make_fixture("f", FixtureScope::Function);
446 let class = make_fixture("c", FixtureScope::Class);
447 let module = make_fixture("m", FixtureScope::Module);
448 let package = make_fixture("p", FixtureScope::Package);
449 let session = make_fixture("s", FixtureScope::Session);
450
451 assert!(!should_exclude_fixture(&func, None));
452 assert!(!should_exclude_fixture(&class, None));
453 assert!(!should_exclude_fixture(&module, None));
454 assert!(!should_exclude_fixture(&package, None));
455 assert!(!should_exclude_fixture(&session, None));
456 }
457
458 #[test]
459 fn test_should_exclude_fixture_session_excludes_narrower() {
460 let func = make_fixture("f", FixtureScope::Function);
461 let class = make_fixture("c", FixtureScope::Class);
462 let module = make_fixture("m", FixtureScope::Module);
463 let package = make_fixture("p", FixtureScope::Package);
464 let session = make_fixture("s", FixtureScope::Session);
465
466 let session_scope = Some(FixtureScope::Session);
467 assert!(should_exclude_fixture(&func, session_scope));
469 assert!(should_exclude_fixture(&class, session_scope));
470 assert!(should_exclude_fixture(&module, session_scope));
471 assert!(should_exclude_fixture(&package, session_scope));
472 assert!(!should_exclude_fixture(&session, session_scope));
474 }
475
476 #[test]
477 fn test_should_exclude_fixture_module_excludes_narrower() {
478 let func = make_fixture("f", FixtureScope::Function);
479 let class = make_fixture("c", FixtureScope::Class);
480 let module = make_fixture("m", FixtureScope::Module);
481 let package = make_fixture("p", FixtureScope::Package);
482 let session = make_fixture("s", FixtureScope::Session);
483
484 let module_scope = Some(FixtureScope::Module);
485 assert!(should_exclude_fixture(&func, module_scope));
486 assert!(should_exclude_fixture(&class, module_scope));
487 assert!(!should_exclude_fixture(&module, module_scope));
488 assert!(!should_exclude_fixture(&package, module_scope));
489 assert!(!should_exclude_fixture(&session, module_scope));
490 }
491
492 #[test]
493 fn test_should_exclude_fixture_function_allows_all() {
494 let func = make_fixture("f", FixtureScope::Function);
495 let class = make_fixture("c", FixtureScope::Class);
496 let module = make_fixture("m", FixtureScope::Module);
497 let session = make_fixture("s", FixtureScope::Session);
498
499 let function_scope = Some(FixtureScope::Function);
500 assert!(!should_exclude_fixture(&func, function_scope));
501 assert!(!should_exclude_fixture(&class, function_scope));
502 assert!(!should_exclude_fixture(&module, function_scope));
503 assert!(!should_exclude_fixture(&session, function_scope));
504 }
505
506 #[test]
507 fn test_should_exclude_fixture_class_excludes_function() {
508 let func = make_fixture("f", FixtureScope::Function);
509 let class = make_fixture("c", FixtureScope::Class);
510 let module = make_fixture("m", FixtureScope::Module);
511 let session = make_fixture("s", FixtureScope::Session);
512
513 let class_scope = Some(FixtureScope::Class);
514 assert!(should_exclude_fixture(&func, class_scope));
515 assert!(!should_exclude_fixture(&class, class_scope));
516 assert!(!should_exclude_fixture(&module, class_scope));
517 assert!(!should_exclude_fixture(&session, class_scope));
518 }
519
520 #[test]
525 fn test_is_fixture_excluded_filters_self_cls() {
526 let self_fixture = make_fixture("self", FixtureScope::Function);
527 let cls_fixture = make_fixture("cls", FixtureScope::Function);
528 let normal_fixture = make_fixture("db", FixtureScope::Function);
529
530 let opts = CompletionOpts {
531 fixture_scope: None,
532 current_fixture_name: None,
533 insert_prefix: "",
534 };
535 assert!(is_fixture_excluded(&self_fixture, None, &opts));
536 assert!(is_fixture_excluded(&cls_fixture, None, &opts));
537 assert!(!is_fixture_excluded(&normal_fixture, None, &opts));
538 }
539
540 #[test]
541 fn test_is_fixture_excluded_filters_declared_params() {
542 let fixture = make_fixture("db", FixtureScope::Function);
543 let declared = vec!["db".to_string()];
544
545 let opts = CompletionOpts {
546 fixture_scope: None,
547 current_fixture_name: None,
548 insert_prefix: "",
549 };
550 assert!(is_fixture_excluded(&fixture, Some(&declared), &opts));
551 assert!(!is_fixture_excluded(&fixture, None, &opts));
552 assert!(!is_fixture_excluded(
553 &fixture,
554 Some(&["other".to_string()]),
555 &opts,
556 ));
557 }
558
559 #[test]
560 fn test_is_fixture_excluded_combines_scope_and_params() {
561 let func_fixture = make_fixture("db", FixtureScope::Function);
562 let declared = vec!["db".to_string()];
563 let session_scope = Some(FixtureScope::Session);
564
565 let opts = CompletionOpts {
567 fixture_scope: session_scope,
568 current_fixture_name: None,
569 insert_prefix: "",
570 };
571 assert!(is_fixture_excluded(&func_fixture, Some(&declared), &opts,));
572
573 let undeclared: Vec<String> = vec![];
575 assert!(is_fixture_excluded(&func_fixture, Some(&undeclared), &opts,));
576
577 let session_opts = CompletionOpts {
579 fixture_scope: session_scope,
580 current_fixture_name: None,
581 insert_prefix: "",
582 };
583 assert!(is_fixture_excluded(
584 &make_fixture("db", FixtureScope::Session),
585 Some(&declared),
586 &session_opts,
587 ));
588
589 assert!(!is_fixture_excluded(
591 &make_fixture("other", FixtureScope::Session),
592 Some(&undeclared),
593 &session_opts,
594 ));
595 }
596
597 #[test]
602 fn test_filter_and_enrich_excludes_current_fixture() {
603 let file = std::path::Path::new("/tmp/test/conftest.py");
604 let fixtures = vec![
605 make_fixture("my_fixture", FixtureScope::Function),
606 make_fixture("other_fixture", FixtureScope::Function),
607 ];
608
609 let opts = CompletionOpts {
611 fixture_scope: Some(FixtureScope::Function),
612 current_fixture_name: Some("my_fixture"),
613 insert_prefix: "",
614 };
615 let enriched = filter_and_enrich_fixtures(fixtures.clone(), file, None, &opts);
616 assert_eq!(enriched.len(), 1);
617 assert_eq!(enriched[0].fixture.name, "other_fixture");
618
619 let test_opts = CompletionOpts {
621 fixture_scope: None,
622 current_fixture_name: None,
623 insert_prefix: "",
624 };
625 let enriched = filter_and_enrich_fixtures(fixtures, file, None, &test_opts);
626 assert_eq!(enriched.len(), 2);
627 }
628
629 #[test]
630 fn test_filter_and_enrich_excludes_scope_incompatible() {
631 let file_path = PathBuf::from("/tmp/test/test_file.py");
632 let fixtures = vec![
633 make_fixture("func_fix", FixtureScope::Function),
634 make_fixture("class_fix", FixtureScope::Class),
635 make_fixture("module_fix", FixtureScope::Module),
636 make_fixture("session_fix", FixtureScope::Session),
637 ];
638
639 let opts = CompletionOpts {
641 fixture_scope: Some(FixtureScope::Session),
642 current_fixture_name: None,
643 insert_prefix: "",
644 };
645 let enriched = filter_and_enrich_fixtures(fixtures.clone(), &file_path, Some(&[]), &opts);
646 let names: Vec<&str> = enriched.iter().map(|e| e.fixture.name.as_str()).collect();
647 assert_eq!(names, vec!["session_fix"]);
648
649 let opts = CompletionOpts {
651 fixture_scope: Some(FixtureScope::Module),
652 current_fixture_name: None,
653 insert_prefix: "",
654 };
655 let enriched = filter_and_enrich_fixtures(fixtures.clone(), &file_path, Some(&[]), &opts);
656 let names: Vec<&str> = enriched.iter().map(|e| e.fixture.name.as_str()).collect();
657 assert_eq!(names, vec!["module_fix", "session_fix"]);
658
659 let opts = CompletionOpts {
661 fixture_scope: Some(FixtureScope::Function),
662 current_fixture_name: None,
663 insert_prefix: "",
664 };
665 let enriched = filter_and_enrich_fixtures(fixtures.clone(), &file_path, Some(&[]), &opts);
666 assert_eq!(enriched.len(), 4);
667
668 let opts = CompletionOpts {
670 fixture_scope: None,
671 current_fixture_name: None,
672 insert_prefix: "",
673 };
674 let enriched = filter_and_enrich_fixtures(fixtures.clone(), &file_path, Some(&[]), &opts);
675 assert_eq!(enriched.len(), 4);
676 }
677
678 #[test]
679 fn test_filter_and_enrich_excludes_declared_params() {
680 let file_path = PathBuf::from("/tmp/test/test_file.py");
681 let fixtures = vec![
682 make_fixture("db", FixtureScope::Function),
683 make_fixture("client", FixtureScope::Function),
684 make_fixture("app", FixtureScope::Function),
685 ];
686
687 let declared = vec!["db".to_string(), "client".to_string()];
688 let opts = CompletionOpts {
689 fixture_scope: None,
690 current_fixture_name: None,
691 insert_prefix: "",
692 };
693 let enriched = filter_and_enrich_fixtures(fixtures, &file_path, Some(&declared), &opts);
694 let names: Vec<&str> = enriched.iter().map(|e| e.fixture.name.as_str()).collect();
695 assert_eq!(names, vec!["app"]);
696 }
697
698 #[test]
699 fn test_filter_and_enrich_excludes_self_cls() {
700 let file_path = PathBuf::from("/tmp/test/test_file.py");
701 let mut fixtures = vec![
702 make_fixture("self", FixtureScope::Function),
703 make_fixture("cls", FixtureScope::Function),
704 make_fixture("real_fixture", FixtureScope::Function),
705 ];
706 fixtures[0].name = "self".to_string();
708 fixtures[1].name = "cls".to_string();
709
710 let opts = CompletionOpts {
711 fixture_scope: None,
712 current_fixture_name: None,
713 insert_prefix: "",
714 };
715 let enriched = filter_and_enrich_fixtures(fixtures, &file_path, None, &opts);
716 let names: Vec<&str> = enriched.iter().map(|e| e.fixture.name.as_str()).collect();
717 assert_eq!(names, vec!["real_fixture"]);
718 }
719
720 #[test]
725 fn test_fixture_sort_priority_same_file() {
726 let current = PathBuf::from("/tmp/test/test_file.py");
727 let mut fixture = make_fixture("f", FixtureScope::Function);
728 fixture.file_path = current.clone();
729
730 assert_eq!(fixture_sort_priority(&fixture, ¤t), 0);
731 }
732
733 #[test]
734 fn test_fixture_sort_priority_conftest() {
735 let current = PathBuf::from("/tmp/test/test_file.py");
736 let mut fixture = make_fixture("f", FixtureScope::Function);
737 fixture.file_path = PathBuf::from("/tmp/test/conftest.py");
738
739 assert_eq!(fixture_sort_priority(&fixture, ¤t), 1);
740 }
741
742 #[test]
743 fn test_fixture_sort_priority_plugin() {
744 let current = PathBuf::from("/tmp/test/test_file.py");
745 let mut fixture = make_fixture("f", FixtureScope::Function);
746 fixture.file_path = PathBuf::from("/tmp/other/plugin.py");
747 fixture.is_plugin = true;
748
749 assert_eq!(fixture_sort_priority(&fixture, ¤t), 2);
750 }
751
752 #[test]
753 fn test_fixture_sort_priority_third_party() {
754 let current = PathBuf::from("/tmp/test/test_file.py");
755 let mut fixture = make_fixture("f", FixtureScope::Function);
756 fixture.file_path = PathBuf::from("/tmp/venv/lib/site-packages/pkg/fix.py");
757 fixture.is_third_party = true;
758
759 assert_eq!(fixture_sort_priority(&fixture, ¤t), 3);
760 }
761
762 #[test]
763 fn test_fixture_sort_priority_third_party_trumps_plugin() {
764 let current = PathBuf::from("/tmp/test/test_file.py");
765 let mut fixture = make_fixture("f", FixtureScope::Function);
766 fixture.file_path = PathBuf::from("/tmp/venv/lib/site-packages/pkg/fix.py");
767 fixture.is_third_party = true;
768 fixture.is_plugin = true;
769
770 assert_eq!(fixture_sort_priority(&fixture, ¤t), 3);
772 }
773
774 #[test]
779 fn test_make_fixture_detail_default_scope() {
780 let fixture = make_fixture("f", FixtureScope::Function);
781 let detail = make_fixture_detail(&fixture);
782 assert_eq!(detail, ""); }
784
785 #[test]
786 fn test_make_fixture_detail_session_scope() {
787 let fixture = make_fixture("f", FixtureScope::Session);
788 let detail = make_fixture_detail(&fixture);
789 assert_eq!(detail, "(session)");
790 }
791
792 #[test]
793 fn test_make_fixture_detail_third_party() {
794 let mut fixture = make_fixture("f", FixtureScope::Function);
795 fixture.is_third_party = true;
796 let detail = make_fixture_detail(&fixture);
797 assert_eq!(detail, "[third-party]");
798 }
799
800 #[test]
801 fn test_make_fixture_detail_plugin_with_scope() {
802 let mut fixture = make_fixture("f", FixtureScope::Module);
803 fixture.is_plugin = true;
804 let detail = make_fixture_detail(&fixture);
805 assert_eq!(detail, "(module) [plugin]");
806 }
807
808 #[test]
809 fn test_make_fixture_detail_third_party_overrides_plugin() {
810 let mut fixture = make_fixture("f", FixtureScope::Session);
811 fixture.is_third_party = true;
812 fixture.is_plugin = true;
813 let detail = make_fixture_detail(&fixture);
814 assert_eq!(detail, "(session) [third-party]");
816 }
817
818 #[test]
823 fn test_make_sort_text_ordering() {
824 let same_file = make_sort_text(0, "zzz");
825 let conftest = make_sort_text(1, "aaa");
826 let plugin = make_sort_text(2, "aaa");
827 let third_party = make_sort_text(3, "aaa");
828
829 assert!(same_file < conftest);
831 assert!(conftest < plugin);
832 assert!(plugin < third_party);
833 }
834
835 #[test]
836 fn test_make_sort_text_alpha_within_group() {
837 let a = make_sort_text(0, "alpha");
838 let b = make_sort_text(0, "beta");
839 assert!(a < b);
840 }
841
842 use tower_lsp_server::LspService;
847
848 fn make_backend_with_db(db: Arc<FixtureDatabase>) -> Backend {
851 let backend_slot: Arc<std::sync::Mutex<Option<Backend>>> =
852 Arc::new(std::sync::Mutex::new(None));
853 let slot_clone = backend_slot.clone();
854 let (_svc, _sock) = LspService::new(move |client| {
855 let b = Backend::new(client, db.clone());
856 *slot_clone.lock().unwrap() = Some(Backend {
858 client: b.client.clone(),
859 fixture_db: b.fixture_db.clone(),
860 workspace_root: b.workspace_root.clone(),
861 original_workspace_root: b.original_workspace_root.clone(),
862 scan_task: b.scan_task.clone(),
863 uri_cache: b.uri_cache.clone(),
864 config: b.config.clone(),
865 });
866 b
867 });
868 let result = backend_slot
869 .lock()
870 .unwrap()
871 .take()
872 .expect("Backend should have been created");
873 result
874 }
875
876 fn setup_backend_with_fixtures() -> (Backend, PathBuf) {
878 let db = Arc::new(FixtureDatabase::new());
879
880 let conftest_content = r#"
881import pytest
882
883@pytest.fixture
884def func_fixture():
885 return "func"
886
887@pytest.fixture(scope="session")
888def session_fixture():
889 """A session-scoped fixture."""
890 return "session"
891
892@pytest.fixture(scope="module")
893def module_fixture():
894 return "module"
895"#;
896
897 let test_content = r#"
898import pytest
899
900@pytest.fixture(scope="session")
901def local_session_fixture():
902 pass
903
904def test_something(func_fixture):
905 pass
906"#;
907
908 let conftest_path = PathBuf::from("/tmp/test_backend/conftest.py");
909 let test_path = PathBuf::from("/tmp/test_backend/test_example.py");
910
911 db.analyze_file(conftest_path, conftest_content);
912 db.analyze_file(test_path.clone(), test_content);
913
914 let backend = make_backend_with_db(db);
915 (backend, test_path)
916 }
917
918 fn extract_items(response: &CompletionResponse) -> &Vec<CompletionItem> {
919 match response {
920 CompletionResponse::Array(items) => items,
921 _ => panic!("Expected CompletionResponse::Array"),
922 }
923 }
924
925 #[test]
930 fn test_create_fixture_completions_returns_items() {
931 let (backend, test_path) = setup_backend_with_fixtures();
932 let declared = vec![];
933 let opts = CompletionOpts {
934 fixture_scope: None,
935 current_fixture_name: None,
936 insert_prefix: "",
937 };
938 let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
939 let items = extract_items(&response);
940 assert!(!items.is_empty(), "Should return completion items");
941 for item in items {
943 assert_eq!(item.kind, Some(CompletionItemKind::VARIABLE));
944 assert!(item.insert_text.is_some());
945 assert!(item.sort_text.is_some());
946 assert!(item.detail.is_some());
947 }
948 }
949
950 #[test]
951 fn test_create_fixture_completions_filters_declared() {
952 let (backend, test_path) = setup_backend_with_fixtures();
953 let declared = vec!["func_fixture".to_string()];
954 let opts = CompletionOpts {
955 fixture_scope: None,
956 current_fixture_name: None,
957 insert_prefix: "",
958 };
959 let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
960 let items = extract_items(&response);
961 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
962 assert!(
963 !labels.contains(&"func_fixture"),
964 "func_fixture should be filtered out"
965 );
966 }
967
968 #[test]
969 fn test_create_fixture_completions_scope_filtering() {
970 let (backend, test_path) = setup_backend_with_fixtures();
971 let declared = vec![];
972 let opts = CompletionOpts {
974 fixture_scope: Some(FixtureScope::Session),
975 current_fixture_name: None,
976 insert_prefix: "",
977 };
978 let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
979 let items = extract_items(&response);
980 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
981 assert!(
982 !labels.contains(&"func_fixture"),
983 "func_fixture should be excluded by session scope filter"
984 );
985 assert!(
986 labels.contains(&"session_fixture"),
987 "session_fixture should be present, got: {:?}",
988 labels
989 );
990 }
991
992 #[test]
993 fn test_create_fixture_completions_detail_and_sort() {
994 let (backend, test_path) = setup_backend_with_fixtures();
995 let declared = vec![];
996 let opts = CompletionOpts {
997 fixture_scope: None,
998 current_fixture_name: None,
999 insert_prefix: "",
1000 };
1001 let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
1002 let items = extract_items(&response);
1003
1004 let session_item = items.iter().find(|i| i.label == "session_fixture");
1006 assert!(session_item.is_some(), "Should find session_fixture");
1007 let session_item = session_item.unwrap();
1008 assert!(
1009 session_item.detail.as_ref().unwrap().contains("session"),
1010 "session_fixture detail should contain scope, got: {:?}",
1011 session_item.detail
1012 );
1013
1014 let func_item = items.iter().find(|i| i.label == "func_fixture");
1016 assert!(func_item.is_some(), "Should find func_fixture");
1017 let func_item = func_item.unwrap();
1018 assert!(
1019 !func_item.detail.as_ref().unwrap().contains("function"),
1020 "func_fixture detail should not contain 'function' (default scope), got: {:?}",
1021 func_item.detail
1022 );
1023 }
1024
1025 #[test]
1026 fn test_create_fixture_completions_documentation() {
1027 let (backend, test_path) = setup_backend_with_fixtures();
1028 let declared = vec![];
1029 let opts = CompletionOpts {
1030 fixture_scope: None,
1031 current_fixture_name: None,
1032 insert_prefix: "",
1033 };
1034 let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
1035 let items = extract_items(&response);
1036
1037 for item in items {
1039 assert!(
1040 item.documentation.is_some(),
1041 "Completion item '{}' should have documentation",
1042 item.label
1043 );
1044 }
1045 }
1046
1047 #[test]
1048 fn test_create_fixture_completions_with_workspace_root() {
1049 let (backend, test_path) = setup_backend_with_fixtures();
1050 let declared = vec![];
1051 let workspace_root = PathBuf::from("/tmp/test_backend");
1052 let opts = CompletionOpts {
1053 fixture_scope: None,
1054 current_fixture_name: None,
1055 insert_prefix: "",
1056 };
1057 let response =
1058 backend.create_fixture_completions(&test_path, &declared, Some(&workspace_root), &opts);
1059 let items = extract_items(&response);
1060 assert!(!items.is_empty());
1061 }
1062
1063 #[test]
1068 fn test_create_fixture_completions_with_auto_add_returns_items() {
1069 let (backend, test_path) = setup_backend_with_fixtures();
1070 let declared = vec![];
1071 let opts = CompletionOpts {
1072 fixture_scope: None,
1073 current_fixture_name: None,
1074 insert_prefix: "",
1075 };
1076 let response =
1079 backend.create_fixture_completions_with_auto_add(&test_path, &declared, 8, None, &opts);
1080 let items = extract_items(&response);
1081 assert!(!items.is_empty(), "Should return completion items");
1082 for item in items {
1083 assert_eq!(item.kind, Some(CompletionItemKind::VARIABLE));
1084 assert!(item.sort_text.is_some());
1085 assert!(item.detail.is_some());
1086 }
1087 }
1088
1089 #[test]
1090 fn test_create_fixture_completions_with_auto_add_has_text_edits() {
1091 let (backend, test_path) = setup_backend_with_fixtures();
1092 let declared = vec!["func_fixture".to_string()];
1093 let opts = CompletionOpts {
1095 fixture_scope: None,
1096 current_fixture_name: None,
1097 insert_prefix: "",
1098 };
1099 let response =
1100 backend.create_fixture_completions_with_auto_add(&test_path, &declared, 8, None, &opts);
1101 let items = extract_items(&response);
1102 for item in items {
1104 assert!(
1105 item.additional_text_edits.is_some(),
1106 "Item '{}' should have additional_text_edits for auto-add",
1107 item.label
1108 );
1109 let edits = item.additional_text_edits.as_ref().unwrap();
1110 assert_eq!(edits.len(), 1, "Should have exactly one text edit");
1111 }
1112 }
1113
1114 #[test]
1115 fn test_create_fixture_completions_with_auto_add_scope_filter() {
1116 let (backend, test_path) = setup_backend_with_fixtures();
1117 let declared = vec![];
1118 let opts = CompletionOpts {
1119 fixture_scope: Some(FixtureScope::Session),
1120 current_fixture_name: None,
1121 insert_prefix: "",
1122 };
1123 let response =
1124 backend.create_fixture_completions_with_auto_add(&test_path, &declared, 8, None, &opts);
1125 let items = extract_items(&response);
1126 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1127 assert!(
1128 !labels.contains(&"func_fixture"),
1129 "func_fixture should be excluded by session scope"
1130 );
1131 }
1132
1133 #[test]
1134 fn test_create_fixture_completions_with_auto_add_filters_declared() {
1135 let (backend, test_path) = setup_backend_with_fixtures();
1136 let declared = vec!["session_fixture".to_string(), "func_fixture".to_string()];
1137 let opts = CompletionOpts {
1138 fixture_scope: None,
1139 current_fixture_name: None,
1140 insert_prefix: "",
1141 };
1142 let response =
1143 backend.create_fixture_completions_with_auto_add(&test_path, &declared, 8, None, &opts);
1144 let items = extract_items(&response);
1145 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1146 assert!(
1147 !labels.contains(&"func_fixture"),
1148 "func_fixture should be filtered"
1149 );
1150 assert!(
1151 !labels.contains(&"session_fixture"),
1152 "session_fixture should be filtered"
1153 );
1154 }
1155
1156 #[test]
1157 fn test_create_fixture_completions_with_auto_add_filters_current_fixture() {
1158 let (backend, file_path) = setup_backend_with_fixtures();
1159 let opts = CompletionOpts {
1161 fixture_scope: Some(FixtureScope::Function),
1162 current_fixture_name: Some("func_fixture"),
1163 insert_prefix: "",
1164 };
1165 let response = backend.create_fixture_completions(&file_path, &[], None, &opts);
1166 let items = extract_items(&response);
1167 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1168 assert!(
1169 !labels.contains(&"func_fixture"),
1170 "Current fixture should be excluded from completions, got: {:?}",
1171 labels
1172 );
1173 assert!(
1174 labels.contains(&"session_fixture"),
1175 "Other fixtures should still appear"
1176 );
1177 }
1178
1179 #[test]
1180 fn test_create_fixture_completions_comma_trigger_adds_space() {
1181 let (backend, test_path) = setup_backend_with_fixtures();
1182 let declared = vec![];
1183 let opts = CompletionOpts {
1185 fixture_scope: None,
1186 current_fixture_name: None,
1187 insert_prefix: " ",
1188 };
1189 let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
1190 let items = extract_items(&response);
1191 assert!(!items.is_empty());
1192 for item in items {
1193 let text = item.insert_text.as_ref().unwrap();
1194 assert!(
1195 text.starts_with(' '),
1196 "insert_text should start with space for comma trigger, got: {:?}",
1197 text
1198 );
1199 }
1200 }
1201
1202 #[test]
1203 fn test_create_fixture_completions_no_trigger_no_space() {
1204 let (backend, test_path) = setup_backend_with_fixtures();
1205 let declared = vec![];
1206 let opts = CompletionOpts {
1208 fixture_scope: None,
1209 current_fixture_name: None,
1210 insert_prefix: "",
1211 };
1212 let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
1213 let items = extract_items(&response);
1214 assert!(!items.is_empty());
1215 for item in items {
1216 let text = item.insert_text.as_ref().unwrap();
1217 assert!(
1218 !text.starts_with(' '),
1219 "insert_text should NOT start with space without comma trigger, got: {:?}",
1220 text
1221 );
1222 }
1223 }
1224
1225 #[test]
1226 fn test_create_fixture_completions_with_auto_add_no_existing_params() {
1227 let db = Arc::new(FixtureDatabase::new());
1229
1230 let conftest_content = r#"
1231import pytest
1232
1233@pytest.fixture
1234def db_fixture():
1235 return "db"
1236"#;
1237
1238 let test_content = r#"
1239def test_empty_params():
1240 pass
1241"#;
1242
1243 let conftest_path = PathBuf::from("/tmp/test_no_params/conftest.py");
1244 let test_path = PathBuf::from("/tmp/test_no_params/test_file.py");
1245
1246 db.analyze_file(conftest_path, conftest_content);
1247 db.analyze_file(test_path.clone(), test_content);
1248
1249 let backend = make_backend_with_db(db);
1250 let declared: Vec<String> = vec![];
1251 let opts = CompletionOpts {
1253 fixture_scope: None,
1254 current_fixture_name: None,
1255 insert_prefix: "",
1256 };
1257 let response =
1258 backend.create_fixture_completions_with_auto_add(&test_path, &declared, 2, None, &opts);
1259 let items = extract_items(&response);
1260 assert!(!items.is_empty(), "Should return completion items");
1261
1262 let item = items.iter().find(|i| i.label == "db_fixture");
1264 assert!(item.is_some(), "Should find db_fixture");
1265 let item = item.unwrap();
1266 let edits = item.additional_text_edits.as_ref().unwrap();
1267 assert_eq!(edits.len(), 1);
1268 assert_eq!(
1270 edits[0].new_text, "db_fixture",
1271 "Should insert fixture name without comma for empty params"
1272 );
1273 }
1274
1275 #[test]
1280 fn test_create_string_fixture_completions_returns_items() {
1281 let (backend, test_path) = setup_backend_with_fixtures();
1282 let response = backend.create_string_fixture_completions(&test_path, None, "");
1283 let items = extract_items(&response);
1284 assert!(!items.is_empty(), "Should return string completion items");
1285 for item in items {
1287 assert_eq!(
1288 item.kind,
1289 Some(CompletionItemKind::TEXT),
1290 "String completions should use TEXT kind"
1291 );
1292 assert!(item.sort_text.is_some());
1293 assert!(item.detail.is_some());
1294 assert!(item.documentation.is_some());
1295 }
1296 }
1297
1298 #[test]
1299 fn test_create_string_fixture_completions_no_scope_filtering() {
1300 let (backend, test_path) = setup_backend_with_fixtures();
1301 let response = backend.create_string_fixture_completions(&test_path, None, "");
1303 let items = extract_items(&response);
1304 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1305 assert!(
1307 labels.contains(&"func_fixture"),
1308 "func_fixture should be in string completions, got: {:?}",
1309 labels
1310 );
1311 assert!(
1312 labels.contains(&"session_fixture"),
1313 "session_fixture should be in string completions, got: {:?}",
1314 labels
1315 );
1316 }
1317
1318 #[test]
1319 fn test_create_string_fixture_completions_with_workspace_root() {
1320 let (backend, test_path) = setup_backend_with_fixtures();
1321 let workspace_root = PathBuf::from("/tmp/test_backend");
1322 let response =
1323 backend.create_string_fixture_completions(&test_path, Some(&workspace_root), "");
1324 let items = extract_items(&response);
1325 assert!(!items.is_empty());
1326 }
1327
1328 #[test]
1329 fn test_create_string_fixture_completions_has_detail_and_sort() {
1330 let (backend, test_path) = setup_backend_with_fixtures();
1331 let response = backend.create_string_fixture_completions(&test_path, None, "");
1332 let items = extract_items(&response);
1333
1334 let session_item = items.iter().find(|i| i.label == "session_fixture");
1335 assert!(session_item.is_some());
1336 let session_item = session_item.unwrap();
1337 assert!(
1338 session_item.detail.as_ref().unwrap().contains("session"),
1339 "session_fixture should have scope in detail"
1340 );
1341 let sort = session_item.sort_text.as_ref().unwrap();
1343 assert!(
1344 sort.starts_with('1') || sort.starts_with('0'),
1345 "Sort text should start with priority digit, got: {}",
1346 sort
1347 );
1348 }
1349
1350 #[test]
1355 fn test_create_fixture_completions_empty_db() {
1356 let db = Arc::new(FixtureDatabase::new());
1357 let backend = make_backend_with_db(db);
1358 let path = PathBuf::from("/tmp/empty/test_file.py");
1359 let opts = CompletionOpts {
1360 fixture_scope: None,
1361 current_fixture_name: None,
1362 insert_prefix: "",
1363 };
1364 let response = backend.create_fixture_completions(&path, &[], None, &opts);
1365 let items = extract_items(&response);
1366 assert!(items.is_empty(), "Empty DB should return no completions");
1367 }
1368
1369 #[test]
1370 fn test_create_fixture_completions_with_auto_add_empty_db() {
1371 let db = Arc::new(FixtureDatabase::new());
1372 let backend = make_backend_with_db(db);
1373 let path = PathBuf::from("/tmp/empty/test_file.py");
1374 let opts = CompletionOpts {
1375 fixture_scope: None,
1376 current_fixture_name: None,
1377 insert_prefix: "",
1378 };
1379 let response = backend.create_fixture_completions_with_auto_add(&path, &[], 1, None, &opts);
1380 let items = extract_items(&response);
1381 assert!(items.is_empty(), "Empty DB should return no completions");
1382 }
1383
1384 #[test]
1385 fn test_create_string_fixture_completions_empty_db() {
1386 let db = Arc::new(FixtureDatabase::new());
1387 let backend = make_backend_with_db(db);
1388 let path = PathBuf::from("/tmp/empty/test_file.py");
1389 let response = backend.create_string_fixture_completions(&path, None, "");
1390 let items = extract_items(&response);
1391 assert!(items.is_empty(), "Empty DB should return no completions");
1392 }
1393}