use super::Backend;
use crate::fixtures::types::FixtureScope;
use crate::fixtures::CompletionContext;
use crate::fixtures::FixtureDefinition;
use std::path::PathBuf;
use tower_lsp_server::jsonrpc::Result;
use tower_lsp_server::ls_types::*;
use tracing::info;
const EXCLUDED_PARAM_NAMES: &[&str] = &["self", "cls"];
pub(crate) struct CompletionOpts<'a> {
fixture_scope: Option<FixtureScope>,
current_fixture_name: Option<&'a str>,
insert_prefix: &'a str,
}
fn should_exclude_fixture(
fixture: &FixtureDefinition,
current_scope: Option<FixtureScope>,
) -> bool {
let Some(scope) = current_scope else {
return false;
};
fixture.scope < scope
}
fn is_fixture_excluded(
fixture: &FixtureDefinition,
declared_params: Option<&[String]>,
opts: &CompletionOpts<'_>,
) -> bool {
if EXCLUDED_PARAM_NAMES.contains(&fixture.name.as_str()) {
return true;
}
if let Some(name) = opts.current_fixture_name {
if fixture.name == name {
return true;
}
}
if let Some(params) = declared_params {
if params.contains(&fixture.name) {
return true;
}
}
if should_exclude_fixture(fixture, opts.fixture_scope) {
return true;
}
false
}
fn fixture_sort_priority(fixture: &FixtureDefinition, current_file: &std::path::Path) -> u8 {
if fixture.file_path == current_file {
0 } else if fixture.is_third_party {
3 } else if fixture.is_plugin {
2 } else {
1 }
}
fn make_sort_text(priority: u8, fixture_name: &str) -> String {
format!("{}_{}", priority, fixture_name)
}
fn make_fixture_detail(fixture: &FixtureDefinition) -> String {
let mut parts = Vec::new();
if fixture.scope != FixtureScope::Function {
parts.push(format!("({})", fixture.scope.as_str()));
}
if fixture.is_third_party {
parts.push("[third-party]".to_string());
} else if fixture.is_plugin {
parts.push("[plugin]".to_string());
}
parts.join(" ")
}
struct EnrichedFixture {
fixture: FixtureDefinition,
detail: String,
sort_text: String,
}
fn filter_and_enrich_fixtures(
available: Vec<FixtureDefinition>,
file_path: &std::path::Path,
declared_params: Option<&[String]>,
opts: &CompletionOpts<'_>,
) -> Vec<EnrichedFixture> {
available
.into_iter()
.filter(|f| !is_fixture_excluded(f, declared_params, opts))
.map(|f| {
let detail = make_fixture_detail(&f);
let priority = fixture_sort_priority(&f, file_path);
let sort_text = make_sort_text(priority, &f.name);
EnrichedFixture {
fixture: f,
detail,
sort_text,
}
})
.collect()
}
impl Backend {
pub async fn handle_completion(
&self,
params: CompletionParams,
) -> Result<Option<CompletionResponse>> {
let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let triggered_by_comma = params
.context
.as_ref()
.and_then(|ctx| ctx.trigger_character.as_deref())
== Some(",");
let insert_prefix = if triggered_by_comma { " " } else { "" };
info!(
"completion request: uri={:?}, line={}, char={}",
uri, position.line, position.character
);
if let Some(file_path) = self.uri_to_path(&uri) {
if let Some(ctx) = self.fixture_db.get_completion_context(
&file_path,
position.line,
position.character,
) {
info!("Completion context: {:?}", ctx);
let workspace_root = self.workspace_root.read().await.clone();
match ctx {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
declared_params,
fixture_scope,
..
} => {
let opts = CompletionOpts {
fixture_scope,
current_fixture_name: if is_fixture {
Some(function_name.as_str())
} else {
None
},
insert_prefix,
};
return Ok(Some(self.create_fixture_completions(
&file_path,
&declared_params,
workspace_root.as_ref(),
&opts,
)));
}
CompletionContext::FunctionBody {
function_name,
function_line,
is_fixture,
declared_params,
fixture_scope,
..
} => {
let opts = CompletionOpts {
fixture_scope,
current_fixture_name: if is_fixture {
Some(function_name.as_str())
} else {
None
},
insert_prefix,
};
return Ok(Some(self.create_fixture_completions_with_auto_add(
&file_path,
&declared_params,
function_line,
workspace_root.as_ref(),
&opts,
)));
}
CompletionContext::UsefixturesDecorator
| CompletionContext::ParametrizeIndirect => {
return Ok(Some(self.create_string_fixture_completions(
&file_path,
workspace_root.as_ref(),
insert_prefix,
)));
}
}
} else {
info!("No completion context found");
}
}
Ok(None)
}
pub(crate) fn create_fixture_completions(
&self,
file_path: &std::path::Path,
declared_params: &[String],
workspace_root: Option<&PathBuf>,
opts: &CompletionOpts<'_>,
) -> CompletionResponse {
let available = self.fixture_db.get_available_fixtures(file_path);
let enriched =
filter_and_enrich_fixtures(available, file_path, Some(declared_params), opts);
let items = enriched
.into_iter()
.map(|ef| {
let documentation = Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: Self::format_fixture_documentation(&ef.fixture, workspace_root),
}));
CompletionItem {
label: ef.fixture.name.clone(),
kind: Some(CompletionItemKind::VARIABLE),
detail: Some(ef.detail),
documentation,
insert_text: Some(format!("{}{}", opts.insert_prefix, ef.fixture.name)),
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
sort_text: Some(ef.sort_text),
..Default::default()
}
})
.collect();
CompletionResponse::Array(items)
}
pub(crate) fn create_fixture_completions_with_auto_add(
&self,
file_path: &std::path::Path,
declared_params: &[String],
function_line: usize,
workspace_root: Option<&PathBuf>,
opts: &CompletionOpts<'_>,
) -> CompletionResponse {
let available = self.fixture_db.get_available_fixtures(file_path);
let enriched =
filter_and_enrich_fixtures(available, file_path, Some(declared_params), opts);
let insertion_info = self
.fixture_db
.get_function_param_insertion_info(file_path, function_line);
let items = enriched
.into_iter()
.map(|ef| {
let documentation = Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: Self::format_fixture_documentation(&ef.fixture, workspace_root),
}));
let additional_text_edits = insertion_info.as_ref().map(|info| {
let text = match &info.multiline_indent {
Some(indent) => {
if info.needs_comma {
format!(",\n{}{}", indent, ef.fixture.name)
} else {
format!("\n{}{},", indent, ef.fixture.name)
}
}
None => {
if info.needs_comma {
format!(", {}", ef.fixture.name)
} else {
ef.fixture.name.clone()
}
}
};
let lsp_line = Self::internal_line_to_lsp(info.line);
vec![TextEdit {
range: Self::create_point_range(lsp_line, info.char_pos as u32),
new_text: text,
}]
});
CompletionItem {
label: ef.fixture.name.clone(),
kind: Some(CompletionItemKind::VARIABLE),
detail: Some(ef.detail),
documentation,
insert_text: Some(format!("{}{}", opts.insert_prefix, ef.fixture.name)),
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
additional_text_edits,
sort_text: Some(ef.sort_text),
..Default::default()
}
})
.collect();
CompletionResponse::Array(items)
}
pub(crate) fn create_string_fixture_completions(
&self,
file_path: &std::path::Path,
workspace_root: Option<&PathBuf>,
insert_prefix: &str,
) -> CompletionResponse {
let available = self.fixture_db.get_available_fixtures(file_path);
let no_filter_opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix,
};
let enriched = filter_and_enrich_fixtures(available, file_path, None, &no_filter_opts);
let items = enriched
.into_iter()
.map(|ef| {
let documentation = Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: Self::format_fixture_documentation(&ef.fixture, workspace_root),
}));
CompletionItem {
label: ef.fixture.name.clone(),
kind: Some(CompletionItemKind::TEXT),
detail: Some(ef.detail),
documentation,
insert_text: Some(format!("{}{}", insert_prefix, ef.fixture.name)),
insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
sort_text: Some(ef.sort_text),
..Default::default()
}
})
.collect();
CompletionResponse::Array(items)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixtures::types::FixtureScope;
use crate::fixtures::FixtureDatabase;
use std::path::PathBuf;
use std::sync::Arc;
fn make_fixture(name: &str, scope: FixtureScope) -> FixtureDefinition {
FixtureDefinition {
name: name.to_string(),
file_path: PathBuf::from("/tmp/test/conftest.py"),
line: 1,
end_line: 5,
start_char: 4,
end_char: 10,
docstring: None,
return_type: None,
return_type_imports: vec![],
is_third_party: false,
is_plugin: false,
dependencies: vec![],
scope,
yield_line: None,
autouse: false,
}
}
#[test]
fn test_should_exclude_fixture_test_function_allows_all() {
let func = make_fixture("f", FixtureScope::Function);
let class = make_fixture("c", FixtureScope::Class);
let module = make_fixture("m", FixtureScope::Module);
let package = make_fixture("p", FixtureScope::Package);
let session = make_fixture("s", FixtureScope::Session);
assert!(!should_exclude_fixture(&func, None));
assert!(!should_exclude_fixture(&class, None));
assert!(!should_exclude_fixture(&module, None));
assert!(!should_exclude_fixture(&package, None));
assert!(!should_exclude_fixture(&session, None));
}
#[test]
fn test_should_exclude_fixture_session_excludes_narrower() {
let func = make_fixture("f", FixtureScope::Function);
let class = make_fixture("c", FixtureScope::Class);
let module = make_fixture("m", FixtureScope::Module);
let package = make_fixture("p", FixtureScope::Package);
let session = make_fixture("s", FixtureScope::Session);
let session_scope = Some(FixtureScope::Session);
assert!(should_exclude_fixture(&func, session_scope));
assert!(should_exclude_fixture(&class, session_scope));
assert!(should_exclude_fixture(&module, session_scope));
assert!(should_exclude_fixture(&package, session_scope));
assert!(!should_exclude_fixture(&session, session_scope));
}
#[test]
fn test_should_exclude_fixture_module_excludes_narrower() {
let func = make_fixture("f", FixtureScope::Function);
let class = make_fixture("c", FixtureScope::Class);
let module = make_fixture("m", FixtureScope::Module);
let package = make_fixture("p", FixtureScope::Package);
let session = make_fixture("s", FixtureScope::Session);
let module_scope = Some(FixtureScope::Module);
assert!(should_exclude_fixture(&func, module_scope));
assert!(should_exclude_fixture(&class, module_scope));
assert!(!should_exclude_fixture(&module, module_scope));
assert!(!should_exclude_fixture(&package, module_scope));
assert!(!should_exclude_fixture(&session, module_scope));
}
#[test]
fn test_should_exclude_fixture_function_allows_all() {
let func = make_fixture("f", FixtureScope::Function);
let class = make_fixture("c", FixtureScope::Class);
let module = make_fixture("m", FixtureScope::Module);
let session = make_fixture("s", FixtureScope::Session);
let function_scope = Some(FixtureScope::Function);
assert!(!should_exclude_fixture(&func, function_scope));
assert!(!should_exclude_fixture(&class, function_scope));
assert!(!should_exclude_fixture(&module, function_scope));
assert!(!should_exclude_fixture(&session, function_scope));
}
#[test]
fn test_should_exclude_fixture_class_excludes_function() {
let func = make_fixture("f", FixtureScope::Function);
let class = make_fixture("c", FixtureScope::Class);
let module = make_fixture("m", FixtureScope::Module);
let session = make_fixture("s", FixtureScope::Session);
let class_scope = Some(FixtureScope::Class);
assert!(should_exclude_fixture(&func, class_scope));
assert!(!should_exclude_fixture(&class, class_scope));
assert!(!should_exclude_fixture(&module, class_scope));
assert!(!should_exclude_fixture(&session, class_scope));
}
#[test]
fn test_is_fixture_excluded_filters_self_cls() {
let self_fixture = make_fixture("self", FixtureScope::Function);
let cls_fixture = make_fixture("cls", FixtureScope::Function);
let normal_fixture = make_fixture("db", FixtureScope::Function);
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
assert!(is_fixture_excluded(&self_fixture, None, &opts));
assert!(is_fixture_excluded(&cls_fixture, None, &opts));
assert!(!is_fixture_excluded(&normal_fixture, None, &opts));
}
#[test]
fn test_is_fixture_excluded_filters_declared_params() {
let fixture = make_fixture("db", FixtureScope::Function);
let declared = vec!["db".to_string()];
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
assert!(is_fixture_excluded(&fixture, Some(&declared), &opts));
assert!(!is_fixture_excluded(&fixture, None, &opts));
assert!(!is_fixture_excluded(
&fixture,
Some(&["other".to_string()]),
&opts,
));
}
#[test]
fn test_is_fixture_excluded_combines_scope_and_params() {
let func_fixture = make_fixture("db", FixtureScope::Function);
let declared = vec!["db".to_string()];
let session_scope = Some(FixtureScope::Session);
let opts = CompletionOpts {
fixture_scope: session_scope,
current_fixture_name: None,
insert_prefix: "",
};
assert!(is_fixture_excluded(&func_fixture, Some(&declared), &opts,));
let undeclared: Vec<String> = vec![];
assert!(is_fixture_excluded(&func_fixture, Some(&undeclared), &opts,));
let session_opts = CompletionOpts {
fixture_scope: session_scope,
current_fixture_name: None,
insert_prefix: "",
};
assert!(is_fixture_excluded(
&make_fixture("db", FixtureScope::Session),
Some(&declared),
&session_opts,
));
assert!(!is_fixture_excluded(
&make_fixture("other", FixtureScope::Session),
Some(&undeclared),
&session_opts,
));
}
#[test]
fn test_filter_and_enrich_excludes_current_fixture() {
let file = std::path::Path::new("/tmp/test/conftest.py");
let fixtures = vec![
make_fixture("my_fixture", FixtureScope::Function),
make_fixture("other_fixture", FixtureScope::Function),
];
let opts = CompletionOpts {
fixture_scope: Some(FixtureScope::Function),
current_fixture_name: Some("my_fixture"),
insert_prefix: "",
};
let enriched = filter_and_enrich_fixtures(fixtures.clone(), file, None, &opts);
assert_eq!(enriched.len(), 1);
assert_eq!(enriched[0].fixture.name, "other_fixture");
let test_opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let enriched = filter_and_enrich_fixtures(fixtures, file, None, &test_opts);
assert_eq!(enriched.len(), 2);
}
#[test]
fn test_filter_and_enrich_excludes_scope_incompatible() {
let file_path = PathBuf::from("/tmp/test/test_file.py");
let fixtures = vec![
make_fixture("func_fix", FixtureScope::Function),
make_fixture("class_fix", FixtureScope::Class),
make_fixture("module_fix", FixtureScope::Module),
make_fixture("session_fix", FixtureScope::Session),
];
let opts = CompletionOpts {
fixture_scope: Some(FixtureScope::Session),
current_fixture_name: None,
insert_prefix: "",
};
let enriched = filter_and_enrich_fixtures(fixtures.clone(), &file_path, Some(&[]), &opts);
let names: Vec<&str> = enriched.iter().map(|e| e.fixture.name.as_str()).collect();
assert_eq!(names, vec!["session_fix"]);
let opts = CompletionOpts {
fixture_scope: Some(FixtureScope::Module),
current_fixture_name: None,
insert_prefix: "",
};
let enriched = filter_and_enrich_fixtures(fixtures.clone(), &file_path, Some(&[]), &opts);
let names: Vec<&str> = enriched.iter().map(|e| e.fixture.name.as_str()).collect();
assert_eq!(names, vec!["module_fix", "session_fix"]);
let opts = CompletionOpts {
fixture_scope: Some(FixtureScope::Function),
current_fixture_name: None,
insert_prefix: "",
};
let enriched = filter_and_enrich_fixtures(fixtures.clone(), &file_path, Some(&[]), &opts);
assert_eq!(enriched.len(), 4);
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let enriched = filter_and_enrich_fixtures(fixtures.clone(), &file_path, Some(&[]), &opts);
assert_eq!(enriched.len(), 4);
}
#[test]
fn test_filter_and_enrich_excludes_declared_params() {
let file_path = PathBuf::from("/tmp/test/test_file.py");
let fixtures = vec![
make_fixture("db", FixtureScope::Function),
make_fixture("client", FixtureScope::Function),
make_fixture("app", FixtureScope::Function),
];
let declared = vec!["db".to_string(), "client".to_string()];
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let enriched = filter_and_enrich_fixtures(fixtures, &file_path, Some(&declared), &opts);
let names: Vec<&str> = enriched.iter().map(|e| e.fixture.name.as_str()).collect();
assert_eq!(names, vec!["app"]);
}
#[test]
fn test_filter_and_enrich_excludes_self_cls() {
let file_path = PathBuf::from("/tmp/test/test_file.py");
let mut fixtures = vec![
make_fixture("self", FixtureScope::Function),
make_fixture("cls", FixtureScope::Function),
make_fixture("real_fixture", FixtureScope::Function),
];
fixtures[0].name = "self".to_string();
fixtures[1].name = "cls".to_string();
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let enriched = filter_and_enrich_fixtures(fixtures, &file_path, None, &opts);
let names: Vec<&str> = enriched.iter().map(|e| e.fixture.name.as_str()).collect();
assert_eq!(names, vec!["real_fixture"]);
}
#[test]
fn test_fixture_sort_priority_same_file() {
let current = PathBuf::from("/tmp/test/test_file.py");
let mut fixture = make_fixture("f", FixtureScope::Function);
fixture.file_path = current.clone();
assert_eq!(fixture_sort_priority(&fixture, ¤t), 0);
}
#[test]
fn test_fixture_sort_priority_conftest() {
let current = PathBuf::from("/tmp/test/test_file.py");
let mut fixture = make_fixture("f", FixtureScope::Function);
fixture.file_path = PathBuf::from("/tmp/test/conftest.py");
assert_eq!(fixture_sort_priority(&fixture, ¤t), 1);
}
#[test]
fn test_fixture_sort_priority_plugin() {
let current = PathBuf::from("/tmp/test/test_file.py");
let mut fixture = make_fixture("f", FixtureScope::Function);
fixture.file_path = PathBuf::from("/tmp/other/plugin.py");
fixture.is_plugin = true;
assert_eq!(fixture_sort_priority(&fixture, ¤t), 2);
}
#[test]
fn test_fixture_sort_priority_third_party() {
let current = PathBuf::from("/tmp/test/test_file.py");
let mut fixture = make_fixture("f", FixtureScope::Function);
fixture.file_path = PathBuf::from("/tmp/venv/lib/site-packages/pkg/fix.py");
fixture.is_third_party = true;
assert_eq!(fixture_sort_priority(&fixture, ¤t), 3);
}
#[test]
fn test_fixture_sort_priority_third_party_trumps_plugin() {
let current = PathBuf::from("/tmp/test/test_file.py");
let mut fixture = make_fixture("f", FixtureScope::Function);
fixture.file_path = PathBuf::from("/tmp/venv/lib/site-packages/pkg/fix.py");
fixture.is_third_party = true;
fixture.is_plugin = true;
assert_eq!(fixture_sort_priority(&fixture, ¤t), 3);
}
#[test]
fn test_make_fixture_detail_default_scope() {
let fixture = make_fixture("f", FixtureScope::Function);
let detail = make_fixture_detail(&fixture);
assert_eq!(detail, ""); }
#[test]
fn test_make_fixture_detail_session_scope() {
let fixture = make_fixture("f", FixtureScope::Session);
let detail = make_fixture_detail(&fixture);
assert_eq!(detail, "(session)");
}
#[test]
fn test_make_fixture_detail_third_party() {
let mut fixture = make_fixture("f", FixtureScope::Function);
fixture.is_third_party = true;
let detail = make_fixture_detail(&fixture);
assert_eq!(detail, "[third-party]");
}
#[test]
fn test_make_fixture_detail_plugin_with_scope() {
let mut fixture = make_fixture("f", FixtureScope::Module);
fixture.is_plugin = true;
let detail = make_fixture_detail(&fixture);
assert_eq!(detail, "(module) [plugin]");
}
#[test]
fn test_make_fixture_detail_third_party_overrides_plugin() {
let mut fixture = make_fixture("f", FixtureScope::Session);
fixture.is_third_party = true;
fixture.is_plugin = true;
let detail = make_fixture_detail(&fixture);
assert_eq!(detail, "(session) [third-party]");
}
#[test]
fn test_make_sort_text_ordering() {
let same_file = make_sort_text(0, "zzz");
let conftest = make_sort_text(1, "aaa");
let plugin = make_sort_text(2, "aaa");
let third_party = make_sort_text(3, "aaa");
assert!(same_file < conftest);
assert!(conftest < plugin);
assert!(plugin < third_party);
}
#[test]
fn test_make_sort_text_alpha_within_group() {
let a = make_sort_text(0, "alpha");
let b = make_sort_text(0, "beta");
assert!(a < b);
}
use tower_lsp_server::LspService;
fn make_backend_with_db(db: Arc<FixtureDatabase>) -> Backend {
let backend_slot: Arc<std::sync::Mutex<Option<Backend>>> =
Arc::new(std::sync::Mutex::new(None));
let slot_clone = backend_slot.clone();
let (_svc, _sock) = LspService::new(move |client| {
let b = Backend::new(client, db.clone());
*slot_clone.lock().unwrap() = Some(Backend {
client: b.client.clone(),
fixture_db: b.fixture_db.clone(),
workspace_root: b.workspace_root.clone(),
original_workspace_root: b.original_workspace_root.clone(),
scan_task: b.scan_task.clone(),
uri_cache: b.uri_cache.clone(),
config: b.config.clone(),
});
b
});
let result = backend_slot
.lock()
.unwrap()
.take()
.expect("Backend should have been created");
result
}
fn setup_backend_with_fixtures() -> (Backend, PathBuf) {
let db = Arc::new(FixtureDatabase::new());
let conftest_content = r#"
import pytest
@pytest.fixture
def func_fixture():
return "func"
@pytest.fixture(scope="session")
def session_fixture():
"""A session-scoped fixture."""
return "session"
@pytest.fixture(scope="module")
def module_fixture():
return "module"
"#;
let test_content = r#"
import pytest
@pytest.fixture(scope="session")
def local_session_fixture():
pass
def test_something(func_fixture):
pass
"#;
let conftest_path = PathBuf::from("/tmp/test_backend/conftest.py");
let test_path = PathBuf::from("/tmp/test_backend/test_example.py");
db.analyze_file(conftest_path, conftest_content);
db.analyze_file(test_path.clone(), test_content);
let backend = make_backend_with_db(db);
(backend, test_path)
}
fn extract_items(response: &CompletionResponse) -> &Vec<CompletionItem> {
match response {
CompletionResponse::Array(items) => items,
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[test]
fn test_create_fixture_completions_returns_items() {
let (backend, test_path) = setup_backend_with_fixtures();
let declared = vec![];
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
let items = extract_items(&response);
assert!(!items.is_empty(), "Should return completion items");
for item in items {
assert_eq!(item.kind, Some(CompletionItemKind::VARIABLE));
assert!(item.insert_text.is_some());
assert!(item.sort_text.is_some());
assert!(item.detail.is_some());
}
}
#[test]
fn test_create_fixture_completions_filters_declared() {
let (backend, test_path) = setup_backend_with_fixtures();
let declared = vec!["func_fixture".to_string()];
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
let items = extract_items(&response);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
!labels.contains(&"func_fixture"),
"func_fixture should be filtered out"
);
}
#[test]
fn test_create_fixture_completions_scope_filtering() {
let (backend, test_path) = setup_backend_with_fixtures();
let declared = vec![];
let opts = CompletionOpts {
fixture_scope: Some(FixtureScope::Session),
current_fixture_name: None,
insert_prefix: "",
};
let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
let items = extract_items(&response);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
!labels.contains(&"func_fixture"),
"func_fixture should be excluded by session scope filter"
);
assert!(
labels.contains(&"session_fixture"),
"session_fixture should be present, got: {:?}",
labels
);
}
#[test]
fn test_create_fixture_completions_detail_and_sort() {
let (backend, test_path) = setup_backend_with_fixtures();
let declared = vec![];
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
let items = extract_items(&response);
let session_item = items.iter().find(|i| i.label == "session_fixture");
assert!(session_item.is_some(), "Should find session_fixture");
let session_item = session_item.unwrap();
assert!(
session_item.detail.as_ref().unwrap().contains("session"),
"session_fixture detail should contain scope, got: {:?}",
session_item.detail
);
let func_item = items.iter().find(|i| i.label == "func_fixture");
assert!(func_item.is_some(), "Should find func_fixture");
let func_item = func_item.unwrap();
assert!(
!func_item.detail.as_ref().unwrap().contains("function"),
"func_fixture detail should not contain 'function' (default scope), got: {:?}",
func_item.detail
);
}
#[test]
fn test_create_fixture_completions_documentation() {
let (backend, test_path) = setup_backend_with_fixtures();
let declared = vec![];
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
let items = extract_items(&response);
for item in items {
assert!(
item.documentation.is_some(),
"Completion item '{}' should have documentation",
item.label
);
}
}
#[test]
fn test_create_fixture_completions_with_workspace_root() {
let (backend, test_path) = setup_backend_with_fixtures();
let declared = vec![];
let workspace_root = PathBuf::from("/tmp/test_backend");
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let response =
backend.create_fixture_completions(&test_path, &declared, Some(&workspace_root), &opts);
let items = extract_items(&response);
assert!(!items.is_empty());
}
#[test]
fn test_create_fixture_completions_with_auto_add_returns_items() {
let (backend, test_path) = setup_backend_with_fixtures();
let declared = vec![];
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let response =
backend.create_fixture_completions_with_auto_add(&test_path, &declared, 8, None, &opts);
let items = extract_items(&response);
assert!(!items.is_empty(), "Should return completion items");
for item in items {
assert_eq!(item.kind, Some(CompletionItemKind::VARIABLE));
assert!(item.sort_text.is_some());
assert!(item.detail.is_some());
}
}
#[test]
fn test_create_fixture_completions_with_auto_add_has_text_edits() {
let (backend, test_path) = setup_backend_with_fixtures();
let declared = vec!["func_fixture".to_string()];
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let response =
backend.create_fixture_completions_with_auto_add(&test_path, &declared, 8, None, &opts);
let items = extract_items(&response);
for item in items {
assert!(
item.additional_text_edits.is_some(),
"Item '{}' should have additional_text_edits for auto-add",
item.label
);
let edits = item.additional_text_edits.as_ref().unwrap();
assert_eq!(edits.len(), 1, "Should have exactly one text edit");
}
}
#[test]
fn test_create_fixture_completions_with_auto_add_scope_filter() {
let (backend, test_path) = setup_backend_with_fixtures();
let declared = vec![];
let opts = CompletionOpts {
fixture_scope: Some(FixtureScope::Session),
current_fixture_name: None,
insert_prefix: "",
};
let response =
backend.create_fixture_completions_with_auto_add(&test_path, &declared, 8, None, &opts);
let items = extract_items(&response);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
!labels.contains(&"func_fixture"),
"func_fixture should be excluded by session scope"
);
}
#[test]
fn test_create_fixture_completions_with_auto_add_filters_declared() {
let (backend, test_path) = setup_backend_with_fixtures();
let declared = vec!["session_fixture".to_string(), "func_fixture".to_string()];
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let response =
backend.create_fixture_completions_with_auto_add(&test_path, &declared, 8, None, &opts);
let items = extract_items(&response);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
!labels.contains(&"func_fixture"),
"func_fixture should be filtered"
);
assert!(
!labels.contains(&"session_fixture"),
"session_fixture should be filtered"
);
}
#[test]
fn test_create_fixture_completions_with_auto_add_filters_current_fixture() {
let (backend, file_path) = setup_backend_with_fixtures();
let opts = CompletionOpts {
fixture_scope: Some(FixtureScope::Function),
current_fixture_name: Some("func_fixture"),
insert_prefix: "",
};
let response = backend.create_fixture_completions(&file_path, &[], None, &opts);
let items = extract_items(&response);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
!labels.contains(&"func_fixture"),
"Current fixture should be excluded from completions, got: {:?}",
labels
);
assert!(
labels.contains(&"session_fixture"),
"Other fixtures should still appear"
);
}
#[test]
fn test_create_fixture_completions_comma_trigger_adds_space() {
let (backend, test_path) = setup_backend_with_fixtures();
let declared = vec![];
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: " ",
};
let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
let items = extract_items(&response);
assert!(!items.is_empty());
for item in items {
let text = item.insert_text.as_ref().unwrap();
assert!(
text.starts_with(' '),
"insert_text should start with space for comma trigger, got: {:?}",
text
);
}
}
#[test]
fn test_create_fixture_completions_no_trigger_no_space() {
let (backend, test_path) = setup_backend_with_fixtures();
let declared = vec![];
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let response = backend.create_fixture_completions(&test_path, &declared, None, &opts);
let items = extract_items(&response);
assert!(!items.is_empty());
for item in items {
let text = item.insert_text.as_ref().unwrap();
assert!(
!text.starts_with(' '),
"insert_text should NOT start with space without comma trigger, got: {:?}",
text
);
}
}
#[test]
fn test_create_fixture_completions_with_auto_add_no_existing_params() {
let db = Arc::new(FixtureDatabase::new());
let conftest_content = r#"
import pytest
@pytest.fixture
def db_fixture():
return "db"
"#;
let test_content = r#"
def test_empty_params():
pass
"#;
let conftest_path = PathBuf::from("/tmp/test_no_params/conftest.py");
let test_path = PathBuf::from("/tmp/test_no_params/test_file.py");
db.analyze_file(conftest_path, conftest_content);
db.analyze_file(test_path.clone(), test_content);
let backend = make_backend_with_db(db);
let declared: Vec<String> = vec![];
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let response =
backend.create_fixture_completions_with_auto_add(&test_path, &declared, 2, None, &opts);
let items = extract_items(&response);
assert!(!items.is_empty(), "Should return completion items");
let item = items.iter().find(|i| i.label == "db_fixture");
assert!(item.is_some(), "Should find db_fixture");
let item = item.unwrap();
let edits = item.additional_text_edits.as_ref().unwrap();
assert_eq!(edits.len(), 1);
assert_eq!(
edits[0].new_text, "db_fixture",
"Should insert fixture name without comma for empty params"
);
}
#[test]
fn test_create_string_fixture_completions_returns_items() {
let (backend, test_path) = setup_backend_with_fixtures();
let response = backend.create_string_fixture_completions(&test_path, None, "");
let items = extract_items(&response);
assert!(!items.is_empty(), "Should return string completion items");
for item in items {
assert_eq!(
item.kind,
Some(CompletionItemKind::TEXT),
"String completions should use TEXT kind"
);
assert!(item.sort_text.is_some());
assert!(item.detail.is_some());
assert!(item.documentation.is_some());
}
}
#[test]
fn test_create_string_fixture_completions_no_scope_filtering() {
let (backend, test_path) = setup_backend_with_fixtures();
let response = backend.create_string_fixture_completions(&test_path, None, "");
let items = extract_items(&response);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"func_fixture"),
"func_fixture should be in string completions, got: {:?}",
labels
);
assert!(
labels.contains(&"session_fixture"),
"session_fixture should be in string completions, got: {:?}",
labels
);
}
#[test]
fn test_create_string_fixture_completions_with_workspace_root() {
let (backend, test_path) = setup_backend_with_fixtures();
let workspace_root = PathBuf::from("/tmp/test_backend");
let response =
backend.create_string_fixture_completions(&test_path, Some(&workspace_root), "");
let items = extract_items(&response);
assert!(!items.is_empty());
}
#[test]
fn test_create_string_fixture_completions_has_detail_and_sort() {
let (backend, test_path) = setup_backend_with_fixtures();
let response = backend.create_string_fixture_completions(&test_path, None, "");
let items = extract_items(&response);
let session_item = items.iter().find(|i| i.label == "session_fixture");
assert!(session_item.is_some());
let session_item = session_item.unwrap();
assert!(
session_item.detail.as_ref().unwrap().contains("session"),
"session_fixture should have scope in detail"
);
let sort = session_item.sort_text.as_ref().unwrap();
assert!(
sort.starts_with('1') || sort.starts_with('0'),
"Sort text should start with priority digit, got: {}",
sort
);
}
#[test]
fn test_create_fixture_completions_empty_db() {
let db = Arc::new(FixtureDatabase::new());
let backend = make_backend_with_db(db);
let path = PathBuf::from("/tmp/empty/test_file.py");
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let response = backend.create_fixture_completions(&path, &[], None, &opts);
let items = extract_items(&response);
assert!(items.is_empty(), "Empty DB should return no completions");
}
#[test]
fn test_create_fixture_completions_with_auto_add_empty_db() {
let db = Arc::new(FixtureDatabase::new());
let backend = make_backend_with_db(db);
let path = PathBuf::from("/tmp/empty/test_file.py");
let opts = CompletionOpts {
fixture_scope: None,
current_fixture_name: None,
insert_prefix: "",
};
let response = backend.create_fixture_completions_with_auto_add(&path, &[], 1, None, &opts);
let items = extract_items(&response);
assert!(items.is_empty(), "Empty DB should return no completions");
}
#[test]
fn test_create_string_fixture_completions_empty_db() {
let db = Arc::new(FixtureDatabase::new());
let backend = make_backend_with_db(db);
let path = PathBuf::from("/tmp/empty/test_file.py");
let response = backend.create_string_fixture_completions(&path, None, "");
let items = extract_items(&response);
assert!(items.is_empty(), "Empty DB should return no completions");
}
}