use ntest::timeout;
use pytest_language_server::FixtureDatabase;
use std::path::PathBuf;
#[test]
#[timeout(30000)]
fn test_fixture_definition_detection() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
@fixture
def another_fixture():
return "hello"
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
assert!(db.definitions.contains_key("my_fixture"));
assert!(db.definitions.contains_key("another_fixture"));
let my_fixture_defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(my_fixture_defs.len(), 1);
assert_eq!(my_fixture_defs[0].name, "my_fixture");
assert_eq!(my_fixture_defs[0].file_path, conftest_path);
}
#[test]
#[timeout(30000)]
fn test_fixture_usage_detection() {
let db = FixtureDatabase::new();
let test_content = r#"
def test_something(my_fixture, another_fixture):
assert my_fixture == 42
assert another_fixture == "hello"
def test_other(my_fixture):
assert my_fixture > 0
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), test_content);
assert!(db.usages.contains_key(&test_path));
let usages = db.usages.get(&test_path).unwrap();
assert!(usages.iter().any(|u| u.name == "my_fixture"));
assert!(usages.iter().any(|u| u.name == "another_fixture"));
}
#[test]
#[timeout(30000)]
fn test_go_to_definition() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_something(my_fixture):
assert my_fixture == 42
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let definition = db.find_fixture_definition(&test_path, 1, 19);
assert!(definition.is_some(), "Definition should be found");
let def = definition.unwrap();
assert_eq!(def.name, "my_fixture");
assert_eq!(def.file_path, conftest_path);
}
#[test]
#[timeout(30000)]
fn test_fixture_decorator_variations() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
from pytest import fixture
@pytest.fixture
def fixture1():
pass
@pytest.fixture()
def fixture2():
pass
@fixture
def fixture3():
pass
@fixture()
def fixture4():
pass
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path, conftest_content);
assert!(db.definitions.contains_key("fixture1"));
assert!(db.definitions.contains_key("fixture2"));
assert!(db.definitions.contains_key("fixture3"));
assert!(db.definitions.contains_key("fixture4"));
}
#[test]
#[timeout(30000)]
fn test_fixture_in_test_file() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def local_fixture():
return 42
def test_something(local_fixture):
assert local_fixture == 42
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), test_content);
assert!(db.definitions.contains_key("local_fixture"));
let local_fixture_defs = db.definitions.get("local_fixture").unwrap();
assert_eq!(local_fixture_defs.len(), 1);
assert_eq!(local_fixture_defs[0].name, "local_fixture");
assert_eq!(local_fixture_defs[0].file_path, test_path);
assert!(db.usages.contains_key(&test_path));
let usages = db.usages.get(&test_path).unwrap();
assert!(usages.iter().any(|u| u.name == "local_fixture"));
let usage_line = usages
.iter()
.find(|u| u.name == "local_fixture")
.map(|u| u.line)
.unwrap();
let definition = db.find_fixture_definition(&test_path, (usage_line - 1) as u32, 19);
assert!(
definition.is_some(),
"Should find definition for fixture in same file. Line: {}, char: 19",
usage_line
);
let def = definition.unwrap();
assert_eq!(def.name, "local_fixture");
assert_eq!(def.file_path, test_path);
}
#[test]
#[timeout(30000)]
fn test_async_test_functions() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
async def test_async_function(my_fixture):
assert my_fixture == 42
def test_sync_function(my_fixture):
assert my_fixture == 42
"#;
let test_path = PathBuf::from("/tmp/test/test_async.py");
db.analyze_file(test_path.clone(), test_content);
assert!(db.definitions.contains_key("my_fixture"));
assert!(db.usages.contains_key(&test_path));
let usages = db.usages.get(&test_path).unwrap();
let fixture_usages: Vec<_> = usages.iter().filter(|u| u.name == "my_fixture").collect();
assert_eq!(
fixture_usages.len(),
2,
"Should detect fixture usage in both async and sync tests"
);
}
#[test]
#[timeout(30000)]
fn test_extract_word_at_position() {
let db = FixtureDatabase::new();
let line = "def test_something(my_fixture):";
assert_eq!(
db.extract_word_at_position(line, 19),
Some("my_fixture".to_string())
);
assert_eq!(
db.extract_word_at_position(line, 20),
Some("my_fixture".to_string())
);
assert_eq!(
db.extract_word_at_position(line, 28),
Some("my_fixture".to_string())
);
assert_eq!(
db.extract_word_at_position(line, 0),
Some("def".to_string())
);
assert_eq!(db.extract_word_at_position(line, 3), None);
assert_eq!(
db.extract_word_at_position(line, 4),
Some("test_something".to_string())
);
assert_eq!(db.extract_word_at_position(line, 18), None);
assert_eq!(db.extract_word_at_position(line, 29), None);
assert_eq!(db.extract_word_at_position(line, 31), None);
}
#[test]
#[timeout(30000)]
fn test_extract_word_at_position_fixture_definition() {
let db = FixtureDatabase::new();
let line = "@pytest.fixture";
assert_eq!(db.extract_word_at_position(line, 0), None);
assert_eq!(
db.extract_word_at_position(line, 1),
Some("pytest".to_string())
);
assert_eq!(db.extract_word_at_position(line, 7), None);
assert_eq!(
db.extract_word_at_position(line, 8),
Some("fixture".to_string())
);
let line2 = "def foo(other_fixture):";
assert_eq!(
db.extract_word_at_position(line2, 0),
Some("def".to_string())
);
assert_eq!(db.extract_word_at_position(line2, 3), None);
assert_eq!(
db.extract_word_at_position(line2, 4),
Some("foo".to_string())
);
assert_eq!(
db.extract_word_at_position(line2, 8),
Some("other_fixture".to_string())
);
assert_eq!(db.extract_word_at_position(line2, 7), None);
}
#[test]
#[timeout(30000)]
fn test_word_detection_only_on_fixtures() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_something(my_fixture, regular_param):
assert my_fixture == 42
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), test_content);
assert_eq!(db.find_fixture_definition(&test_path, 1, 0), None);
assert_eq!(db.find_fixture_definition(&test_path, 1, 4), None);
let result = db.find_fixture_definition(&test_path, 1, 19);
assert!(result.is_some());
let def = result.unwrap();
assert_eq!(def.name, "my_fixture");
assert_eq!(db.find_fixture_definition(&test_path, 1, 31), None);
assert_eq!(db.find_fixture_definition(&test_path, 1, 18), None); assert_eq!(db.find_fixture_definition(&test_path, 1, 29), None); }
#[test]
#[timeout(30000)]
fn test_self_referencing_fixture() {
let db = FixtureDatabase::new();
let parent_conftest_content = r#"
import pytest
@pytest.fixture
def foo():
return "parent"
"#;
let parent_conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(parent_conftest_path.clone(), parent_conftest_content);
let child_conftest_content = r#"
import pytest
@pytest.fixture
def foo(foo):
return foo + " child"
"#;
let child_conftest_path = PathBuf::from("/tmp/test/subdir/conftest.py");
db.analyze_file(child_conftest_path.clone(), child_conftest_content);
let result = db.find_fixture_definition(&child_conftest_path, 4, 8);
assert!(
result.is_some(),
"Should find parent definition for self-referencing fixture"
);
let def = result.unwrap();
assert_eq!(def.name, "foo");
assert_eq!(
def.file_path, parent_conftest_path,
"Should resolve to parent conftest.py, not the child"
);
assert_eq!(def.line, 5, "Should point to line 5 of parent conftest.py");
}
#[test]
#[timeout(30000)]
fn test_fixture_overriding_same_file() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return "first"
@pytest.fixture
def my_fixture():
return "second"
def test_something(my_fixture):
assert my_fixture == "second"
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let result = db.find_fixture_definition(&test_path, 11, 19);
assert!(result.is_some(), "Should find fixture definition");
let def = result.unwrap();
assert_eq!(def.name, "my_fixture");
assert_eq!(def.file_path, test_path);
}
#[test]
#[timeout(30000)]
fn test_fixture_overriding_conftest_hierarchy() {
let db = FixtureDatabase::new();
let root_conftest_content = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "root"
"#;
let root_conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(root_conftest_path.clone(), root_conftest_content);
let sub_conftest_content = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "subdir"
"#;
let sub_conftest_path = PathBuf::from("/tmp/test/subdir/conftest.py");
db.analyze_file(sub_conftest_path.clone(), sub_conftest_content);
let test_content = r#"
def test_something(shared_fixture):
assert shared_fixture == "subdir"
"#;
let test_path = PathBuf::from("/tmp/test/subdir/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let result = db.find_fixture_definition(&test_path, 1, 19);
assert!(result.is_some(), "Should find fixture definition");
let def = result.unwrap();
assert_eq!(def.name, "shared_fixture");
assert_eq!(
def.file_path, sub_conftest_path,
"Should resolve to closest conftest.py"
);
let parent_test_content = r#"
def test_parent(shared_fixture):
assert shared_fixture == "root"
"#;
let parent_test_path = PathBuf::from("/tmp/test/test_parent.py");
db.analyze_file(parent_test_path.clone(), parent_test_content);
let result = db.find_fixture_definition(&parent_test_path, 1, 16);
assert!(result.is_some(), "Should find fixture definition");
let def = result.unwrap();
assert_eq!(def.name, "shared_fixture");
assert_eq!(
def.file_path, root_conftest_path,
"Should resolve to root conftest.py"
);
}
#[test]
#[timeout(30000)]
fn test_scoped_references() {
let db = FixtureDatabase::new();
let root_conftest_content = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "root"
"#;
let root_conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(root_conftest_path.clone(), root_conftest_content);
let sub_conftest_content = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "subdir"
"#;
let sub_conftest_path = PathBuf::from("/tmp/test/subdir/conftest.py");
db.analyze_file(sub_conftest_path.clone(), sub_conftest_content);
let root_test_content = r#"
def test_root(shared_fixture):
assert shared_fixture == "root"
"#;
let root_test_path = PathBuf::from("/tmp/test/test_root.py");
db.analyze_file(root_test_path.clone(), root_test_content);
let sub_test_content = r#"
def test_sub(shared_fixture):
assert shared_fixture == "subdir"
"#;
let sub_test_path = PathBuf::from("/tmp/test/subdir/test_sub.py");
db.analyze_file(sub_test_path.clone(), sub_test_content);
let sub_test2_content = r#"
def test_sub2(shared_fixture):
assert shared_fixture == "subdir"
"#;
let sub_test2_path = PathBuf::from("/tmp/test/subdir/test_sub2.py");
db.analyze_file(sub_test2_path.clone(), sub_test2_content);
let root_definitions = db.definitions.get("shared_fixture").unwrap();
let root_definition = root_definitions
.iter()
.find(|d| d.file_path == root_conftest_path)
.unwrap();
let sub_definition = root_definitions
.iter()
.find(|d| d.file_path == sub_conftest_path)
.unwrap();
let root_refs = db.find_references_for_definition(root_definition);
assert_eq!(
root_refs.len(),
1,
"Root definition should have 1 reference (from root test)"
);
assert_eq!(root_refs[0].file_path, root_test_path);
let sub_refs = db.find_references_for_definition(sub_definition);
assert_eq!(
sub_refs.len(),
2,
"Subdir definition should have 2 references (from subdir tests)"
);
let sub_ref_paths: Vec<_> = sub_refs.iter().map(|r| &r.file_path).collect();
assert!(sub_ref_paths.contains(&&sub_test_path));
assert!(sub_ref_paths.contains(&&sub_test2_path));
let all_refs = db.find_fixture_references("shared_fixture");
assert_eq!(
all_refs.len(),
3,
"Should find 3 total references across all scopes"
);
}
#[test]
#[timeout(30000)]
fn test_multiline_parameters() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def foo():
return 42
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_xxx(
foo,
):
assert foo == 42
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), test_content);
if let Some(usages) = db.usages.get(&test_path) {
println!("Usages recorded:");
for usage in usages.iter() {
println!(" {} at line {} (1-indexed)", usage.name, usage.line);
}
} else {
println!("No usages recorded for test file");
}
let result = db.find_fixture_definition(&test_path, 2, 4);
assert!(
result.is_some(),
"Should find fixture definition when cursor is on parameter line"
);
let def = result.unwrap();
assert_eq!(def.name, "foo");
}
#[test]
#[timeout(30000)]
fn test_find_references_from_usage() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def foo(): ...
def test_xxx(foo):
pass
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let foo_defs = db.definitions.get("foo").unwrap();
assert_eq!(foo_defs.len(), 1, "Should have exactly one foo definition");
let foo_def = &foo_defs[0];
assert_eq!(foo_def.line, 5, "foo definition should be on line 5");
let refs_from_def = db.find_references_for_definition(foo_def);
println!("References from definition:");
for r in &refs_from_def {
println!(" {} at line {}", r.name, r.line);
}
assert_eq!(
refs_from_def.len(),
1,
"Should find 1 usage reference (test_xxx parameter)"
);
assert_eq!(refs_from_def[0].line, 8, "Usage should be on line 8");
let fixture_name = db.find_fixture_at_position(&test_path, 7, 13);
println!(
"\nfind_fixture_at_position(line 7, char 13): {:?}",
fixture_name
);
assert_eq!(
fixture_name,
Some("foo".to_string()),
"Should find fixture name at usage position"
);
let resolved_def = db.find_fixture_definition(&test_path, 7, 13);
println!(
"\nfind_fixture_definition(line 7, char 13): {:?}",
resolved_def.as_ref().map(|d| (d.line, &d.file_path))
);
assert!(resolved_def.is_some(), "Should resolve usage to definition");
assert_eq!(
resolved_def.unwrap(),
*foo_def,
"Should resolve to the correct definition"
);
}
#[test]
#[timeout(30000)]
fn test_find_references_with_ellipsis_body() {
let db = FixtureDatabase::new();
let test_content = r#"@pytest.fixture
def foo(): ...
def test_xxx(foo):
pass
"#;
let test_path = PathBuf::from("/tmp/test/test_codegen.py");
db.analyze_file(test_path.clone(), test_content);
let foo_defs = db.definitions.get("foo");
println!(
"foo definitions: {:?}",
foo_defs
.as_ref()
.map(|defs| defs.iter().map(|d| d.line).collect::<Vec<_>>())
);
if let Some(usages) = db.usages.get(&test_path) {
println!("usages:");
for u in usages.iter() {
println!(" {} at line {}", u.name, u.line);
}
}
assert!(foo_defs.is_some(), "Should find foo definition");
let foo_def = &foo_defs.unwrap()[0];
let usages = db.usages.get(&test_path).unwrap();
let foo_usage = usages.iter().find(|u| u.name == "foo").unwrap();
let usage_lsp_line = (foo_usage.line - 1) as u32;
println!("\nTesting from usage at LSP line {}", usage_lsp_line);
let fixture_name = db.find_fixture_at_position(&test_path, usage_lsp_line, 13);
assert_eq!(
fixture_name,
Some("foo".to_string()),
"Should find foo at usage"
);
let def_from_usage = db.find_fixture_definition(&test_path, usage_lsp_line, 13);
assert!(
def_from_usage.is_some(),
"Should resolve usage to definition"
);
assert_eq!(def_from_usage.unwrap(), *foo_def);
}
#[test]
#[timeout(30000)]
fn test_fixture_hierarchy_parent_references() {
let db = FixtureDatabase::new();
let parent_content = r#"
import pytest
@pytest.fixture
def cli_runner():
"""Parent fixture"""
return "parent"
"#;
let parent_conftest = PathBuf::from("/tmp/project/conftest.py");
db.analyze_file(parent_conftest.clone(), parent_content);
let child_content = r#"
import pytest
@pytest.fixture
def cli_runner(cli_runner):
"""Child override that uses parent"""
return cli_runner
"#;
let child_conftest = PathBuf::from("/tmp/project/subdir/conftest.py");
db.analyze_file(child_conftest.clone(), child_content);
let test_content = r#"
def test_one(cli_runner):
pass
def test_two(cli_runner):
pass
"#;
let test_path = PathBuf::from("/tmp/project/subdir/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let parent_defs = db.definitions.get("cli_runner").unwrap();
let parent_def = parent_defs
.iter()
.find(|d| d.file_path == parent_conftest)
.unwrap();
println!(
"\nParent definition: {:?}:{}",
parent_def.file_path, parent_def.line
);
let refs = db.find_references_for_definition(parent_def);
println!("\nReferences for parent definition:");
for r in &refs {
println!(" {} at {:?}:{}", r.name, r.file_path, r.line);
}
assert!(
refs.len() <= 2,
"Parent should have at most 2 references: child definition and its parameter, got {}",
refs.len()
);
let child_refs: Vec<_> = refs
.iter()
.filter(|r| r.file_path == child_conftest)
.collect();
assert!(
!child_refs.is_empty(),
"Parent references should include child fixture definition"
);
let test_refs: Vec<_> = refs.iter().filter(|r| r.file_path == test_path).collect();
assert!(
test_refs.is_empty(),
"Parent references should NOT include child's test file usages"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_hierarchy_child_references() {
let db = FixtureDatabase::new();
let parent_content = r#"
import pytest
@pytest.fixture
def cli_runner():
return "parent"
"#;
let parent_conftest = PathBuf::from("/tmp/project/conftest.py");
db.analyze_file(parent_conftest.clone(), parent_content);
let child_content = r#"
import pytest
@pytest.fixture
def cli_runner(cli_runner):
return cli_runner
"#;
let child_conftest = PathBuf::from("/tmp/project/subdir/conftest.py");
db.analyze_file(child_conftest.clone(), child_content);
let test_content = r#"
def test_one(cli_runner):
pass
def test_two(cli_runner):
pass
"#;
let test_path = PathBuf::from("/tmp/project/subdir/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let child_defs = db.definitions.get("cli_runner").unwrap();
let child_def = child_defs
.iter()
.find(|d| d.file_path == child_conftest)
.unwrap();
println!(
"\nChild definition: {:?}:{}",
child_def.file_path, child_def.line
);
let refs = db.find_references_for_definition(child_def);
println!("\nReferences for child definition:");
for r in &refs {
println!(" {} at {:?}:{}", r.name, r.file_path, r.line);
}
assert!(
refs.len() >= 2,
"Child should have at least 2 references from test file, got {}",
refs.len()
);
let test_refs: Vec<_> = refs.iter().filter(|r| r.file_path == test_path).collect();
assert_eq!(
test_refs.len(),
2,
"Should have 2 references from test file"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_hierarchy_child_parameter_references() {
let db = FixtureDatabase::new();
let parent_content = r#"
import pytest
@pytest.fixture
def cli_runner():
return "parent"
"#;
let parent_conftest = PathBuf::from("/tmp/project/conftest.py");
db.analyze_file(parent_conftest.clone(), parent_content);
let child_content = r#"
import pytest
@pytest.fixture
def cli_runner(cli_runner):
return cli_runner
"#;
let child_conftest = PathBuf::from("/tmp/project/subdir/conftest.py");
db.analyze_file(child_conftest.clone(), child_content);
let resolved_def = db.find_fixture_definition(&child_conftest, 4, 15);
assert!(
resolved_def.is_some(),
"Child parameter should resolve to parent definition"
);
let def = resolved_def.unwrap();
assert_eq!(
def.file_path, parent_conftest,
"Should resolve to parent conftest"
);
let refs = db.find_references_for_definition(&def);
println!("\nReferences for parent (from child parameter):");
for r in &refs {
println!(" {} at {:?}:{}", r.name, r.file_path, r.line);
}
let child_refs: Vec<_> = refs
.iter()
.filter(|r| r.file_path == child_conftest)
.collect();
assert!(
!child_refs.is_empty(),
"Parent references should include child fixture parameter"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_hierarchy_usage_from_test() {
let db = FixtureDatabase::new();
let parent_content = r#"
import pytest
@pytest.fixture
def cli_runner():
return "parent"
"#;
let parent_conftest = PathBuf::from("/tmp/project/conftest.py");
db.analyze_file(parent_conftest.clone(), parent_content);
let child_content = r#"
import pytest
@pytest.fixture
def cli_runner(cli_runner):
return cli_runner
"#;
let child_conftest = PathBuf::from("/tmp/project/subdir/conftest.py");
db.analyze_file(child_conftest.clone(), child_content);
let test_content = r#"
def test_one(cli_runner):
pass
def test_two(cli_runner):
pass
def test_three(cli_runner):
pass
"#;
let test_path = PathBuf::from("/tmp/project/subdir/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let resolved_def = db.find_fixture_definition(&test_path, 1, 13);
assert!(
resolved_def.is_some(),
"Usage should resolve to child definition"
);
let def = resolved_def.unwrap();
assert_eq!(
def.file_path, child_conftest,
"Should resolve to child conftest (not parent)"
);
let refs = db.find_references_for_definition(&def);
println!("\nReferences for child (from test usage):");
for r in &refs {
println!(" {} at {:?}:{}", r.name, r.file_path, r.line);
}
let test_refs: Vec<_> = refs.iter().filter(|r| r.file_path == test_path).collect();
assert_eq!(test_refs.len(), 3, "Should find all 3 usages in test file");
}
#[test]
#[timeout(30000)]
fn test_fixture_hierarchy_multiple_levels() {
let db = FixtureDatabase::new();
let grandparent_content = r#"
import pytest
@pytest.fixture
def db():
return "grandparent_db"
"#;
let grandparent_conftest = PathBuf::from("/tmp/project/conftest.py");
db.analyze_file(grandparent_conftest.clone(), grandparent_content);
let parent_content = r#"
import pytest
@pytest.fixture
def db(db):
return f"parent_{db}"
"#;
let parent_conftest = PathBuf::from("/tmp/project/api/conftest.py");
db.analyze_file(parent_conftest.clone(), parent_content);
let child_content = r#"
import pytest
@pytest.fixture
def db(db):
return f"child_{db}"
"#;
let child_conftest = PathBuf::from("/tmp/project/api/tests/conftest.py");
db.analyze_file(child_conftest.clone(), child_content);
let test_content = r#"
def test_db(db):
pass
"#;
let test_path = PathBuf::from("/tmp/project/api/tests/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let all_defs = db.definitions.get("db").unwrap();
assert_eq!(all_defs.len(), 3, "Should have 3 definitions");
let grandparent_def = all_defs
.iter()
.find(|d| d.file_path == grandparent_conftest)
.unwrap();
let parent_def = all_defs
.iter()
.find(|d| d.file_path == parent_conftest)
.unwrap();
let child_def = all_defs
.iter()
.find(|d| d.file_path == child_conftest)
.unwrap();
let resolved = db.find_fixture_definition(&test_path, 1, 12);
assert_eq!(
resolved.as_ref(),
Some(child_def),
"Test should use child definition"
);
let child_refs = db.find_references_for_definition(child_def);
let test_refs: Vec<_> = child_refs
.iter()
.filter(|r| r.file_path == test_path)
.collect();
assert!(
!test_refs.is_empty(),
"Child should have test file references"
);
let parent_refs = db.find_references_for_definition(parent_def);
let child_param_refs: Vec<_> = parent_refs
.iter()
.filter(|r| r.file_path == child_conftest)
.collect();
let test_refs_in_parent: Vec<_> = parent_refs
.iter()
.filter(|r| r.file_path == test_path)
.collect();
assert!(
!child_param_refs.is_empty(),
"Parent should have child parameter reference"
);
assert!(
test_refs_in_parent.is_empty(),
"Parent should NOT have test file references"
);
let grandparent_refs = db.find_references_for_definition(grandparent_def);
let parent_param_refs: Vec<_> = grandparent_refs
.iter()
.filter(|r| r.file_path == parent_conftest)
.collect();
let child_refs_in_gp: Vec<_> = grandparent_refs
.iter()
.filter(|r| r.file_path == child_conftest)
.collect();
assert!(
!parent_param_refs.is_empty(),
"Grandparent should have parent parameter reference"
);
assert!(
child_refs_in_gp.is_empty(),
"Grandparent should NOT have child references"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_hierarchy_same_file_override() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def base():
return "base"
@pytest.fixture
def base(base):
return f"override_{base}"
def test_uses_override(base):
pass
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), content);
let defs = db.definitions.get("base").unwrap();
assert_eq!(defs.len(), 2, "Should have 2 definitions in same file");
println!("\nDefinitions found:");
for d in defs.iter() {
println!(" base at line {}", d.line);
}
if let Some(usages) = db.usages.get(&test_path) {
println!("\nUsages found:");
for u in usages.iter() {
println!(" {} at line {}", u.name, u.line);
}
} else {
println!("\nNo usages found!");
}
let resolved = db.find_fixture_definition(&test_path, 11, 23);
println!("\nResolved: {:?}", resolved.as_ref().map(|d| d.line));
assert!(resolved.is_some(), "Should resolve to override definition");
let override_def = defs.iter().find(|d| d.line == 9).unwrap();
println!("Override def at line: {}", override_def.line);
assert_eq!(resolved.as_ref(), Some(override_def));
}
#[test]
#[timeout(30000)]
fn test_cursor_position_on_definition_line() {
let db = FixtureDatabase::new();
let parent_content = r#"
import pytest
@pytest.fixture
def cli_runner():
return "parent"
"#;
let parent_conftest = PathBuf::from("/tmp/conftest.py");
db.analyze_file(parent_conftest.clone(), parent_content);
let content = r#"
import pytest
@pytest.fixture
def cli_runner(cli_runner):
return cli_runner
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), content);
println!("\n=== Testing character positions on line 5 ===");
if let Some(usages) = db.usages.get(&test_path) {
println!("\nUsages found:");
for u in usages.iter() {
println!(
" {} at line {}, chars {}-{}",
u.name, u.line, u.start_char, u.end_char
);
}
} else {
println!("\nNo usages found!");
}
let line_content = "def cli_runner(cli_runner):";
println!("\nLine content: '{}'", line_content);
println!("\nPosition 4 (function name):");
let word_at_4 = db.extract_word_at_position(line_content, 4);
println!(" Word at cursor: {:?}", word_at_4);
let fixture_name_at_4 = db.find_fixture_at_position(&test_path, 4, 4);
println!(" find_fixture_at_position: {:?}", fixture_name_at_4);
let resolved_4 = db.find_fixture_definition(&test_path, 4, 4); println!(
" Resolved: {:?}",
resolved_4.as_ref().map(|d| (d.name.as_str(), d.line))
);
println!("\nPosition 16 (parameter name):");
let word_at_16 = db.extract_word_at_position(line_content, 16);
println!(" Word at cursor: {:?}", word_at_16);
if let Some(usages) = db.usages.get(&test_path) {
for usage in usages.iter() {
println!(" Checking usage: {} at line {}", usage.name, usage.line);
if usage.line == 5 && usage.name == "cli_runner" {
println!(" MATCH! Usage matches our position");
}
}
}
let fixture_name_at_16 = db.find_fixture_at_position(&test_path, 4, 16);
println!(" find_fixture_at_position: {:?}", fixture_name_at_16);
let resolved_16 = db.find_fixture_definition(&test_path, 4, 16); println!(
" Resolved: {:?}",
resolved_16.as_ref().map(|d| (d.name.as_str(), d.line))
);
assert_eq!(word_at_4, Some("cli_runner".to_string()));
assert_eq!(word_at_16, Some("cli_runner".to_string()));
println!("\n=== ACTUAL vs EXPECTED ===");
println!("Position 4 (function name):");
println!(
" Actual: {:?}",
resolved_4.as_ref().map(|d| (&d.file_path, d.line))
);
println!(" Expected: test file, line 5 (the child definition itself)");
println!("\nPosition 16 (parameter):");
println!(
" Actual: {:?}",
resolved_16.as_ref().map(|d| (&d.file_path, d.line))
);
println!(" Expected: conftest, line 5 (the parent definition)");
if let Some(ref def) = resolved_16 {
assert_eq!(
def.file_path, parent_conftest,
"Parameter should resolve to parent definition"
);
} else {
panic!("Position 16 (parameter) should resolve to parent definition");
}
}
#[test]
#[timeout(30000)]
fn test_undeclared_fixture_detection_in_test() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_example():
result = my_fixture.get()
assert result == 42
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(undeclared.len(), 1, "Should detect one undeclared fixture");
let fixture = &undeclared[0];
assert_eq!(fixture.name, "my_fixture");
assert_eq!(fixture.function_name, "test_example");
assert_eq!(fixture.line, 3); }
#[test]
#[timeout(30000)]
fn test_undeclared_fixture_detection_in_fixture() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def base_fixture():
return "base"
@pytest.fixture
def helper_fixture():
return "helper"
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
import pytest
@pytest.fixture
def my_fixture(base_fixture):
data = helper_fixture.value
return f"{base_fixture}-{data}"
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(undeclared.len(), 1, "Should detect one undeclared fixture");
let fixture = &undeclared[0];
assert_eq!(fixture.name, "helper_fixture");
assert_eq!(fixture.function_name, "my_fixture");
assert_eq!(fixture.line, 6); }
#[test]
#[timeout(30000)]
fn test_no_false_positive_for_declared_fixtures() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_example(my_fixture):
result = my_fixture
assert result == 42
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
0,
"Should not detect any undeclared fixtures"
);
}
#[test]
#[timeout(30000)]
fn test_no_false_positive_for_non_fixtures() {
let db = FixtureDatabase::new();
let test_content = r#"
def test_example():
my_variable = 42
result = my_variable + 10
assert result == 52
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
0,
"Should not detect any undeclared fixtures"
);
}
#[test]
#[timeout(30000)]
fn test_undeclared_fixture_not_available_in_hierarchy() {
let db = FixtureDatabase::new();
let other_conftest = r#"
import pytest
@pytest.fixture
def other_fixture():
return "other"
"#;
let other_path = PathBuf::from("/other/conftest.py");
db.analyze_file(other_path.clone(), other_conftest);
let test_content = r#"
def test_example():
result = other_fixture.value
assert result == "other"
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
0,
"Should not detect fixtures not in hierarchy"
);
}
#[test]
#[timeout(30000)]
fn test_undeclared_fixture_in_async_test() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def http_client():
return "MockClient"
async def test_with_undeclared():
response = await http_client.query("test")
assert response == "test"
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), content);
let undeclared = db.get_undeclared_fixtures(&test_path);
println!("Found {} undeclared fixtures", undeclared.len());
for u in &undeclared {
println!(" - {} at line {} in {}", u.name, u.line, u.function_name);
}
assert_eq!(undeclared.len(), 1, "Should detect one undeclared fixture");
assert_eq!(undeclared[0].name, "http_client");
assert_eq!(undeclared[0].function_name, "test_with_undeclared");
assert_eq!(undeclared[0].line, 9);
}
#[test]
#[timeout(30000)]
fn test_undeclared_fixture_in_assert_statement() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def expected_value():
return 42
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_assertion():
result = calculate_value()
assert result == expected_value
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
1,
"Should detect one undeclared fixture in assert"
);
assert_eq!(undeclared[0].name, "expected_value");
assert_eq!(undeclared[0].function_name, "test_assertion");
}
#[test]
#[timeout(30000)]
fn test_no_false_positive_for_local_variable() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def foo():
return "fixture"
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_with_local_variable():
foo = "local variable"
result = foo.upper()
assert result == "LOCAL VARIABLE"
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
0,
"Should not detect undeclared fixture when name is a local variable"
);
}
#[test]
#[timeout(30000)]
fn test_no_false_positive_for_imported_name() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def foo():
return "fixture"
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
from mymodule import foo
def test_with_import():
result = foo.something()
assert result == "value"
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
0,
"Should not detect undeclared fixture when name is imported"
);
}
#[test]
#[timeout(30000)]
fn test_warn_for_fixture_used_directly() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def foo():
return "fixture"
def test_using_fixture_directly():
# This is an error - fixtures must be declared as parameters
result = foo.something()
assert result == "value"
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
1,
"Should detect fixture used directly without parameter declaration"
);
assert_eq!(undeclared[0].name, "foo");
assert_eq!(undeclared[0].function_name, "test_using_fixture_directly");
}
#[test]
#[timeout(30000)]
fn test_no_false_positive_for_module_level_assignment() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def foo():
return "fixture"
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
# Module-level assignment
foo = SomeClass()
def test_with_module_var():
result = foo.method()
assert result == "value"
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
0,
"Should not detect undeclared fixture when name is assigned at module level"
);
}
#[test]
#[timeout(30000)]
fn test_no_false_positive_for_function_definition() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def foo():
return "fixture"
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def foo():
return "not a fixture"
def test_with_function():
result = foo()
assert result == "not a fixture"
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
0,
"Should not detect undeclared fixture when name is a regular function"
);
}
#[test]
#[timeout(30000)]
fn test_no_false_positive_for_class_definition() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def MyClass():
return "fixture"
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
class MyClass:
pass
def test_with_class():
obj = MyClass()
assert obj is not None
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
0,
"Should not detect undeclared fixture when name is a class"
);
}
#[test]
#[timeout(30000)]
fn test_line_aware_local_variable_scope() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def http_client():
return "MockClient"
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"async def test_example():
# Line 1: http_client should be flagged (not yet assigned)
result = await http_client.get("/api")
# Line 3: Now we assign http_client locally
http_client = "local"
# Line 5: http_client should NOT be flagged (local var now)
result2 = await http_client.get("/api2")
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
1,
"Should detect http_client only before local assignment"
);
assert_eq!(undeclared[0].name, "http_client");
assert_eq!(
undeclared[0].line, 3,
"Should flag usage on line 3 (before assignment on line 5)"
);
}
#[test]
#[timeout(30000)]
fn test_same_line_assignment_and_usage() {
let db = FixtureDatabase::new();
let conftest_content = r#"import pytest
@pytest.fixture
def http_client():
return "parent"
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"async def test_example():
# This references the fixture on the RHS, then assigns to local var
http_client = await http_client.get("/api")
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(undeclared.len(), 1);
assert_eq!(undeclared[0].name, "http_client");
assert_eq!(undeclared[0].line, 3);
}
#[test]
#[timeout(30000)]
fn test_no_false_positive_for_later_assignment() {
let db = FixtureDatabase::new();
let conftest_content = r#"import pytest
@pytest.fixture
def http_client():
return "fixture"
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"async def test_example():
result = await http_client.get("/api") # Should be flagged
# Now assign locally
http_client = "local"
# This should NOT be flagged because variable is now assigned
result2 = http_client
"#;
let test_path = PathBuf::from("/tmp/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
1,
"Should detect exactly one undeclared fixture"
);
assert_eq!(undeclared[0].name, "http_client");
assert_eq!(
undeclared[0].line, 2,
"Should flag usage on line 2 before assignment on line 4"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_resolution_priority_deterministic() {
let db = FixtureDatabase::new();
let root_content = r#"
import pytest
@pytest.fixture
def db():
return "root_db"
"#;
let root_conftest = PathBuf::from("/tmp/project/conftest.py");
db.analyze_file(root_conftest.clone(), root_content);
let unrelated_content = r#"
import pytest
@pytest.fixture
def db():
return "unrelated_db"
"#;
let unrelated_conftest = PathBuf::from("/tmp/other/conftest.py");
db.analyze_file(unrelated_conftest.clone(), unrelated_content);
let app_content = r#"
import pytest
@pytest.fixture
def db():
return "app_db"
"#;
let app_conftest = PathBuf::from("/tmp/project/app/conftest.py");
db.analyze_file(app_conftest.clone(), app_content);
let tests_content = r#"
import pytest
@pytest.fixture
def db():
return "tests_db"
"#;
let tests_conftest = PathBuf::from("/tmp/project/app/tests/conftest.py");
db.analyze_file(tests_conftest.clone(), tests_content);
let test_content = r#"
def test_database(db):
assert db is not None
"#;
let test_path = PathBuf::from("/tmp/project/app/tests/test_foo.py");
db.analyze_file(test_path.clone(), test_content);
for iteration in 0..10 {
let result = db.find_fixture_definition(&test_path, 1, 18);
assert!(
result.is_some(),
"Iteration {}: Should find a fixture definition",
iteration
);
let def = result.unwrap();
assert_eq!(
def.name, "db",
"Iteration {}: Should find 'db' fixture",
iteration
);
assert_eq!(
def.file_path, tests_conftest,
"Iteration {}: Should consistently resolve to closest conftest.py at {:?}, but got {:?}",
iteration,
tests_conftest,
def.file_path
);
}
}
#[test]
#[timeout(30000)]
fn test_fixture_resolution_prefers_parent_over_unrelated() {
let db = FixtureDatabase::new();
let unrelated_content = r#"
import pytest
@pytest.fixture
def custom_fixture():
return "unrelated"
"#;
let unrelated_conftest = PathBuf::from("/tmp/other_project/conftest.py");
db.analyze_file(unrelated_conftest.clone(), unrelated_content);
let third_party_content = r#"
import pytest
@pytest.fixture
def custom_fixture():
return "third_party"
"#;
let third_party_path =
PathBuf::from("/tmp/.venv/lib/python3.11/site-packages/pytest_custom/plugin.py");
db.analyze_file(third_party_path.clone(), third_party_content);
let test_content = r#"
def test_custom(custom_fixture):
assert custom_fixture is not None
"#;
let test_path = PathBuf::from("/tmp/my_project/test_foo.py");
db.analyze_file(test_path.clone(), test_content);
let result = db.find_fixture_definition(&test_path, 1, 16);
assert!(result.is_some());
let def = result.unwrap();
assert_eq!(
def.file_path, third_party_path,
"Should prefer third-party fixture from site-packages over unrelated conftest.py"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_resolution_hierarchy_over_third_party() {
let db = FixtureDatabase::new();
let third_party_content = r#"
import pytest
@pytest.fixture
def mocker():
return "third_party_mocker"
"#;
let third_party_path =
PathBuf::from("/tmp/project/.venv/lib/python3.11/site-packages/pytest_mock/plugin.py");
db.analyze_file(third_party_path.clone(), third_party_content);
let local_content = r#"
import pytest
@pytest.fixture
def mocker():
return "local_mocker"
"#;
let local_conftest = PathBuf::from("/tmp/project/conftest.py");
db.analyze_file(local_conftest.clone(), local_content);
let test_content = r#"
def test_mocking(mocker):
assert mocker is not None
"#;
let test_path = PathBuf::from("/tmp/project/test_foo.py");
db.analyze_file(test_path.clone(), test_content);
let result = db.find_fixture_definition(&test_path, 1, 17);
assert!(result.is_some());
let def = result.unwrap();
assert_eq!(
def.file_path, local_conftest,
"Should prefer local conftest.py fixture over third-party fixture"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_resolution_with_relative_paths() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def shared():
return "conftest"
"#;
let conftest_abs = PathBuf::from("/tmp/project/tests/conftest.py");
db.analyze_file(conftest_abs.clone(), conftest_content);
let test_content = r#"
def test_example(shared):
assert shared == "conftest"
"#;
let test_abs = PathBuf::from("/tmp/project/tests/test_foo.py");
db.analyze_file(test_abs.clone(), test_content);
let result = db.find_fixture_definition(&test_abs, 1, 17);
assert!(result.is_some(), "Should find fixture with absolute paths");
let def = result.unwrap();
assert_eq!(def.file_path, conftest_abs, "Should resolve to conftest.py");
}
#[test]
#[timeout(30000)]
fn test_fixture_resolution_deep_hierarchy() {
let db = FixtureDatabase::new();
let root_content = r#"
import pytest
@pytest.fixture
def db():
return "root"
"#;
let root_conftest = PathBuf::from("/tmp/project/conftest.py");
db.analyze_file(root_conftest.clone(), root_content);
let level1_content = r#"
import pytest
@pytest.fixture
def db():
return "level1"
"#;
let level1_conftest = PathBuf::from("/tmp/project/src/conftest.py");
db.analyze_file(level1_conftest.clone(), level1_content);
let level2_content = r#"
import pytest
@pytest.fixture
def db():
return "level2"
"#;
let level2_conftest = PathBuf::from("/tmp/project/src/app/conftest.py");
db.analyze_file(level2_conftest.clone(), level2_content);
let level3_content = r#"
import pytest
@pytest.fixture
def db():
return "level3"
"#;
let level3_conftest = PathBuf::from("/tmp/project/src/app/tests/conftest.py");
db.analyze_file(level3_conftest.clone(), level3_content);
let test_l3_content = r#"
def test_db(db):
assert db == "level3"
"#;
let test_l3 = PathBuf::from("/tmp/project/src/app/tests/test_foo.py");
db.analyze_file(test_l3.clone(), test_l3_content);
let result_l3 = db.find_fixture_definition(&test_l3, 1, 12);
assert!(result_l3.is_some());
assert_eq!(
result_l3.unwrap().file_path,
level3_conftest,
"Test at level 3 should use level 3 fixture"
);
let test_l2_content = r#"
def test_db(db):
assert db == "level2"
"#;
let test_l2 = PathBuf::from("/tmp/project/src/app/test_bar.py");
db.analyze_file(test_l2.clone(), test_l2_content);
let result_l2 = db.find_fixture_definition(&test_l2, 1, 12);
assert!(result_l2.is_some());
assert_eq!(
result_l2.unwrap().file_path,
level2_conftest,
"Test at level 2 should use level 2 fixture"
);
let test_l1_content = r#"
def test_db(db):
assert db == "level1"
"#;
let test_l1 = PathBuf::from("/tmp/project/src/test_baz.py");
db.analyze_file(test_l1.clone(), test_l1_content);
let result_l1 = db.find_fixture_definition(&test_l1, 1, 12);
assert!(result_l1.is_some());
assert_eq!(
result_l1.unwrap().file_path,
level1_conftest,
"Test at level 1 should use level 1 fixture"
);
let test_root_content = r#"
def test_db(db):
assert db == "root"
"#;
let test_root = PathBuf::from("/tmp/project/test_root.py");
db.analyze_file(test_root.clone(), test_root_content);
let result_root = db.find_fixture_definition(&test_root, 1, 12);
assert!(result_root.is_some());
assert_eq!(
result_root.unwrap().file_path,
root_conftest,
"Test at root should use root fixture"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_resolution_sibling_directories() {
let db = FixtureDatabase::new();
let root_content = r#"
import pytest
@pytest.fixture
def shared():
return "root"
"#;
let root_conftest = PathBuf::from("/tmp/project/conftest.py");
db.analyze_file(root_conftest.clone(), root_content);
let module_a_content = r#"
import pytest
@pytest.fixture
def module_specific():
return "module_a"
"#;
let module_a_conftest = PathBuf::from("/tmp/project/module_a/conftest.py");
db.analyze_file(module_a_conftest.clone(), module_a_content);
let module_b_content = r#"
import pytest
@pytest.fixture
def module_specific():
return "module_b"
"#;
let module_b_conftest = PathBuf::from("/tmp/project/module_b/conftest.py");
db.analyze_file(module_b_conftest.clone(), module_b_content);
let test_a_content = r#"
def test_a(module_specific, shared):
assert module_specific == "module_a"
assert shared == "root"
"#;
let test_a = PathBuf::from("/tmp/project/module_a/test_a.py");
db.analyze_file(test_a.clone(), test_a_content);
let result_a = db.find_fixture_definition(&test_a, 1, 11);
assert!(result_a.is_some());
assert_eq!(
result_a.unwrap().file_path,
module_a_conftest,
"Test in module_a should use module_a's fixture"
);
let test_b_content = r#"
def test_b(module_specific, shared):
assert module_specific == "module_b"
assert shared == "root"
"#;
let test_b = PathBuf::from("/tmp/project/module_b/test_b.py");
db.analyze_file(test_b.clone(), test_b_content);
let result_b = db.find_fixture_definition(&test_b, 1, 11);
assert!(result_b.is_some());
assert_eq!(
result_b.unwrap().file_path,
module_b_conftest,
"Test in module_b should use module_b's fixture"
);
let result_a_shared = db.find_fixture_definition(&test_a, 1, 29);
assert!(result_a_shared.is_some());
assert_eq!(
result_a_shared.unwrap().file_path,
root_conftest,
"Test in module_a should access root's shared fixture"
);
let result_b_shared = db.find_fixture_definition(&test_b, 1, 29);
assert!(result_b_shared.is_some());
assert_eq!(
result_b_shared.unwrap().file_path,
root_conftest,
"Test in module_b should access root's shared fixture"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_resolution_multiple_unrelated_branches_is_deterministic() {
let db = FixtureDatabase::new();
let branch_a_content = r#"
import pytest
@pytest.fixture
def common_fixture():
return "branch_a"
"#;
let branch_a_conftest = PathBuf::from("/tmp/projects/project_a/conftest.py");
db.analyze_file(branch_a_conftest.clone(), branch_a_content);
let branch_b_content = r#"
import pytest
@pytest.fixture
def common_fixture():
return "branch_b"
"#;
let branch_b_conftest = PathBuf::from("/tmp/projects/project_b/conftest.py");
db.analyze_file(branch_b_conftest.clone(), branch_b_content);
let branch_c_content = r#"
import pytest
@pytest.fixture
def common_fixture():
return "branch_c"
"#;
let branch_c_conftest = PathBuf::from("/tmp/projects/project_c/conftest.py");
db.analyze_file(branch_c_conftest.clone(), branch_c_content);
let test_content = r#"
def test_something(common_fixture):
assert common_fixture is not None
"#;
let test_path = PathBuf::from("/tmp/unrelated/test_foo.py");
db.analyze_file(test_path.clone(), test_content);
let result = db.find_fixture_definition(&test_path, 1, 19);
assert!(
result.is_none(),
"Fixture should NOT be found - test file is not in any conftest hierarchy"
);
let test_in_a_content = r#"
def test_in_project_a(common_fixture):
pass
"#;
let test_in_a_path = PathBuf::from("/tmp/projects/project_a/test_example.py");
db.analyze_file(test_in_a_path.clone(), test_in_a_content);
let result_in_a = db.find_fixture_definition(&test_in_a_path, 1, 22);
assert!(
result_in_a.is_some(),
"Fixture should be found in project_a"
);
assert_eq!(
result_in_a.unwrap().file_path,
branch_a_conftest,
"Should resolve to project_a's conftest.py"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_resolution_conftest_at_various_depths() {
let db = FixtureDatabase::new();
let deep_content = r#"
import pytest
@pytest.fixture
def fixture_a():
return "deep"
@pytest.fixture
def fixture_b():
return "deep"
"#;
let deep_conftest = PathBuf::from("/tmp/project/src/module/tests/integration/conftest.py");
db.analyze_file(deep_conftest.clone(), deep_content);
let mid_content = r#"
import pytest
@pytest.fixture
def fixture_a():
return "mid"
"#;
let mid_conftest = PathBuf::from("/tmp/project/src/module/conftest.py");
db.analyze_file(mid_conftest.clone(), mid_content);
let root_content = r#"
import pytest
@pytest.fixture
def fixture_c():
return "root"
"#;
let root_conftest = PathBuf::from("/tmp/project/conftest.py");
db.analyze_file(root_conftest.clone(), root_content);
let test_content = r#"
def test_all(fixture_a, fixture_b, fixture_c):
assert fixture_a == "deep"
assert fixture_b == "deep"
assert fixture_c == "root"
"#;
let test_path = PathBuf::from("/tmp/project/src/module/tests/integration/test_foo.py");
db.analyze_file(test_path.clone(), test_content);
let result_a = db.find_fixture_definition(&test_path, 1, 13);
assert!(result_a.is_some());
assert_eq!(
result_a.unwrap().file_path,
deep_conftest,
"fixture_a should resolve to closest conftest (deep)"
);
let result_b = db.find_fixture_definition(&test_path, 1, 24);
assert!(result_b.is_some());
assert_eq!(
result_b.unwrap().file_path,
deep_conftest,
"fixture_b should resolve to deep conftest"
);
let result_c = db.find_fixture_definition(&test_path, 1, 35);
assert!(result_c.is_some());
assert_eq!(
result_c.unwrap().file_path,
root_conftest,
"fixture_c should resolve to root conftest"
);
let test_mid_content = r#"
def test_mid(fixture_a, fixture_c):
assert fixture_a == "mid"
assert fixture_c == "root"
"#;
let test_mid_path = PathBuf::from("/tmp/project/src/module/test_bar.py");
db.analyze_file(test_mid_path.clone(), test_mid_content);
let result_a_mid = db.find_fixture_definition(&test_mid_path, 1, 13);
assert!(result_a_mid.is_some());
assert_eq!(
result_a_mid.unwrap().file_path,
mid_conftest,
"fixture_a from mid-level test should resolve to mid conftest"
);
}
#[test]
#[timeout(30000)]
fn test_get_available_fixtures_same_file() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def fixture_a():
return "a"
@pytest.fixture
def fixture_b():
return "b"
def test_something():
pass
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let available = db.get_available_fixtures(&test_path);
assert_eq!(available.len(), 2, "Should find 2 fixtures in same file");
let names: Vec<_> = available.iter().map(|f| f.name.as_str()).collect();
assert!(names.contains(&"fixture_a"));
assert!(names.contains(&"fixture_b"));
}
#[test]
#[timeout(30000)]
fn test_get_available_fixtures_conftest_hierarchy() {
let db = FixtureDatabase::new();
let root_conftest = r#"
import pytest
@pytest.fixture
def root_fixture():
return "root"
"#;
let root_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(root_path.clone(), root_conftest);
let sub_conftest = r#"
import pytest
@pytest.fixture
def sub_fixture():
return "sub"
"#;
let sub_path = PathBuf::from("/tmp/test/subdir/conftest.py");
db.analyze_file(sub_path.clone(), sub_conftest);
let test_content = r#"
def test_something():
pass
"#;
let test_path = PathBuf::from("/tmp/test/subdir/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let available = db.get_available_fixtures(&test_path);
assert_eq!(
available.len(),
2,
"Should find fixtures from both conftest files"
);
let names: Vec<_> = available.iter().map(|f| f.name.as_str()).collect();
assert!(names.contains(&"root_fixture"));
assert!(names.contains(&"sub_fixture"));
}
#[test]
#[timeout(30000)]
fn test_get_available_fixtures_no_duplicates() {
let db = FixtureDatabase::new();
let root_conftest = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "root"
"#;
let root_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(root_path.clone(), root_conftest);
let sub_conftest = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "sub"
"#;
let sub_path = PathBuf::from("/tmp/test/subdir/conftest.py");
db.analyze_file(sub_path.clone(), sub_conftest);
let test_content = r#"
def test_something():
pass
"#;
let test_path = PathBuf::from("/tmp/test/subdir/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let available = db.get_available_fixtures(&test_path);
let shared_count = available
.iter()
.filter(|f| f.name == "shared_fixture")
.count();
assert_eq!(shared_count, 1, "Should only include shared_fixture once");
let shared_fixture = available
.iter()
.find(|f| f.name == "shared_fixture")
.unwrap();
assert_eq!(shared_fixture.file_path, sub_path);
}
#[test]
#[timeout(30000)]
fn test_is_inside_function_in_test() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
def test_example(fixture_a, fixture_b):
result = fixture_a + fixture_b
assert result == "ab"
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let result = db.is_inside_function(&test_path, 3, 10);
assert!(result.is_some());
let (func_name, is_fixture, params) = result.unwrap();
assert_eq!(func_name, "test_example");
assert!(!is_fixture);
assert_eq!(params, vec!["fixture_a", "fixture_b"]);
let result = db.is_inside_function(&test_path, 4, 10);
assert!(result.is_some());
let (func_name, is_fixture, _) = result.unwrap();
assert_eq!(func_name, "test_example");
assert!(!is_fixture);
}
#[test]
#[timeout(30000)]
fn test_is_inside_function_in_fixture() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def my_fixture(other_fixture):
return other_fixture + "_modified"
"#;
let test_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(test_path.clone(), test_content);
let result = db.is_inside_function(&test_path, 4, 10);
assert!(result.is_some());
let (func_name, is_fixture, params) = result.unwrap();
assert_eq!(func_name, "my_fixture");
assert!(is_fixture);
assert_eq!(params, vec!["other_fixture"]);
let result = db.is_inside_function(&test_path, 5, 10);
assert!(result.is_some());
let (func_name, is_fixture, _) = result.unwrap();
assert_eq!(func_name, "my_fixture");
assert!(is_fixture);
}
#[test]
#[timeout(30000)]
fn test_is_inside_function_outside() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return "value"
def test_example(my_fixture):
assert my_fixture == "value"
# This is a comment outside any function
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let result = db.is_inside_function(&test_path, 0, 0);
assert!(
result.is_none(),
"Should not be inside a function on import line"
);
let result = db.is_inside_function(&test_path, 9, 0);
assert!(
result.is_none(),
"Should not be inside a function on comment line"
);
}
#[test]
#[timeout(30000)]
fn test_is_inside_function_non_test() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
def helper_function():
return "helper"
def test_example():
result = helper_function()
assert result == "helper"
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let result = db.is_inside_function(&test_path, 3, 10);
assert!(
result.is_none(),
"Should not return non-test, non-fixture functions"
);
let result = db.is_inside_function(&test_path, 6, 10);
assert!(result.is_some(), "Should return test functions");
let (func_name, is_fixture, _) = result.unwrap();
assert_eq!(func_name, "test_example");
assert!(!is_fixture);
}
#[test]
#[timeout(30000)]
fn test_is_inside_async_function() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
async def async_fixture():
return "async_value"
async def test_async_example(async_fixture):
assert async_fixture == "async_value"
"#;
let test_path = PathBuf::from("/tmp/test/test_async.py");
db.analyze_file(test_path.clone(), test_content);
let result = db.is_inside_function(&test_path, 4, 10);
assert!(result.is_some());
let (func_name, is_fixture, _) = result.unwrap();
assert_eq!(func_name, "async_fixture");
assert!(is_fixture);
let result = db.is_inside_function(&test_path, 7, 10);
assert!(result.is_some());
let (func_name, is_fixture, params) = result.unwrap();
assert_eq!(func_name, "test_async_example");
assert!(!is_fixture);
assert_eq!(params, vec!["async_fixture"]);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_simple_return_type() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def string_fixture() -> str:
return "hello"
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
let fixtures = db.definitions.get("string_fixture").unwrap();
assert_eq!(fixtures.len(), 1);
assert_eq!(fixtures[0].return_type, Some("str".to_string()));
}
#[test]
#[timeout(30000)]
fn test_fixture_with_generator_return_type() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
from typing import Generator
@pytest.fixture
def generator_fixture() -> Generator[str, None, None]:
yield "value"
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
let fixtures = db.definitions.get("generator_fixture").unwrap();
assert_eq!(fixtures.len(), 1);
assert_eq!(fixtures[0].return_type, Some("str".to_string()));
}
#[test]
#[timeout(30000)]
fn test_fixture_with_iterator_return_type() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
from typing import Iterator
@pytest.fixture
def iterator_fixture() -> Iterator[int]:
yield 42
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
let fixtures = db.definitions.get("iterator_fixture").unwrap();
assert_eq!(fixtures.len(), 1);
assert_eq!(fixtures[0].return_type, Some("int".to_string()));
}
#[test]
#[timeout(30000)]
fn test_fixture_without_return_type() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def no_type_fixture():
return 123
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
let fixtures = db.definitions.get("no_type_fixture").unwrap();
assert_eq!(fixtures.len(), 1);
assert_eq!(fixtures[0].return_type, None);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_union_return_type() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def union_fixture() -> str | int:
return "string"
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
let fixtures = db.definitions.get("union_fixture").unwrap();
assert_eq!(fixtures.len(), 1);
assert_eq!(fixtures[0].return_type, Some("str | int".to_string()));
}
#[test]
#[timeout(30000)]
fn test_parametrized_fixture_detection() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(params=[1, 2, 3])
def number_fixture(request):
return request.param
@pytest.fixture(params=["a", "b"])
def letter_fixture(request):
return request.param
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("number_fixture"));
assert!(db.definitions.contains_key("letter_fixture"));
let number_defs = db.definitions.get("number_fixture").unwrap();
assert_eq!(number_defs.len(), 1);
assert_eq!(number_defs[0].name, "number_fixture");
}
#[test]
#[timeout(30000)]
fn test_parametrized_fixture_usage() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture(params=[1, 2, 3])
def number_fixture(request):
return request.param
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_with_parametrized(number_fixture):
assert number_fixture > 0
"#;
let test_path = PathBuf::from("/tmp/test/test_param.py");
db.analyze_file(test_path.clone(), test_content);
let definition = db.find_fixture_definition(&test_path, 1, 27);
assert!(
definition.is_some(),
"Should find parametrized fixture definition"
);
let def = definition.unwrap();
assert_eq!(def.name, "number_fixture");
assert_eq!(def.file_path, conftest_path);
}
#[test]
#[timeout(30000)]
fn test_parametrized_fixture_with_ids() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(params=[1, 2, 3], ids=["one", "two", "three"])
def number_with_ids(request):
return request.param
@pytest.fixture(params=["x", "y"], ids=lambda x: f"letter_{x}")
def letter_with_ids(request):
return request.param
@pytest.fixture(
params=[{"a": 1}, {"b": 2}],
ids=["dict_a", "dict_b"],
scope="module"
)
def complex_params(request):
return request.param
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(
db.definitions.contains_key("number_with_ids"),
"Should detect fixture with list ids"
);
assert!(
db.definitions.contains_key("letter_with_ids"),
"Should detect fixture with lambda ids"
);
assert!(
db.definitions.contains_key("complex_params"),
"Should detect multi-line parametrized fixture"
);
}
#[test]
#[timeout(30000)]
fn test_factory_fixture_pattern() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def user_factory():
def _create_user(name, email):
return {"name": name, "email": email}
return _create_user
@pytest.fixture
def database_factory(db_connection):
def _create_database(name):
return db_connection.create(name)
return _create_database
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("user_factory"));
assert!(db.definitions.contains_key("database_factory"));
let user_factory = db.definitions.get("user_factory").unwrap();
assert_eq!(user_factory.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_autouse_fixture_detection() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(autouse=True)
def auto_fixture():
print("Running automatically")
yield
print("Cleanup")
@pytest.fixture(scope="function", autouse=True)
def another_auto():
return 42
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("auto_fixture"));
assert!(db.definitions.contains_key("another_auto"));
let auto_fixture = &db.definitions.get("auto_fixture").unwrap()[0];
assert!(
auto_fixture.autouse,
"auto_fixture should have autouse=true"
);
let another_auto = &db.definitions.get("another_auto").unwrap()[0];
assert!(
another_auto.autouse,
"another_auto should have autouse=true"
);
}
#[test]
#[timeout(30000)]
fn test_autouse_fixture_not_flagged_as_undeclared() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture(autouse=True)
def auto_setup():
return "setup"
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_something():
# auto_setup runs automatically, not declared in parameters
# Using it in body should NOT be flagged since it's autouse
result = auto_setup
assert result == "setup"
"#;
let test_path = PathBuf::from("/tmp/test/test_autouse.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert!(
undeclared.iter().any(|u| u.name == "auto_setup"),
"Current implementation flags autouse fixtures - this is a known limitation"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_scope_session() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(scope="session")
def session_fixture():
return "session data"
@pytest.fixture(scope="module")
def module_fixture():
return "module data"
@pytest.fixture(scope="class")
def class_fixture():
return "class data"
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("session_fixture"));
assert!(db.definitions.contains_key("module_fixture"));
assert!(db.definitions.contains_key("class_fixture"));
}
#[test]
#[timeout(30000)]
fn test_pytest_asyncio_fixture() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
import pytest_asyncio
@pytest_asyncio.fixture
async def async_fixture():
return "async data"
@pytest.fixture
async def regular_async_fixture():
return "also async"
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(
db.definitions.contains_key("async_fixture"),
"pytest_asyncio.fixture should be detected"
);
assert!(db.definitions.contains_key("regular_async_fixture"));
}
#[test]
#[timeout(30000)]
fn test_fixture_name_aliasing() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(name="custom_name")
def internal_fixture_name():
return "aliased"
@pytest.fixture(name="db")
def database_connection():
return "connection"
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("custom_name"));
assert!(db.definitions.contains_key("db"));
assert!(!db.definitions.contains_key("internal_fixture_name"));
assert!(!db.definitions.contains_key("database_connection"));
}
#[test]
#[timeout(30000)]
fn test_renamed_fixture_usage_detection() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(name="new")
def old() -> int:
return 1
def test_example(new: int):
assert new == 1
"#;
let file_path = PathBuf::from("/tmp/test/test_renamed.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("new"));
assert!(!db.definitions.contains_key("old"));
let usages = db.usages.get(&file_path).unwrap();
assert!(usages.iter().any(|u| u.name == "new"));
let new_defs = db.definitions.get("new").unwrap();
assert_eq!(new_defs.len(), 1);
assert_eq!(new_defs[0].file_path, file_path);
}
#[test]
#[timeout(30000)]
fn test_class_based_test_methods_use_fixtures() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture() -> int:
return 1
class TestInClass:
def test_in_class(self, my_fixture: int):
assert my_fixture == 1
def test_another(self, my_fixture: int):
assert my_fixture == 1
"#;
let file_path = PathBuf::from("/tmp/test/test_class.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("my_fixture"));
let usages = db.usages.get(&file_path).unwrap();
let my_fixture_usages: Vec<_> = usages.iter().filter(|u| u.name == "my_fixture").collect();
assert_eq!(
my_fixture_usages.len(),
2,
"Should have 2 usages of my_fixture from test methods in class"
);
}
#[test]
#[timeout(30000)]
fn test_nested_class_test_methods() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def outer_fixture():
return "outer"
class TestOuter:
def test_outer(self, outer_fixture):
pass
class TestNested:
def test_nested(self, outer_fixture):
pass
"#;
let file_path = PathBuf::from("/tmp/test/test_nested.py");
db.analyze_file(file_path.clone(), content);
let usages = db.usages.get(&file_path).unwrap();
let fixture_usages: Vec<_> = usages
.iter()
.filter(|u| u.name == "outer_fixture")
.collect();
assert_eq!(
fixture_usages.len(),
2,
"Should have 2 usages from both outer and nested test classes"
);
}
#[test]
#[timeout(30000)]
fn test_deeply_nested_classes() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "shared"
class TestLevel1:
def test_level1(self, shared_fixture):
pass
class TestLevel2:
def test_level2(self, shared_fixture):
pass
class TestLevel3:
def test_level3(self, shared_fixture):
pass
"#;
let file_path = PathBuf::from("/tmp/test/test_deep_nested.py");
db.analyze_file(file_path.clone(), content);
let usages = db.usages.get(&file_path).unwrap();
let fixture_usages: Vec<_> = usages
.iter()
.filter(|u| u.name == "shared_fixture")
.collect();
assert_eq!(
fixture_usages.len(),
3,
"Should have 3 usages from all nesting levels"
);
}
#[test]
#[timeout(30000)]
fn test_nested_class_with_usefixtures() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def setup_fixture():
return "setup"
@pytest.fixture
def nested_setup():
return "nested"
@pytest.mark.usefixtures("setup_fixture")
class TestOuter:
def test_outer(self):
pass
@pytest.mark.usefixtures("nested_setup")
class TestNested:
def test_nested(self):
pass
"#;
let file_path = PathBuf::from("/tmp/test/test_nested_usefixtures.py");
db.analyze_file(file_path.clone(), content);
let usages = db.usages.get(&file_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "setup_fixture"),
"setup_fixture from outer class usefixtures should be detected"
);
assert!(
usages.iter().any(|u| u.name == "nested_setup"),
"nested_setup from nested class usefixtures should be detected"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_in_nested_class() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
class TestOuter:
@pytest.fixture
def outer_class_fixture(self):
return "outer"
def test_uses_outer(self, outer_class_fixture):
pass
class TestNested:
@pytest.fixture
def nested_class_fixture(self):
return "nested"
def test_uses_nested(self, nested_class_fixture):
pass
def test_uses_both(self, outer_class_fixture, nested_class_fixture):
pass
"#;
let file_path = PathBuf::from("/tmp/test/test_fixture_in_nested.py");
db.analyze_file(file_path.clone(), content);
assert!(
db.definitions.contains_key("outer_class_fixture"),
"Fixture in outer class should be detected"
);
assert!(
db.definitions.contains_key("nested_class_fixture"),
"Fixture in nested class should be detected"
);
let usages = db.usages.get(&file_path).unwrap();
let outer_usages: Vec<_> = usages
.iter()
.filter(|u| u.name == "outer_class_fixture")
.collect();
assert_eq!(
outer_usages.len(),
2,
"outer_class_fixture should be used twice"
);
let nested_usages: Vec<_> = usages
.iter()
.filter(|u| u.name == "nested_class_fixture")
.collect();
assert_eq!(
nested_usages.len(),
2,
"nested_class_fixture should be used twice"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_defined_in_class() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
class TestWithFixture:
@pytest.fixture
def class_fixture(self):
return "class_value"
def test_uses_class_fixture(self, class_fixture):
assert class_fixture == "class_value"
"#;
let file_path = PathBuf::from("/tmp/test/test_class_fixture.py");
db.analyze_file(file_path.clone(), content);
assert!(
db.definitions.contains_key("class_fixture"),
"Class-defined fixture should be detected"
);
let usages = db.usages.get(&file_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "class_fixture"),
"Usage of class fixture should be detected"
);
}
#[test]
#[timeout(30000)]
fn test_request_fixture_definition_registered_after_venv_scan() {
use tempfile::tempdir;
let temp = tempdir().unwrap();
let venv = temp.path().join(".venv");
let site_packages = venv.join("lib").join("python3.11").join("site-packages");
let pytest_internal = site_packages.join("_pytest");
std::fs::create_dir_all(&pytest_internal).unwrap();
std::fs::write(
pytest_internal.join("fixtures.py"),
b"# pytest internal fixtures\n",
)
.unwrap();
let db = FixtureDatabase::new();
db.scan_workspace(temp.path());
let defs = db.definitions.get("request");
assert!(
defs.is_some(),
"'request' fixture definition must be registered after venv scan"
);
let def = &defs.unwrap()[0];
assert_eq!(def.name, "request");
assert_eq!(
def.return_type.as_deref(),
Some("FixtureRequest"),
"request fixture must have return_type = FixtureRequest"
);
assert!(
def.is_third_party,
"request fixture must be marked as third-party"
);
assert!(def.is_plugin, "request fixture must be marked as plugin");
}
#[test]
#[timeout(30000)]
fn test_request_fixture_return_type_import_spec() {
use tempfile::tempdir;
let temp = tempdir().unwrap();
let venv = temp.path().join(".venv");
let site_packages = venv.join("lib").join("python3.11").join("site-packages");
let pytest_internal = site_packages.join("_pytest");
std::fs::create_dir_all(&pytest_internal).unwrap();
std::fs::write(pytest_internal.join("fixtures.py"), b"").unwrap();
let db = FixtureDatabase::new();
db.scan_workspace(temp.path());
let defs = db
.definitions
.get("request")
.expect("request must be registered");
let def = &defs[0];
assert_eq!(def.return_type_imports.len(), 1);
let spec = &def.return_type_imports[0];
assert_eq!(spec.check_name, "FixtureRequest");
assert_eq!(spec.import_statement, "from pytest import FixtureRequest");
}
#[test]
#[timeout(30000)]
fn test_request_fixture_has_docstring() {
use tempfile::tempdir;
let temp = tempdir().unwrap();
let venv = temp.path().join(".venv");
let site_packages = venv.join("lib").join("python3.11").join("site-packages");
let pytest_internal = site_packages.join("_pytest");
std::fs::create_dir_all(&pytest_internal).unwrap();
std::fs::write(pytest_internal.join("fixtures.py"), b"").unwrap();
let db = FixtureDatabase::new();
db.scan_workspace(temp.path());
let defs = db
.definitions
.get("request")
.expect("request must be registered");
let def = &defs[0];
let doc = def.docstring.as_deref().unwrap_or("");
assert!(
doc.contains("FixtureRequest") || doc.contains("requesting test context"),
"request docstring should describe its purpose, got: {:?}",
doc
);
assert!(
doc.contains("param") || doc.contains("addfinalizer") || doc.contains("docs.pytest.org"),
"request docstring should mention key attributes or docs URL, got: {:?}",
doc
);
}
#[test]
#[timeout(30000)]
fn test_request_not_flagged_as_undeclared_in_test_function() {
let db = FixtureDatabase::new();
let content = r#"
def test_uses_request_in_body():
val = request.param
assert val is not None
"#;
let path = PathBuf::from("/tmp/test_req_undecl/test_req.py");
db.analyze_file(path.clone(), content);
let undeclared = db.get_undeclared_fixtures(&path);
assert!(
!undeclared.iter().any(|u| u.name == "request"),
"request must never be reported as undeclared"
);
}
#[test]
#[timeout(30000)]
fn test_request_not_flagged_as_undeclared_in_fixture() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture(request):
return request.param
"#;
let path = PathBuf::from("/tmp/test_req_fixt_undecl/conftest.py");
db.analyze_file(path.clone(), content);
let undeclared = db.get_undeclared_fixtures(&path);
assert!(
!undeclared.iter().any(|u| u.name == "request"),
"request in a fixture parameter must not be flagged as undeclared"
);
}
#[test]
#[timeout(30000)]
fn test_request_recorded_as_usage_in_test_function() {
let db = FixtureDatabase::new();
let content = r#"
def test_uses_request(request):
assert request.node is not None
"#;
let path = PathBuf::from("/tmp/test_req_usage_test/test_req.py");
db.analyze_file(path.clone(), content);
let usages = db.usages.get(&path).expect("usages should be tracked");
let request_usage = usages.iter().find(|u| u.name == "request");
assert!(
request_usage.is_some(),
"request parameter in a test function must be tracked as a usage"
);
assert!(
request_usage.unwrap().is_parameter,
"request in a test function parameter must have is_parameter = true"
);
}
#[test]
#[timeout(30000)]
fn test_request_recorded_as_usage_in_fixture_function() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def parametrized_fixture(request):
return request.param * 2
"#;
let path = PathBuf::from("/tmp/test_req_usage_fix/conftest.py");
db.analyze_file(path.clone(), content);
let usages = db.usages.get(&path).expect("usages should be tracked");
let request_usage = usages.iter().find(|u| u.name == "request");
assert!(
request_usage.is_some(),
"request parameter in a fixture function must be tracked as a usage"
);
assert!(
request_usage.unwrap().is_parameter,
"request in a fixture function parameter must have is_parameter = true"
);
}
#[test]
#[timeout(30000)]
fn test_request_not_added_as_fixture_dependency() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture(request, tmp_path):
return request.param
"#;
let path = PathBuf::from("/tmp/test_req_dep/conftest.py");
db.analyze_file(path.clone(), content);
let defs = db
.definitions
.get("my_fixture")
.expect("fixture must be detected");
let def = &defs[0];
assert!(
!def.dependencies.contains(&"request".to_string()),
"request must not appear in fixture dependencies, got: {:?}",
def.dependencies
);
assert!(
def.dependencies.contains(&"tmp_path".to_string()),
"tmp_path should be listed as a dependency"
);
}
#[test]
#[timeout(30000)]
fn test_request_completion_available() {
use tempfile::tempdir;
let temp = tempdir().unwrap();
let venv = temp.path().join(".venv");
let site_packages = venv.join("lib").join("python3.11").join("site-packages");
let pytest_internal = site_packages.join("_pytest");
std::fs::create_dir_all(&pytest_internal).unwrap();
std::fs::write(pytest_internal.join("fixtures.py"), b"").unwrap();
let test_dir = temp.path().join("tests");
std::fs::create_dir_all(&test_dir).unwrap();
let test_path = test_dir.join("test_req.py");
std::fs::write(&test_path, b"def test_something(request): pass").unwrap();
let db = FixtureDatabase::new();
db.scan_workspace(temp.path());
let available = db.get_available_fixtures(&test_path);
let request_def = available.iter().find(|f| f.name == "request");
assert!(
request_def.is_some(),
"request must appear in available fixtures after venv scan"
);
assert_eq!(
request_def.unwrap().return_type.as_deref(),
Some("FixtureRequest")
);
}
#[test]
#[timeout(30000)]
fn test_request_not_in_cycle_detection() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def a(request):
return request.param
@pytest.fixture
def b(a):
return a + 1
"#;
let path = PathBuf::from("/tmp/test_req_cycle/conftest.py");
db.analyze_file(path.clone(), content);
let cycles = db.detect_fixture_cycles();
assert!(
cycles.is_empty(),
"There should be no cycles; request must not cause false cycle detection"
);
}
#[test]
#[timeout(30000)]
fn test_pytest_django_builtin_fixtures() {
let db = FixtureDatabase::new();
let django_plugin_content = r#"
import pytest
@pytest.fixture
def db():
"""Provide django database access"""
return "db_connection"
@pytest.fixture
def client():
"""Provide django test client"""
return "test_client"
@pytest.fixture
def admin_client():
"""Provide django admin client"""
return "admin_client"
"#;
let plugin_path =
PathBuf::from("/tmp/.venv/lib/python3.11/site-packages/pytest_django/fixtures.py");
db.analyze_file(plugin_path.clone(), django_plugin_content);
let test_content = r#"
def test_with_django_fixtures(db, client, admin_client):
assert db is not None
assert client is not None
"#;
let test_path = PathBuf::from("/tmp/test/test_django.py");
db.analyze_file(test_path.clone(), test_content);
assert!(db.definitions.contains_key("db"));
assert!(db.definitions.contains_key("client"));
assert!(db.definitions.contains_key("admin_client"));
assert!(
db.usages.contains_key(&test_path),
"Test file should have fixture usages"
);
let usages = db.usages.get(&test_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "db"),
"Should detect 'db' fixture usage"
);
assert!(
usages.iter().any(|u| u.name == "client"),
"Should detect 'client' fixture usage"
);
let db_def = db.find_fixture_definition(&test_path, 1, 31);
assert!(db_def.is_some(), "Should find third-party fixture 'db'");
assert_eq!(db_def.unwrap().name, "db");
}
#[test]
#[timeout(30000)]
fn test_pytest_mock_advanced_patterns() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
from unittest.mock import Mock
@pytest.fixture
def mock_service():
return Mock()
@pytest.fixture
def patched_function(mocker):
return mocker.patch('module.function')
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
assert!(db.definitions.contains_key("mock_service"));
assert!(db.definitions.contains_key("patched_function"));
let patched = db.definitions.get("patched_function").unwrap();
assert_eq!(patched.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_mixed_sync_async_fixture_dependencies() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def sync_fixture():
return "sync"
@pytest.fixture
async def async_fixture(sync_fixture):
return f"async_{sync_fixture}"
@pytest.fixture
async def another_async(async_fixture):
return f"another_{await async_fixture}"
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("sync_fixture"));
assert!(db.definitions.contains_key("async_fixture"));
assert!(db.definitions.contains_key("another_async"));
let async_usages = db.usages.get(&file_path).unwrap();
assert!(async_usages.iter().any(|u| u.name == "sync_fixture"));
}
#[test]
#[timeout(30000)]
fn test_yield_fixture_with_exception_handling() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def resource_with_cleanup():
resource = acquire_resource()
try:
yield resource
except Exception as e:
handle_error(e)
finally:
cleanup_resource(resource)
@pytest.fixture
def complex_fixture():
setup()
try:
yield "value"
finally:
teardown()
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("resource_with_cleanup"));
assert!(db.definitions.contains_key("complex_fixture"));
}
#[test]
#[timeout(30000)]
fn test_yield_fixture_basic() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def simple_yield_fixture():
"""A simple yield fixture with setup and teardown."""
# Setup
connection = create_connection()
yield connection
# Teardown
connection.close()
@pytest.fixture
def yield_with_value():
yield 42
@pytest.fixture
def yield_none():
yield
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(
db.definitions.contains_key("simple_yield_fixture"),
"Simple yield fixture should be detected"
);
assert!(
db.definitions.contains_key("yield_with_value"),
"Yield with value should be detected"
);
assert!(
db.definitions.contains_key("yield_none"),
"Yield None should be detected"
);
let simple = db.definitions.get("simple_yield_fixture").unwrap();
assert!(
simple[0].docstring.is_some(),
"Docstring should be extracted from yield fixture"
);
}
#[test]
#[timeout(30000)]
fn test_yield_fixture_usage_in_test() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def db_session():
session = create_session()
yield session
session.rollback()
session.close()
"#;
let test_content = r#"
def test_with_db(db_session):
db_session.query("SELECT 1")
"#;
let conftest_path = PathBuf::from("/tmp/test_yield/conftest.py");
let test_path = PathBuf::from("/tmp/test_yield/test_db.py");
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let definition = db.find_fixture_definition(&test_path, 1, 18);
assert!(definition.is_some(), "Should find yield fixture definition");
let def = definition.unwrap();
assert_eq!(def.name, "db_session");
assert_eq!(def.file_path, conftest_path);
}
#[test]
#[timeout(30000)]
fn test_yield_fixture_with_context_manager() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
from contextlib import contextmanager
@pytest.fixture
def managed_resource():
with open("file.txt") as f:
yield f
@pytest.fixture
def nested_context():
with lock:
with connection:
yield connection
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(
db.definitions.contains_key("managed_resource"),
"Yield fixture with context manager should be detected"
);
assert!(
db.definitions.contains_key("nested_context"),
"Yield fixture with nested context should be detected"
);
}
#[test]
#[timeout(30000)]
fn test_async_yield_fixture() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
async def async_db():
db = await create_async_db()
yield db
await db.close()
@pytest.fixture
async def async_client():
async with httpx.AsyncClient() as client:
yield client
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(
db.definitions.contains_key("async_db"),
"Async yield fixture should be detected"
);
assert!(
db.definitions.contains_key("async_client"),
"Async yield fixture with context manager should be detected"
);
}
#[test]
#[timeout(30000)]
fn test_indirect_parametrization() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def user_data(request):
return request.param
@pytest.mark.parametrize("user_data", [
{"name": "Alice"},
{"name": "Bob"}
], indirect=True)
def test_user(user_data):
assert user_data["name"] in ["Alice", "Bob"]
"#;
let test_path = PathBuf::from("/tmp/test/test_indirect.py");
db.analyze_file(test_path.clone(), test_content);
assert!(db.definitions.contains_key("user_data"));
let usages = db.usages.get(&test_path).unwrap();
assert!(usages.iter().any(|u| u.name == "user_data"));
}
#[test]
#[timeout(30000)]
fn test_undeclared_fixture_in_walrus_operator() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return [1, 2, 3, 4, 5]
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path, conftest_content);
let test_content = r#"
def test_walrus():
# Using walrus operator with fixture name
if (data := my_fixture):
assert len(data) > 0
"#;
let test_path = PathBuf::from("/tmp/test/test_walrus.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
if undeclared.is_empty() {
println!("LIMITATION: Walrus operator assignments not detected as local variables");
} else {
assert!(undeclared.iter().any(|u| u.name == "my_fixture"));
}
}
#[test]
#[timeout(30000)]
fn test_undeclared_fixture_in_list_comprehension() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def items():
return [1, 2, 3]
@pytest.fixture
def multiplier():
return 2
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path, conftest_content);
let test_content = r#"
def test_comprehension():
# Using fixture in list comprehension iterable - should be flagged
result = [x * 2 for x in items]
assert len(result) == 3
# Using fixture in comprehension expression - should be flagged
result2 = [multiplier * x for x in [1, 2, 3]]
assert result2 == [2, 4, 6]
"#;
let test_path = PathBuf::from("/tmp/test/test_comprehension.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
println!(
"Undeclared fixtures detected: {:?}",
undeclared.iter().map(|u| &u.name).collect::<Vec<_>>()
);
if undeclared.iter().any(|u| u.name == "items") {
} else {
println!("LIMITATION: List comprehension variables not fully analyzed");
}
}
#[test]
#[timeout(30000)]
fn test_undeclared_fixture_in_dict_comprehension() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def data_dict():
return {"a": 1, "b": 2}
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path, conftest_content);
let test_content = r#"
def test_dict_comp():
# Using fixture in dict comprehension
result = {k: v * 2 for k, v in data_dict.items()}
assert result["a"] == 2
"#;
let test_path = PathBuf::from("/tmp/test/test_dict_comp.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
if undeclared.iter().any(|u| u.name == "data_dict") {
} else {
println!("LIMITATION: Dict comprehension fixture detection not implemented");
}
}
#[test]
#[timeout(30000)]
fn test_undeclared_fixture_in_generator_expression() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def numbers():
return [1, 2, 3, 4, 5]
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path, conftest_content);
let test_content = r#"
def test_generator():
# Using fixture in generator expression
gen = (x * 2 for x in numbers)
result = list(gen)
assert len(result) == 5
"#;
let test_path = PathBuf::from("/tmp/test/test_generator.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
if undeclared.iter().any(|u| u.name == "numbers") {
} else {
println!("LIMITATION: Generator expression fixture detection not implemented");
}
}
#[test]
#[timeout(30000)]
fn test_undeclared_fixture_in_f_string() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def user_name():
return "Alice"
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path, conftest_content);
let test_content = r#"
def test_f_string():
# Using fixture in f-string interpolation
message = f"Hello {user_name}"
assert "Alice" in message
"#;
let test_path = PathBuf::from("/tmp/test/test_f_string.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
if undeclared.iter().any(|u| u.name == "user_name") {
} else {
println!("LIMITATION: F-string interpolation not analyzed for fixture references");
}
}
#[test]
#[timeout(30000)]
fn test_undeclared_fixture_in_lambda() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def multiplier():
return 3
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path, conftest_content);
let test_content = r#"
def test_lambda():
# Using fixture in lambda body
func = lambda x: x * multiplier
result = func(5)
assert result == 15
"#;
let test_path = PathBuf::from("/tmp/test/test_lambda.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
if undeclared.iter().any(|u| u.name == "multiplier") {
} else {
println!("LIMITATION: Lambda expressions not analyzed for fixture references");
}
}
#[test]
#[timeout(30000)]
fn test_undeclared_fixture_in_nested_function() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def config():
return {"key": "value"}
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path, conftest_content);
let test_content = r#"
def test_nested():
def inner_function():
# Using fixture from outer scope
return config["key"]
result = inner_function()
assert result == "value"
"#;
let test_path = PathBuf::from("/tmp/test/test_nested.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
if undeclared.iter().any(|u| u.name == "config") {
} else {
println!("LIMITATION: Nested functions not analyzed for fixture references");
}
}
#[test]
#[timeout(30000)]
fn test_undeclared_fixture_in_decorator_argument() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def timeout_value():
return 30
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path, conftest_content);
let test_content = r#"
import pytest
def timeout_decorator(seconds):
def decorator(func):
return func
return decorator
@timeout_decorator(timeout_value)
def test_with_timeout():
assert True
"#;
let test_path = PathBuf::from("/tmp/test/test_decorator.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
if undeclared.iter().any(|u| u.name == "timeout_value") {
} else {
println!("LIMITATION: Decorator arguments not analyzed for fixture references");
}
}
#[test]
#[timeout(30000)]
fn test_local_variable_shadowing_fixture() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def data():
return "fixture_data"
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path, conftest_content);
let test_content = r#"
def test_shadowing():
# Local variable shadows fixture name
data = "local_data"
assert data == "local_data"
# This should NOT be flagged as undeclared
result = data.upper()
assert result == "LOCAL_DATA"
"#;
let test_path = PathBuf::from("/tmp/test/test_shadow.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert!(
!undeclared.iter().any(|u| u.name == "data"),
"Local variable should shadow fixture name - should not be flagged"
);
}
#[test]
#[timeout(30000)]
fn test_comprehension_variable_shadowing_fixture() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def x():
return 100
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path, conftest_content);
let test_content = r#"
def test_comp_shadow():
# Comprehension variable 'x' shadows fixture 'x'
result = [x * 2 for x in [1, 2, 3]]
assert result == [2, 4, 6]
"#;
let test_path = PathBuf::from("/tmp/test/test_comp_shadow.py");
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
if undeclared.iter().any(|u| u.name == "x") {
println!("LIMITATION: Comprehension variables not tracked - false positive for 'x'");
} else {
}
}
#[test]
#[timeout(30000)]
fn test_decorator_with_multiple_arguments() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(scope="session", autouse=True, name="custom")
def complex_fixture():
return 42
@pytest.fixture(scope="module", params=[1, 2, 3])
def parametrized_scoped():
return "data"
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("custom")); assert!(!db.definitions.contains_key("complex_fixture")); assert!(db.definitions.contains_key("parametrized_scoped")); }
#[test]
#[timeout(30000)]
fn test_parameter_with_type_hints() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
from typing import List, Dict
@pytest.fixture
def typed_fixture(param: str, count: int) -> Dict[str, int]:
return {param: count}
@pytest.fixture
def complex_types(data: List[str]) -> List[Dict[str, int]]:
return [{"item": len(d)} for d in data]
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("typed_fixture"));
assert!(db.definitions.contains_key("complex_types"));
let typed_usages = db.usages.get(&file_path).unwrap();
assert!(typed_usages.iter().any(|u| u.name == "param"));
assert!(typed_usages.iter().any(|u| u.name == "count"));
}
#[test]
#[timeout(30000)]
fn test_default_parameter_values() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def fixture_with_defaults(value="default", count=0):
return value * count
@pytest.fixture
def optional_param(data=None):
return data or []
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("fixture_with_defaults"));
assert!(db.definitions.contains_key("optional_param"));
}
#[test]
#[timeout(30000)]
fn test_variadic_parameters() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def fixture_with_args(*args):
return args
@pytest.fixture
def fixture_with_kwargs(**kwargs):
return kwargs
@pytest.fixture
def fixture_with_both(base, *args, **kwargs):
return (base, args, kwargs)
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("fixture_with_args"));
assert!(db.definitions.contains_key("fixture_with_kwargs"));
assert!(db.definitions.contains_key("fixture_with_both"));
let usages = db.usages.get(&file_path).unwrap();
assert!(usages.iter().any(|u| u.name == "base"));
assert!(!usages.iter().any(|u| u.name == "args"));
assert!(!usages.iter().any(|u| u.name == "kwargs"));
}
#[test]
#[timeout(30000)]
fn test_variadic_with_fixture_dependencies() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def base_fixture():
return "base"
@pytest.fixture
def config_fixture():
return {"key": "value"}
@pytest.fixture
def combined_fixture(base_fixture, config_fixture, *args, **kwargs):
"""Fixture that depends on other fixtures and also accepts variadic args."""
return {
"base": base_fixture,
"config": config_fixture,
"extra_args": args,
"extra_kwargs": kwargs,
}
"#;
let conftest_path = PathBuf::from("/tmp/test_variadic/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
assert!(db.definitions.contains_key("base_fixture"));
assert!(db.definitions.contains_key("config_fixture"));
assert!(db.definitions.contains_key("combined_fixture"));
let usages = db.usages.get(&conftest_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "base_fixture"),
"base_fixture should be tracked as dependency"
);
assert!(
usages.iter().any(|u| u.name == "config_fixture"),
"config_fixture should be tracked as dependency"
);
}
#[test]
#[timeout(30000)]
fn test_variadic_in_test_function() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
def test_with_variadic(my_fixture, *args, **kwargs):
# Note: This is unusual but valid Python
assert my_fixture == 42
"#;
let test_path = PathBuf::from("/tmp/test_variadic/test_func.py");
db.analyze_file(test_path.clone(), test_content);
assert!(db.definitions.contains_key("my_fixture"));
let usages = db.usages.get(&test_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "my_fixture"),
"my_fixture should be tracked as usage in test"
);
assert!(
!usages.iter().any(|u| u.name == "args"),
"args should not be tracked as fixture"
);
assert!(
!usages.iter().any(|u| u.name == "kwargs"),
"kwargs should not be tracked as fixture"
);
}
#[test]
#[timeout(30000)]
fn test_keyword_only_with_variadic() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def dep_fixture():
return "dep"
@pytest.fixture
def complex_fixture(*args, kwonly_dep: str, **kwargs):
# kwonly_dep is a keyword-only parameter that could be a fixture
return kwonly_dep
"#;
let file_path = PathBuf::from("/tmp/test_variadic/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("dep_fixture"));
assert!(db.definitions.contains_key("complex_fixture"));
let usages = db.usages.get(&file_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "kwonly_dep"),
"Keyword-only parameter should be tracked as potential fixture dependency"
);
}
#[test]
#[timeout(30000)]
fn test_class_based_fixtures() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
class TestClass:
@pytest.fixture
def class_fixture(self):
return "class_value"
def test_method(self, class_fixture):
assert class_fixture == "class_value"
"#;
let file_path = PathBuf::from("/tmp/test/test_class.py");
db.analyze_file(file_path.clone(), content);
if db.definitions.contains_key("class_fixture") {
} else {
println!("LIMITATION: Class-based fixtures not detected");
}
}
#[test]
#[timeout(30000)]
fn test_classmethod_and_staticmethod_fixtures() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
class TestClass:
@classmethod
@pytest.fixture
def class_method_fixture(cls):
return "classmethod"
@staticmethod
@pytest.fixture
def static_method_fixture():
return "staticmethod"
"#;
let file_path = PathBuf::from("/tmp/test/test_methods.py");
db.analyze_file(file_path.clone(), content);
if db.definitions.contains_key("class_method_fixture") {
println!("Class method fixtures detected");
}
if db.definitions.contains_key("static_method_fixture") {
println!("Static method fixtures detected");
}
}
#[test]
#[timeout(30000)]
fn test_unicode_fixture_names() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def 測試_fixture():
"""Chinese/Japanese test fixture"""
return "test"
@pytest.fixture
def фикстура():
"""Russian fixture"""
return "fixture"
@pytest.fixture
def fixture_émoji():
"""French accent fixture"""
return "emoji"
@pytest.fixture
def données_utilisateur():
"""French: user data"""
return {"name": "Jean"}
@pytest.fixture
def δεδομένα_χρήστη():
"""Greek: user data"""
return {"name": "Γιώργος"}
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(
db.definitions.contains_key("測試_fixture"),
"Chinese/Japanese fixture should be detected"
);
assert!(
db.definitions.contains_key("фикстура"),
"Russian fixture should be detected"
);
assert!(
db.definitions.contains_key("fixture_émoji"),
"French accent fixture should be detected"
);
assert!(
db.definitions.contains_key("données_utilisateur"),
"French fixture should be detected"
);
assert!(
db.definitions.contains_key("δεδομένα_χρήστη"),
"Greek fixture should be detected"
);
let russian = db.definitions.get("фикстура").unwrap();
assert!(
russian[0].docstring.as_ref().unwrap().contains("Russian"),
"Russian docstring should be extracted"
);
}
#[test]
#[timeout(30000)]
fn test_unicode_fixture_usage_detection() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def données():
return 42
"#;
let test_content = r#"
def test_unicode_usage(données):
assert données == 42
"#;
let conftest_path = PathBuf::from("/tmp/test_unicode/conftest.py");
let test_path = PathBuf::from("/tmp/test_unicode/test_example.py");
db.analyze_file(conftest_path, conftest_content);
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "données"),
"Unicode fixture usage should be detected"
);
}
#[test]
#[timeout(30000)]
fn test_unicode_fixture_goto_definition() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def données():
return 42
"#;
let test_content = r#"
def test_unicode(données):
pass
"#;
let conftest_path = PathBuf::from("/tmp/test_unicode/conftest.py");
let test_path = PathBuf::from("/tmp/test_unicode/test_example.py");
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let definition = db.find_fixture_definition(&test_path, 1, 17);
assert!(
definition.is_some(),
"Definition should be found for Unicode fixture"
);
let def = definition.unwrap();
assert_eq!(def.name, "données");
assert_eq!(def.file_path, conftest_path);
}
#[test]
#[timeout(30000)]
fn test_fixture_names_with_underscores() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def _private_fixture():
return "private"
@pytest.fixture
def __dunder_fixture__():
return "dunder"
@pytest.fixture
def fixture__double():
return "double"
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("_private_fixture"));
assert!(db.definitions.contains_key("__dunder_fixture__"));
assert!(db.definitions.contains_key("fixture__double"));
}
#[test]
#[timeout(30000)]
fn test_very_long_fixture_name() {
let db = FixtureDatabase::new();
let long_name = "fixture_with_an_extremely_long_name_that_exceeds_typical_naming_conventions_and_tests_the_system_capacity_for_handling_lengthy_identifiers";
let content = format!(
r#"
import pytest
@pytest.fixture
def {}():
return 42
"#,
long_name
);
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), &content);
assert!(
db.definitions.contains_key(long_name),
"Should handle fixture names over 100 characters"
);
}
#[test]
#[timeout(30000)]
fn test_optional_and_union_type_hints() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
from typing import Optional, Union, List
@pytest.fixture
def optional_fixture(data: Optional[str]) -> Optional[int]:
return len(data) if data else None
@pytest.fixture
def union_fixture(value: Union[str, int, List[str]]) -> Union[str, int]:
return value
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("optional_fixture"));
assert!(db.definitions.contains_key("union_fixture"));
let optional_defs = db.definitions.get("optional_fixture").unwrap();
if let Some(ref return_type) = optional_defs[0].return_type {
assert!(return_type.contains("Optional") || return_type.contains("int"));
}
}
#[test]
#[timeout(30000)]
fn test_forward_reference_type_hints() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def forward_ref_fixture() -> "MyClass":
return MyClass()
class MyClass:
pass
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("forward_ref_fixture"));
let defs = db.definitions.get("forward_ref_fixture").unwrap();
if let Some(ref return_type) = defs[0].return_type {
assert!(return_type.contains("MyClass"));
}
}
#[test]
#[timeout(30000)]
fn test_generic_type_hints() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
from typing import List, Dict, Tuple, Generic, TypeVar
T = TypeVar('T')
@pytest.fixture
def list_fixture() -> List[str]:
return ["a", "b", "c"]
@pytest.fixture
def dict_fixture() -> Dict[str, List[int]]:
return {"key": [1, 2, 3]}
@pytest.fixture
def tuple_fixture() -> Tuple[str, int, bool]:
return ("text", 42, True)
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
assert!(db.definitions.contains_key("list_fixture"));
assert!(db.definitions.contains_key("dict_fixture"));
assert!(db.definitions.contains_key("tuple_fixture"));
}
#[test]
#[timeout(30000)]
fn test_five_level_override_chain() {
let db = FixtureDatabase::new();
let root_conftest = r#"
import pytest
@pytest.fixture
def deep_fixture():
return "root"
"#;
db.analyze_file(PathBuf::from("/tmp/project/conftest.py"), root_conftest);
let level2_conftest = r#"
import pytest
@pytest.fixture
def deep_fixture(deep_fixture):
return f"{deep_fixture}_level2"
"#;
db.analyze_file(
PathBuf::from("/tmp/project/level2/conftest.py"),
level2_conftest,
);
let level3_conftest = r#"
import pytest
@pytest.fixture
def deep_fixture(deep_fixture):
return f"{deep_fixture}_level3"
"#;
db.analyze_file(
PathBuf::from("/tmp/project/level2/level3/conftest.py"),
level3_conftest,
);
let level4_conftest = r#"
import pytest
@pytest.fixture
def deep_fixture(deep_fixture):
return f"{deep_fixture}_level4"
"#;
db.analyze_file(
PathBuf::from("/tmp/project/level2/level3/level4/conftest.py"),
level4_conftest,
);
let level5_conftest = r#"
import pytest
@pytest.fixture
def deep_fixture(deep_fixture):
return f"{deep_fixture}_level5"
"#;
db.analyze_file(
PathBuf::from("/tmp/project/level2/level3/level4/level5/conftest.py"),
level5_conftest,
);
let test_content = r#"
def test_deep(deep_fixture):
assert "level5" in deep_fixture
"#;
let test_path = PathBuf::from("/tmp/project/level2/level3/level4/level5/test_deep.py");
db.analyze_file(test_path.clone(), test_content);
let definition = db.find_fixture_definition(&test_path, 1, 15);
assert!(definition.is_some());
assert!(definition
.unwrap()
.file_path
.ends_with("level5/conftest.py"));
}
#[test]
#[timeout(30000)]
fn test_diamond_dependency_pattern() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def base_fixture():
return "base"
@pytest.fixture
def branch_a(base_fixture):
return f"{base_fixture}_a"
@pytest.fixture
def branch_b(base_fixture):
return f"{base_fixture}_b"
@pytest.fixture
def diamond(branch_a, branch_b):
return f"{branch_a}_{branch_b}"
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
assert!(db.definitions.contains_key("base_fixture"));
assert!(db.definitions.contains_key("branch_a"));
assert!(db.definitions.contains_key("branch_b"));
assert!(db.definitions.contains_key("diamond"));
let usages = db.usages.get(&conftest_path).unwrap();
assert!(usages.iter().any(|u| u.name == "base_fixture"));
assert!(usages.iter().any(|u| u.name == "branch_a"));
assert!(usages.iter().any(|u| u.name == "branch_b"));
}
#[test]
#[timeout(30000)]
fn test_ten_level_directory_depth() {
let db = FixtureDatabase::new();
let root_conftest = r#"
import pytest
@pytest.fixture
def deep_search():
return "found"
"#;
db.analyze_file(PathBuf::from("/tmp/root/conftest.py"), root_conftest);
let test_content = r#"
def test_deep_search(deep_search):
assert deep_search == "found"
"#;
let deep_path = PathBuf::from("/tmp/root/a/b/c/d/e/f/g/h/i/j/test_deep.py");
db.analyze_file(deep_path.clone(), test_content);
let definition = db.find_fixture_definition(&deep_path, 1, 22);
assert!(definition.is_some(), "Should find fixture 10 levels up");
assert_eq!(definition.unwrap().name, "deep_search");
}
#[test]
#[timeout(30000)]
fn test_fixture_chain_middle_doesnt_use_parent() {
let db = FixtureDatabase::new();
let root_conftest = r#"
import pytest
@pytest.fixture
def chain_fixture():
return "root"
"#;
db.analyze_file(PathBuf::from("/tmp/test/conftest.py"), root_conftest);
let middle_conftest = r#"
import pytest
@pytest.fixture
def chain_fixture():
# Middle fixture doesn't use parent - breaks chain
return "middle_independent"
"#;
db.analyze_file(
PathBuf::from("/tmp/test/subdir/conftest.py"),
middle_conftest,
);
let leaf_conftest = r#"
import pytest
@pytest.fixture
def chain_fixture(chain_fixture):
# Leaf uses parent (middle), but middle doesn't use root
return f"{chain_fixture}_leaf"
"#;
db.analyze_file(
PathBuf::from("/tmp/test/subdir/deep/conftest.py"),
leaf_conftest,
);
let test_content = r#"
def test_chain(chain_fixture):
assert "leaf" in chain_fixture
"#;
let test_path = PathBuf::from("/tmp/test/subdir/deep/test_chain.py");
db.analyze_file(test_path.clone(), test_content);
let definition = db.find_fixture_definition(&test_path, 1, 16);
assert!(definition.is_some());
let def = definition.unwrap();
assert!(def.file_path.ends_with("deep/conftest.py"));
}
#[test]
#[timeout(30000)]
fn test_multiple_fixtures_same_name_in_file() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def duplicate_fixture():
return "first"
@pytest.fixture
def duplicate_fixture():
return "second"
@pytest.fixture
def duplicate_fixture():
return "third"
"#;
let file_path = PathBuf::from("/home/test/conftest.py");
db.analyze_file(file_path.clone(), content);
let defs = db.definitions.get("duplicate_fixture").unwrap();
assert_eq!(defs.len(), 3, "Should store all duplicate definitions");
let lines: Vec<usize> = defs.iter().map(|d| d.line).collect();
assert_eq!(lines.len(), 3);
assert!(lines[0] < lines[1]);
assert!(lines[1] < lines[2]);
}
#[test]
#[timeout(30000)]
fn test_sibling_directories_with_same_fixture() {
let db = FixtureDatabase::new();
let dir_a_conftest = r#"
import pytest
@pytest.fixture
def sibling_fixture():
return "from_a"
"#;
db.analyze_file(
PathBuf::from("/tmp/project/dir_a/conftest.py"),
dir_a_conftest,
);
let dir_b_conftest = r#"
import pytest
@pytest.fixture
def sibling_fixture():
return "from_b"
"#;
db.analyze_file(
PathBuf::from("/tmp/project/dir_b/conftest.py"),
dir_b_conftest,
);
let test_a_content = r#"
def test_a(sibling_fixture):
assert sibling_fixture == "from_a"
"#;
let test_a_path = PathBuf::from("/tmp/project/dir_a/test_a.py");
db.analyze_file(test_a_path.clone(), test_a_content);
let def_a = db.find_fixture_definition(&test_a_path, 1, 12);
assert!(def_a.is_some());
assert!(def_a.unwrap().file_path.to_str().unwrap().contains("dir_a"));
let test_b_content = r#"
def test_b(sibling_fixture):
assert sibling_fixture == "from_b"
"#;
let test_b_path = PathBuf::from("/tmp/project/dir_b/test_b.py");
db.analyze_file(test_b_path.clone(), test_b_content);
let def_b = db.find_fixture_definition(&test_b_path, 1, 12);
assert!(def_b.is_some());
assert!(def_b.unwrap().file_path.to_str().unwrap().contains("dir_b"));
}
#[test]
#[timeout(30000)]
fn test_fixture_with_six_level_parameter_chain() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def level1():
return 1
@pytest.fixture
def level2(level1):
return level1 + 1
@pytest.fixture
def level3(level2):
return level2 + 1
@pytest.fixture
def level4(level3):
return level3 + 1
@pytest.fixture
def level5(level4):
return level4 + 1
@pytest.fixture
def level6(level5):
return level5 + 1
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path.clone(), content);
for i in 1..=6 {
let name = format!("level{}", i);
assert!(db.definitions.contains_key(&name), "Should detect {}", name);
}
let usages = db.usages.get(&conftest_path).unwrap();
assert!(usages.iter().any(|u| u.name == "level1"));
assert!(usages.iter().any(|u| u.name == "level2"));
assert!(usages.iter().any(|u| u.name == "level3"));
assert!(usages.iter().any(|u| u.name == "level4"));
assert!(usages.iter().any(|u| u.name == "level5"));
}
#[test]
#[timeout(30000)]
fn test_circular_dependency_detection() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def fixture_a(fixture_b):
return f"a_{fixture_b}"
@pytest.fixture
def fixture_b(fixture_a):
return f"b_{fixture_a}"
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path.clone(), content);
assert!(db.definitions.contains_key("fixture_a"));
assert!(db.definitions.contains_key("fixture_b"));
let usages = db.usages.get(&conftest_path).unwrap();
assert!(usages.iter().any(|u| u.name == "fixture_a"));
assert!(usages.iter().any(|u| u.name == "fixture_b"));
println!("Circular dependencies detected but not validated (pytest's job)");
}
#[test]
#[timeout(30000)]
fn test_multiple_third_party_same_fixture_name() {
let db = FixtureDatabase::new();
let plugin1_content = r#"
import pytest
@pytest.fixture
def event_loop():
return "from_plugin1"
"#;
db.analyze_file(
PathBuf::from("/tmp/.venv/lib/python3.11/site-packages/plugin1/fixtures.py"),
plugin1_content,
);
let plugin2_content = r#"
import pytest
@pytest.fixture
def event_loop():
return "from_plugin2"
"#;
db.analyze_file(
PathBuf::from("/tmp/.venv/lib/python3.11/site-packages/plugin2/fixtures.py"),
plugin2_content,
);
let defs = db.definitions.get("event_loop").unwrap();
assert_eq!(defs.len(), 2, "Should detect both third-party fixtures");
let paths: Vec<&str> = defs.iter().map(|d| d.file_path.to_str().unwrap()).collect();
assert!(
paths.iter().all(|p| p.contains("site-packages")),
"All definitions should be from site-packages"
);
let test_content = r#"
def test_event_loop(event_loop):
pass
"#;
let test_path = PathBuf::from("/tmp/project/test_async.py");
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
assert_eq!(usages.len(), 1, "Should detect usage in test");
assert_eq!(usages[0].name, "event_loop");
}
#[test]
#[timeout(30000)]
fn test_unicode_characters_in_path() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
return "test"
"#;
let path = PathBuf::from("/tmp/test/日本語/тест/test_unicode.py");
db.analyze_file(path.clone(), content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert_eq!(defs[0].file_path, path);
}
#[test]
#[timeout(30000)]
fn test_spaces_in_path() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
return "test"
"#;
let path = PathBuf::from("/tmp/test/my folder/sub folder/test file.py");
db.analyze_file(path.clone(), content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert_eq!(defs[0].file_path, path);
}
#[test]
#[timeout(30000)]
fn test_special_characters_in_path() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
return "test"
"#;
let path = PathBuf::from("/tmp/test/my(folder)[2023]/test-file_v2.py");
db.analyze_file(path.clone(), content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert_eq!(defs[0].file_path, path);
}
#[test]
#[timeout(30000)]
fn test_very_long_path() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
return "test"
"#;
let long_component = "a".repeat(50);
let path_str = format!(
"/tmp/{}/{}/{}/{}/{}/{}/test.py",
long_component,
long_component,
long_component,
long_component,
long_component,
long_component
);
let path = PathBuf::from(path_str);
db.analyze_file(path.clone(), content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_paths_with_dots() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
return "test"
"#;
let path = PathBuf::from("/tmp/test/.hidden/.config/test.py");
db.analyze_file(path.clone(), content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert_eq!(defs[0].file_path, path);
}
#[test]
#[timeout(30000)]
fn test_conftest_hierarchy_with_unicode_paths() {
let db = FixtureDatabase::new();
let parent_content = r#"
import pytest
@pytest.fixture
def base_fixture():
return "base"
"#;
let parent_path = PathBuf::from("/tmp/проект/conftest.py");
db.analyze_file(parent_path.clone(), parent_content);
let test_content = r#"
def test_something(base_fixture):
assert base_fixture == "base"
"#;
let test_path = PathBuf::from("/tmp/проект/tests/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
assert_eq!(usages.len(), 1);
assert_eq!(usages[0].name, "base_fixture");
}
#[test]
#[timeout(30000)]
fn test_fixture_resolution_with_special_char_paths() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def special_fixture():
return "special"
"#;
let conftest_path = PathBuf::from("/tmp/my-project (2023)/conftest.py");
db.analyze_file(conftest_path, conftest_content);
let test_content = r#"
def test_something(special_fixture):
pass
"#;
let test_path = PathBuf::from("/tmp/my-project (2023)/tests/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
assert_eq!(usages.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_multiple_consecutive_slashes_in_path() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
return "test"
"#;
let path = PathBuf::from("/tmp/test//subdir///test.py");
db.analyze_file(path.clone(), content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_path_with_trailing_slash() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
return "test"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert_eq!(defs[0].file_path, path);
}
#[test]
#[timeout(30000)]
fn test_emoji_in_path() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
return "test"
"#;
let path = PathBuf::from("/tmp/test/😀_folder/🎉test.py");
db.analyze_file(path.clone(), content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert_eq!(defs[0].file_path, path);
}
#[test]
#[timeout(30000)]
fn test_scan_workspace_nonexistent_path() {
let db = FixtureDatabase::new();
let nonexistent_path = std::path::PathBuf::from("/nonexistent/path/that/should/not/exist");
db.scan_workspace(&nonexistent_path);
assert!(db.definitions.is_empty());
assert!(db.usages.is_empty());
}
#[test]
#[timeout(30000)]
fn test_scan_workspace_with_no_python_files() {
let db = FixtureDatabase::new();
let temp_dir = std::env::temp_dir().join("test_no_python_files");
std::fs::create_dir_all(&temp_dir).ok();
db.scan_workspace(&temp_dir);
assert!(db.definitions.is_empty());
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
#[timeout(30000)]
fn test_scan_workspace_with_only_non_test_files() {
let db = FixtureDatabase::new();
let temp_dir = std::env::temp_dir().join("test_no_test_files");
std::fs::create_dir_all(&temp_dir).ok();
let file_path = temp_dir.join("utils.py");
std::fs::write(
&file_path,
r#"
import pytest
@pytest.fixture
def my_fixture():
return "test"
"#,
)
.ok();
db.scan_workspace(&temp_dir);
assert!(db.definitions.get("my_fixture").is_none());
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
#[timeout(30000)]
fn test_scan_workspace_with_deeply_nested_structure() {
let db = FixtureDatabase::new();
let temp_dir = std::env::temp_dir().join("test_deep_nesting");
let deep_path = temp_dir.join("a/b/c/d/e/f/g/h/i/j");
std::fs::create_dir_all(&deep_path).ok();
let test_file = deep_path.join("test_deep.py");
std::fs::write(
&test_file,
r#"
import pytest
@pytest.fixture
def deep_fixture():
return "deep"
"#,
)
.ok();
db.scan_workspace(&temp_dir);
let defs = db.definitions.get("deep_fixture");
assert!(defs.is_some());
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
#[timeout(30000)]
fn test_scan_workspace_with_mixed_file_types() {
let db = FixtureDatabase::new();
let temp_dir = std::env::temp_dir().join("test_mixed_files");
std::fs::create_dir_all(&temp_dir).ok();
std::fs::write(
temp_dir.join("conftest.py"),
r#"
import pytest
@pytest.fixture
def conftest_fixture():
return "conftest"
"#,
)
.ok();
std::fs::write(
temp_dir.join("test_example.py"),
r#"
import pytest
@pytest.fixture
def test_file_fixture():
return "test"
"#,
)
.ok();
std::fs::write(
temp_dir.join("example_test.py"),
r#"
import pytest
@pytest.fixture
def suffix_test_fixture():
return "suffix"
"#,
)
.ok();
std::fs::write(
temp_dir.join("utils.py"),
r#"
import pytest
@pytest.fixture
def utils_fixture():
return "utils"
"#,
)
.ok();
db.scan_workspace(&temp_dir);
assert!(db.definitions.get("conftest_fixture").is_some());
assert!(db.definitions.get("test_file_fixture").is_some());
assert!(db.definitions.get("suffix_test_fixture").is_some());
assert!(db.definitions.get("utils_fixture").is_none());
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
#[timeout(30000)]
fn test_empty_conftest_file() {
let db = FixtureDatabase::new();
let content = "";
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path, content);
assert!(db.definitions.is_empty());
}
#[test]
#[timeout(30000)]
fn test_conftest_with_only_imports() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
import sys
from pathlib import Path
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path, content);
assert!(db.definitions.is_empty());
}
#[test]
#[timeout(30000)]
fn test_file_with_syntax_error_in_docstring() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
"""
This docstring has "quotes" and 'apostrophes'
And some special chars: @#$%^&*()
"""
return "test"
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert!(defs[0].docstring.is_some());
}
#[test]
#[timeout(30000)]
fn test_fixture_in_file_with_multiple_encodings_declared() {
let db = FixtureDatabase::new();
let content = r#"# -*- coding: utf-8 -*-
import pytest
@pytest.fixture
def my_fixture():
return "test"
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_empty_docstring() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
""""""
return "test"
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
if let Some(doc) = &defs[0].docstring {
assert!(doc.trim().is_empty());
}
}
#[test]
#[timeout(30000)]
fn test_fixture_with_multiline_docstring() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
"""
This is a multi-line docstring.
It has multiple paragraphs.
Args:
None
Returns:
str: A test string
"""
return "test"
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert!(defs[0].docstring.is_some());
let docstring = defs[0].docstring.as_ref().unwrap();
assert!(docstring.contains("multi-line"));
assert!(docstring.contains("Returns:"));
}
#[test]
#[timeout(30000)]
fn test_fixture_with_single_quoted_docstring() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
'''Single quoted docstring'''
return "test"
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert!(defs[0].docstring.is_some());
assert_eq!(
defs[0].docstring.as_ref().unwrap().trim(),
"Single quoted docstring"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_rst_formatted_docstring() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
"""
Fixture with RST formatting.
:param param1: First parameter
:type param1: str
:returns: Test value
:rtype: str
.. note::
This is a note block
"""
return "test"
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert!(defs[0].docstring.is_some());
let docstring = defs[0].docstring.as_ref().unwrap();
assert!(docstring.contains(":param"));
assert!(docstring.contains(".. note::"));
}
#[test]
#[timeout(30000)]
fn test_fixture_with_google_style_docstring() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
"""Fixture with Google-style docstring.
This fixture provides a test value.
Args:
None
Returns:
str: A test string value
Yields:
str: Test value for the fixture
"""
return "test"
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert!(defs[0].docstring.is_some());
let docstring = defs[0].docstring.as_ref().unwrap();
assert!(docstring.contains("Args:"));
assert!(docstring.contains("Returns:"));
assert!(docstring.contains("Yields:"));
}
#[test]
#[timeout(30000)]
fn test_fixture_with_numpy_style_docstring() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
"""
Fixture with NumPy-style docstring.
Parameters
----------
None
Returns
-------
str
A test string value
Notes
-----
This is a test fixture
"""
return "test"
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert!(defs[0].docstring.is_some());
let docstring = defs[0].docstring.as_ref().unwrap();
assert!(docstring.contains("Parameters"));
assert!(docstring.contains("----------"));
assert!(docstring.contains("Returns"));
}
#[test]
#[timeout(30000)]
fn test_fixture_with_unicode_in_docstring() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
"""
Fixture with Unicode characters: 日本語, Русский, العربية, 🎉
This tests international character support in docstrings.
"""
return "test"
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert!(defs[0].docstring.is_some());
let docstring = defs[0].docstring.as_ref().unwrap();
assert!(docstring.contains("日本語"));
assert!(docstring.contains("Русский"));
assert!(docstring.contains("🎉"));
}
#[test]
#[timeout(30000)]
fn test_fixture_with_code_blocks_in_docstring() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
"""
Fixture with code examples.
Example:
>>> result = my_fixture()
>>> assert result == "test"
Code block:
```python
def use_fixture(my_fixture):
print(my_fixture)
```
"""
return "test"
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert!(defs[0].docstring.is_some());
let docstring = defs[0].docstring.as_ref().unwrap();
assert!(docstring.contains(">>>"));
assert!(docstring.contains("```python"));
}
#[test]
#[timeout(30000)]
fn test_large_number_of_fixtures_in_single_file() {
let db = FixtureDatabase::new();
let mut content = String::from("import pytest\n\n");
for i in 0..100 {
content.push_str(&format!(
"@pytest.fixture\ndef fixture_{}():\n return {}\n\n",
i, i
));
}
let path = PathBuf::from("/tmp/test/test_many_fixtures.py");
db.analyze_file(path, &content);
assert_eq!(db.definitions.len(), 100);
assert!(db.definitions.get("fixture_0").is_some());
assert!(db.definitions.get("fixture_50").is_some());
assert!(db.definitions.get("fixture_99").is_some());
}
#[test]
#[timeout(30000)]
fn test_deeply_nested_fixture_dependencies() {
let db = FixtureDatabase::new();
let mut content = String::from("import pytest\n\n");
content.push_str("@pytest.fixture\ndef fixture_0():\n return 0\n\n");
for i in 1..20 {
content.push_str(&format!(
"@pytest.fixture\ndef fixture_{}(fixture_{}):\n return {} + fixture_{}\n\n",
i,
i - 1,
i,
i - 1
));
}
let path = PathBuf::from("/tmp/test/test_deep_chain.py");
db.analyze_file(path, &content);
assert_eq!(db.definitions.len(), 20);
let deepest = db.definitions.get("fixture_19").unwrap();
assert_eq!(deepest.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_many_parameters() {
let db = FixtureDatabase::new();
let mut content = String::from("import pytest\n\n");
for i in 0..15 {
content.push_str(&format!(
"@pytest.fixture\ndef dep_{}():\n return {}\n\n",
i, i
));
}
content.push_str("@pytest.fixture\ndef mega_fixture(");
for i in 0..15 {
if i > 0 {
content.push_str(", ");
}
content.push_str(&format!("dep_{}", i));
}
content.push_str("):\n return sum([");
for i in 0..15 {
if i > 0 {
content.push_str(", ");
}
content.push_str(&format!("dep_{}", i));
}
content.push_str("])\n");
let path = PathBuf::from("/tmp/test/test_many_params.py");
db.analyze_file(path, &content);
assert_eq!(db.definitions.len(), 16);
assert!(db.definitions.get("mega_fixture").is_some());
}
#[test]
#[timeout(30000)]
fn test_very_long_fixture_function_body() {
let db = FixtureDatabase::new();
let mut content = String::from("import pytest\n\n@pytest.fixture\ndef long_fixture():\n");
content.push_str(" \"\"\"A fixture with a very long body.\"\"\"\n");
for i in 0..100 {
content.push_str(&format!(" line_{} = {}\n", i, i));
}
content.push_str(" return line_99\n");
let path = PathBuf::from("/tmp/test/test_long_function.py");
db.analyze_file(path, &content);
let defs = db.definitions.get("long_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert!(defs[0].docstring.is_some());
}
#[test]
#[timeout(30000)]
fn test_multiple_files_with_same_fixture_names() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "value"
"#;
for i in 0..50 {
let path = PathBuf::from(format!("/tmp/test/dir_{}/test_file.py", i));
db.analyze_file(path, content);
}
let defs = db.definitions.get("shared_fixture").unwrap();
assert_eq!(defs.len(), 50);
}
#[test]
#[timeout(30000)]
fn test_rapid_file_updates() {
let db = FixtureDatabase::new();
let path = PathBuf::from("/tmp/test/test_updates.py");
for i in 0..20 {
let content = format!(
r#"
import pytest
@pytest.fixture
def dynamic_fixture():
return {}
"#,
i
);
db.analyze_file(path.clone(), &content);
}
let defs = db.definitions.get("dynamic_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert_eq!(defs[0].file_path, path);
}
#[test]
#[timeout(30000)]
fn test_fixture_detection_without_venv() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
return "test"
def test_example(my_fixture):
assert my_fixture == "test"
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path.clone(), content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
let usages = db.usages.get(&path).unwrap();
assert_eq!(usages.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_third_party_fixture_in_site_packages() {
let db = FixtureDatabase::new();
let plugin_content = r#"
import pytest
@pytest.fixture
def third_party_fixture():
"""A fixture from a third-party plugin."""
return "plugin_value"
"#;
let plugin_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_plugin/fixtures.py");
db.analyze_file(plugin_path, plugin_content);
let test_content = r#"
def test_example(third_party_fixture):
assert third_party_fixture == "plugin_value"
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let defs = db.definitions.get("third_party_fixture").unwrap();
assert_eq!(defs.len(), 1);
let usages = db.usages.get(&test_path).unwrap();
assert_eq!(usages.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_override_from_third_party() {
let db = FixtureDatabase::new();
let plugin_content = r#"
import pytest
@pytest.fixture
def event_loop():
"""Plugin event loop fixture."""
return "plugin_loop"
"#;
let plugin_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_asyncio/fixtures.py");
db.analyze_file(plugin_path.clone(), plugin_content);
let conftest_content = r#"
import pytest
@pytest.fixture
def event_loop():
"""Custom event loop fixture."""
return "custom_loop"
"#;
let conftest_path = PathBuf::from("/tmp/project/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_example(event_loop):
assert event_loop is not None
"#;
let test_path = PathBuf::from("/tmp/project/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let defs = db.definitions.get("event_loop").unwrap();
assert_eq!(defs.len(), 2);
let conftest_def = defs.iter().find(|d| d.file_path == conftest_path);
assert!(conftest_def.is_some());
let plugin_def = defs.iter().find(|d| d.file_path == plugin_path);
assert!(plugin_def.is_some());
}
#[test]
#[timeout(30000)]
fn test_multiple_third_party_plugins_same_fixture() {
let db = FixtureDatabase::new();
let plugin1_content = r#"
import pytest
@pytest.fixture
def common_fixture():
return "plugin1"
"#;
let plugin1_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_plugin1/fixtures.py");
db.analyze_file(plugin1_path, plugin1_content);
let plugin2_content = r#"
import pytest
@pytest.fixture
def common_fixture():
return "plugin2"
"#;
let plugin2_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_plugin2/fixtures.py");
db.analyze_file(plugin2_path, plugin2_content);
let defs = db.definitions.get("common_fixture").unwrap();
assert_eq!(defs.len(), 2);
}
#[test]
#[timeout(30000)]
fn test_venv_fixture_with_no_usage() {
let db = FixtureDatabase::new();
let plugin_content = r#"
import pytest
@pytest.fixture
def unused_plugin_fixture():
"""A fixture that's defined but never used."""
return "unused"
"#;
let plugin_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_plugin/fixtures.py");
db.analyze_file(plugin_path, plugin_content);
let defs = db.definitions.get("unused_plugin_fixture").unwrap();
assert_eq!(defs.len(), 1);
let refs = db.find_fixture_references("unused_plugin_fixture");
assert!(refs.is_empty());
}
#[test]
#[timeout(30000)]
fn test_fixture_with_property_decorator() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
class MyFixture:
@property
def value(self):
return "test"
@pytest.fixture
def my_fixture():
return MyFixture()
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_staticmethod() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
class FixtureHelper:
@staticmethod
def create():
return "test"
@pytest.fixture
def my_fixture():
return FixtureHelper.create()
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_classmethod() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
class FixtureHelper:
@classmethod
def create(cls):
return "test"
@pytest.fixture
def my_fixture():
return FixtureHelper.create()
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_contextmanager() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
from contextlib import contextmanager
@contextmanager
def resource():
yield "resource"
@pytest.fixture
def my_fixture():
with resource() as r:
return r
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_multiple_decorators() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
def custom_decorator(func):
return func
@pytest.fixture
@custom_decorator
def my_fixture():
return "test"
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_inside_if_block_not_supported() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
import sys
if sys.version_info >= (3, 8):
@pytest.fixture
def version_specific_fixture():
return "py38+"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path, content);
assert!(db.definitions.get("version_specific_fixture").is_none());
}
#[test]
#[timeout(30000)]
fn test_fixture_with_walrus_operator_in_body() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
if (result := expensive_operation()):
return result
return None
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_match_statement() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
value = "test"
match value:
case "test":
return "matched"
case _:
return "default"
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_exception_group() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
try:
return "test"
except* ValueError as e:
return None
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_dataclass() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
from dataclasses import dataclass
@dataclass
class Config:
value: str
@pytest.fixture
def config_fixture():
return Config(value="test")
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("config_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_named_tuple() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
@pytest.fixture
def point_fixture():
return Point(1, 2)
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("point_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_protocol() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
from typing import Protocol
class Readable(Protocol):
def read(self) -> str: ...
@pytest.fixture
def readable_fixture() -> Readable:
class TextReader:
def read(self) -> str:
return "test"
return TextReader()
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("readable_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_fixture_with_generic_type() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
from typing import Generic, TypeVar
T = TypeVar('T')
class Container(Generic[T]):
def __init__(self, value: T):
self.value = value
@pytest.fixture
def container_fixture() -> Container[str]:
return Container("test")
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path, content);
let defs = db.definitions.get("container_fixture").unwrap();
assert_eq!(defs.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_pytest_flask_fixtures() {
let db = FixtureDatabase::new();
let plugin_content = r#"
import pytest
@pytest.fixture
def app():
"""Flask application fixture."""
from flask import Flask
app = Flask(__name__)
return app
@pytest.fixture
def client(app):
"""Flask test client fixture."""
return app.test_client()
"#;
let plugin_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_flask/fixtures.py");
db.analyze_file(plugin_path, plugin_content);
assert!(db.definitions.get("app").is_some());
assert!(db.definitions.get("client").is_some());
}
#[test]
#[timeout(30000)]
fn test_pytest_httpx_fixtures() {
let db = FixtureDatabase::new();
let plugin_content = r#"
import pytest
@pytest.fixture
async def async_client():
"""HTTPX async client fixture."""
import httpx
async with httpx.AsyncClient() as client:
yield client
"#;
let plugin_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_httpx/fixtures.py");
db.analyze_file(plugin_path, plugin_content);
assert!(db.definitions.get("async_client").is_some());
}
#[test]
#[timeout(30000)]
fn test_pytest_postgresql_fixtures() {
let db = FixtureDatabase::new();
let plugin_content = r#"
import pytest
@pytest.fixture
def postgresql():
"""PostgreSQL database fixture."""
return {"host": "localhost", "port": 5432}
@pytest.fixture
def postgresql_proc(postgresql):
"""PostgreSQL process fixture."""
return postgresql
"#;
let plugin_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_postgresql/fixtures.py");
db.analyze_file(plugin_path, plugin_content);
assert!(db.definitions.get("postgresql").is_some());
assert!(db.definitions.get("postgresql_proc").is_some());
}
#[test]
#[timeout(30000)]
fn test_pytest_docker_fixtures() {
let db = FixtureDatabase::new();
let plugin_content = r#"
import pytest
@pytest.fixture(scope="session")
def docker_compose_file():
"""Docker compose file fixture."""
return "docker-compose.yml"
@pytest.fixture(scope="session")
def docker_services(docker_compose_file):
"""Docker services fixture."""
return {"web": "http://localhost:8000"}
"#;
let plugin_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_docker/fixtures.py");
db.analyze_file(plugin_path, plugin_content);
assert!(db.definitions.get("docker_compose_file").is_some());
assert!(db.definitions.get("docker_services").is_some());
}
#[test]
#[timeout(30000)]
fn test_pytest_factoryboy_fixtures() {
let db = FixtureDatabase::new();
let plugin_content = r#"
import pytest
import factory
class UserFactory(factory.Factory):
class Meta:
model = dict
username = "testuser"
email = "test@example.com"
@pytest.fixture
def user_factory():
"""User factory fixture."""
return UserFactory
"#;
let plugin_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_factoryboy/fixtures.py");
db.analyze_file(plugin_path, plugin_content);
assert!(db.definitions.get("user_factory").is_some());
}
#[test]
#[timeout(30000)]
fn test_pytest_freezegun_fixtures() {
let db = FixtureDatabase::new();
let plugin_content = r#"
import pytest
from freezegun import freeze_time
@pytest.fixture
def frozen_time():
"""Frozen time fixture."""
with freeze_time("2024-01-01"):
yield
"#;
let plugin_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_freezegun/fixtures.py");
db.analyze_file(plugin_path, plugin_content);
assert!(db.definitions.get("frozen_time").is_some());
}
#[test]
#[timeout(30000)]
fn test_pytest_celery_fixtures() {
let db = FixtureDatabase::new();
let plugin_content = r#"
import pytest
@pytest.fixture(scope="session")
def celery_config():
"""Celery configuration fixture."""
return {"broker_url": "redis://localhost:6379"}
@pytest.fixture
def celery_app(celery_config):
"""Celery application fixture."""
from celery import Celery
return Celery("test_app", **celery_config)
@pytest.fixture
def celery_worker(celery_app):
"""Celery worker fixture."""
return celery_app.Worker()
"#;
let plugin_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_celery/fixtures.py");
db.analyze_file(plugin_path, plugin_content);
assert!(db.definitions.get("celery_config").is_some());
assert!(db.definitions.get("celery_app").is_some());
assert!(db.definitions.get("celery_worker").is_some());
}
#[test]
#[timeout(30000)]
fn test_pytest_aiohttp_fixtures() {
let db = FixtureDatabase::new();
let plugin_content = r#"
import pytest
@pytest.fixture
async def aiohttp_client():
"""Aiohttp client fixture."""
import aiohttp
async with aiohttp.ClientSession() as session:
yield session
@pytest.fixture
async def aiohttp_server():
"""Aiohttp server fixture."""
from aiohttp import web
app = web.Application()
return app
"#;
let plugin_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_aiohttp/fixtures.py");
db.analyze_file(plugin_path, plugin_content);
assert!(db.definitions.get("aiohttp_client").is_some());
assert!(db.definitions.get("aiohttp_server").is_some());
}
#[test]
#[timeout(30000)]
fn test_pytest_benchmark_fixtures() {
let db = FixtureDatabase::new();
let plugin_content = r#"
import pytest
@pytest.fixture
def benchmark():
"""Benchmark fixture."""
class Benchmark:
def __call__(self, func):
return func()
return Benchmark()
"#;
let plugin_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_benchmark/fixtures.py");
db.analyze_file(plugin_path, plugin_content);
assert!(db.definitions.get("benchmark").is_some());
}
#[test]
#[timeout(30000)]
fn test_pytest_playwright_fixtures() {
let db = FixtureDatabase::new();
let plugin_content = r#"
import pytest
@pytest.fixture(scope="session")
def browser():
"""Playwright browser fixture."""
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
yield p.chromium.launch()
@pytest.fixture
def page(browser):
"""Playwright page fixture."""
page = browser.new_page()
yield page
page.close()
@pytest.fixture
def context(browser):
"""Playwright browser context fixture."""
context = browser.new_context()
yield context
context.close()
"#;
let plugin_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_playwright/fixtures.py");
db.analyze_file(plugin_path, plugin_content);
assert!(db.definitions.get("browser").is_some());
assert!(db.definitions.get("page").is_some());
assert!(db.definitions.get("context").is_some());
}
#[test]
#[timeout(30000)]
fn test_keyword_only_fixture_usage_in_test() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_with_kwonly(*, my_fixture):
assert my_fixture == 42
"#;
let test_path = PathBuf::from("/tmp/test_kwonly.py");
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path);
assert!(usages.is_some(), "Usages should be detected");
let usages = usages.unwrap();
assert!(
usages.iter().any(|u| u.name == "my_fixture"),
"Should detect my_fixture usage in keyword-only argument"
);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
0,
"Should not detect any undeclared fixtures for keyword-only arg"
);
}
#[test]
#[timeout(30000)]
fn test_keyword_only_fixture_usage_with_type_annotation() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
from pathlib import Path
@pytest.fixture
def tmp_path():
return Path("/tmp")
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
from pathlib import Path
def test_run_command(*, tmp_path: Path) -> None:
"""Test that uses a keyword-only fixture with type annotation."""
rst_file = tmp_path / "example.rst"
assert rst_file.parent == tmp_path
"#;
let test_path = PathBuf::from("/tmp/test_kwonly_typed.py");
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path);
assert!(usages.is_some(), "Usages should be detected");
let usages = usages.unwrap();
assert!(
usages.iter().any(|u| u.name == "tmp_path"),
"Should detect tmp_path usage in keyword-only argument"
);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
0,
"Should not detect any undeclared fixtures for keyword-only arg with type annotation"
);
}
#[test]
#[timeout(30000)]
fn test_positional_only_fixture_usage() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_with_posonly(my_fixture, /):
assert my_fixture == 42
"#;
let test_path = PathBuf::from("/tmp/test_posonly.py");
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path);
assert!(usages.is_some(), "Usages should be detected");
let usages = usages.unwrap();
assert!(
usages.iter().any(|u| u.name == "my_fixture"),
"Should detect my_fixture usage in positional-only argument"
);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
0,
"Should not detect any undeclared fixtures for positional-only arg"
);
}
#[test]
#[timeout(30000)]
fn test_mixed_argument_types_fixture_usage() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def fixture_a():
return "a"
@pytest.fixture
def fixture_b():
return "b"
@pytest.fixture
def fixture_c():
return "c"
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_with_all_types(fixture_a, /, fixture_b, *, fixture_c):
assert fixture_a == "a"
assert fixture_b == "b"
assert fixture_c == "c"
"#;
let test_path = PathBuf::from("/tmp/test_mixed.py");
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path);
assert!(usages.is_some(), "Usages should be detected");
let usages = usages.unwrap();
assert!(
usages.iter().any(|u| u.name == "fixture_a"),
"Should detect fixture_a usage in positional-only argument"
);
assert!(
usages.iter().any(|u| u.name == "fixture_b"),
"Should detect fixture_b usage in regular argument"
);
assert!(
usages.iter().any(|u| u.name == "fixture_c"),
"Should detect fixture_c usage in keyword-only argument"
);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
0,
"Should not detect any undeclared fixtures for mixed argument types"
);
}
#[test]
#[timeout(30000)]
fn test_keyword_only_fixture_in_fixture_definition() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def base_fixture():
return 42
@pytest.fixture
def dependent_fixture(*, base_fixture):
return base_fixture * 2
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
assert!(
db.definitions.contains_key("base_fixture"),
"base_fixture should be detected"
);
assert!(
db.definitions.contains_key("dependent_fixture"),
"dependent_fixture should be detected"
);
let usages = db.usages.get(&conftest_path);
assert!(usages.is_some(), "Usages should be detected");
let usages = usages.unwrap();
assert!(
usages.iter().any(|u| u.name == "base_fixture"),
"Should detect base_fixture usage as keyword-only dependency in dependent_fixture"
);
}
#[test]
#[timeout(30000)]
fn test_keyword_only_with_multiple_fixtures() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def fixture_x():
return "x"
@pytest.fixture
def fixture_y():
return "y"
@pytest.fixture
def fixture_z():
return "z"
"#;
let conftest_path = PathBuf::from("/tmp/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_multi_kwonly(*, fixture_x, fixture_y, fixture_z):
assert fixture_x == "x"
assert fixture_y == "y"
assert fixture_z == "z"
"#;
let test_path = PathBuf::from("/tmp/test_multi_kwonly.py");
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path);
assert!(usages.is_some(), "Usages should be detected");
let usages = usages.unwrap();
assert!(
usages.iter().any(|u| u.name == "fixture_x"),
"Should detect fixture_x usage"
);
assert!(
usages.iter().any(|u| u.name == "fixture_y"),
"Should detect fixture_y usage"
);
assert!(
usages.iter().any(|u| u.name == "fixture_z"),
"Should detect fixture_z usage"
);
let undeclared = db.get_undeclared_fixtures(&test_path);
assert_eq!(
undeclared.len(),
0,
"Should not detect any undeclared fixtures"
);
}
#[test]
#[timeout(30000)]
fn test_go_to_definition_for_keyword_only_fixture() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_something(*, my_fixture):
assert my_fixture == 42
"#;
let test_path = PathBuf::from("/tmp/test/test_kwonly.py");
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path);
assert!(usages.is_some(), "Usages should be detected");
let usages = usages.unwrap();
let fixture_usage = usages.iter().find(|u| u.name == "my_fixture");
assert!(
fixture_usage.is_some(),
"Should detect my_fixture usage in keyword-only position"
);
let usage = fixture_usage.unwrap();
let definition =
db.find_fixture_definition(&test_path, (usage.line - 1) as u32, usage.start_char as u32);
assert!(definition.is_some(), "Definition should be found");
let def = definition.unwrap();
assert_eq!(def.name, "my_fixture");
assert_eq!(def.file_path, conftest_path);
}
#[test]
#[timeout(30000)]
fn test_scan_skips_node_modules() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_test = root.join("test_root.py");
fs::write(
&root_test,
r#"
def test_root(root_fixture):
pass
"#,
)
.unwrap();
let root_conftest = root.join("conftest.py");
fs::write(
&root_conftest,
r#"
import pytest
@pytest.fixture
def root_fixture():
return 1
"#,
)
.unwrap();
let node_modules = root.join("node_modules");
fs::create_dir_all(&node_modules).unwrap();
let node_test = node_modules.join("test_node.py");
fs::write(
&node_test,
r#"
def test_node(node_fixture):
pass
"#,
)
.unwrap();
let node_conftest = node_modules.join("conftest.py");
fs::write(
&node_conftest,
r#"
import pytest
@pytest.fixture
def node_fixture():
return 2
"#,
)
.unwrap();
let db = FixtureDatabase::new();
db.scan_workspace(root);
assert!(
db.definitions.contains_key("root_fixture"),
"root_fixture should be found"
);
assert!(
!db.definitions.contains_key("node_fixture"),
"node_fixture should NOT be found (node_modules should be skipped)"
);
}
#[test]
#[timeout(30000)]
fn test_scan_skips_git_directory() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_conftest = root.join("conftest.py");
fs::write(
&root_conftest,
r#"
import pytest
@pytest.fixture
def real_fixture():
return 1
"#,
)
.unwrap();
let git_dir = root.join(".git");
fs::create_dir_all(&git_dir).unwrap();
let git_conftest = git_dir.join("conftest.py");
fs::write(
&git_conftest,
r#"
import pytest
@pytest.fixture
def git_fixture():
return 2
"#,
)
.unwrap();
let db = FixtureDatabase::new();
db.scan_workspace(root);
assert!(
db.definitions.contains_key("real_fixture"),
"real_fixture should be found"
);
assert!(
!db.definitions.contains_key("git_fixture"),
"git_fixture should NOT be found (.git should be skipped)"
);
}
#[test]
#[timeout(30000)]
fn test_scan_skips_pycache() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_conftest = root.join("conftest.py");
fs::write(
&root_conftest,
r#"
import pytest
@pytest.fixture
def actual_fixture():
return 1
"#,
)
.unwrap();
let pycache = root.join("__pycache__");
fs::create_dir_all(&pycache).unwrap();
let cache_conftest = pycache.join("conftest.py");
fs::write(
&cache_conftest,
r#"
import pytest
@pytest.fixture
def cache_fixture():
return 2
"#,
)
.unwrap();
let db = FixtureDatabase::new();
db.scan_workspace(root);
assert!(
db.definitions.contains_key("actual_fixture"),
"actual_fixture should be found"
);
assert!(
!db.definitions.contains_key("cache_fixture"),
"cache_fixture should NOT be found (__pycache__ should be skipped)"
);
}
#[test]
#[timeout(30000)]
fn test_scan_skips_venv_but_scans_plugins() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_conftest = root.join("conftest.py");
fs::write(
&root_conftest,
r#"
import pytest
@pytest.fixture
def project_fixture():
return 1
"#,
)
.unwrap();
let venv = root.join(".venv");
fs::create_dir_all(&venv).unwrap();
let venv_test = venv.join("test_venv.py");
fs::write(
&venv_test,
r#"
def test_venv(venv_fixture):
pass
"#,
)
.unwrap();
let db = FixtureDatabase::new();
db.scan_workspace(root);
assert!(
db.definitions.contains_key("project_fixture"),
"project_fixture should be found"
);
let venv_test_path = venv_test.canonicalize().unwrap_or(venv_test);
assert!(
!db.usages.contains_key(&venv_test_path),
"test files in .venv should not be scanned"
);
}
#[test]
#[timeout(30000)]
fn test_scan_skips_multiple_directories() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_conftest = root.join("conftest.py");
fs::write(
&root_conftest,
r#"
import pytest
@pytest.fixture
def main_fixture():
return 1
"#,
)
.unwrap();
for skip_dir in &[
"node_modules",
".git",
"__pycache__",
".pytest_cache",
".mypy_cache",
"build",
"dist",
".tox",
] {
let dir = root.join(skip_dir);
fs::create_dir_all(&dir).unwrap();
let conftest = dir.join("conftest.py");
fs::write(
&conftest,
format!(
r#"
import pytest
@pytest.fixture
def {}_fixture():
return 2
"#,
skip_dir.replace(".", "").replace("-", "_")
),
)
.unwrap();
}
let db = FixtureDatabase::new();
db.scan_workspace(root);
assert!(
db.definitions.contains_key("main_fixture"),
"main_fixture should be found"
);
assert!(
!db.definitions.contains_key("node_modules_fixture"),
"node_modules fixture should be skipped"
);
assert!(
!db.definitions.contains_key("git_fixture"),
".git fixture should be skipped"
);
assert!(
!db.definitions.contains_key("__pycache___fixture"),
"__pycache__ fixture should be skipped"
);
assert!(
!db.definitions.contains_key("pytest_cache_fixture"),
".pytest_cache fixture should be skipped"
);
}
#[test]
#[timeout(30000)]
fn test_scan_skips_nested_node_modules() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_conftest = root.join("conftest.py");
fs::write(
&root_conftest,
r#"
import pytest
@pytest.fixture
def root_fix():
return 1
"#,
)
.unwrap();
let tests_dir = root.join("tests");
fs::create_dir_all(&tests_dir).unwrap();
let tests_conftest = tests_dir.join("conftest.py");
fs::write(
&tests_conftest,
r#"
import pytest
@pytest.fixture
def tests_fix():
return 2
"#,
)
.unwrap();
let deep_node = root.join("frontend/app/node_modules/some_package");
fs::create_dir_all(&deep_node).unwrap();
let deep_conftest = deep_node.join("conftest.py");
fs::write(
&deep_conftest,
r#"
import pytest
@pytest.fixture
def deep_node_fix():
return 3
"#,
)
.unwrap();
let db = FixtureDatabase::new();
db.scan_workspace(root);
assert!(
db.definitions.contains_key("root_fix"),
"root_fix should be found"
);
assert!(
db.definitions.contains_key("tests_fix"),
"tests_fix should be found"
);
assert!(
!db.definitions.contains_key("deep_node_fix"),
"deep_node_fix should NOT be found (nested node_modules should be skipped)"
);
}
#[test]
#[timeout(30000)]
fn test_usefixtures_decorator_on_function() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def db_connection():
return "connection"
@pytest.fixture
def auth_user():
return "user"
"#;
let test_content = r#"
import pytest
@pytest.mark.usefixtures("db_connection")
def test_with_usefixtures():
pass
@pytest.mark.usefixtures("db_connection", "auth_user")
def test_with_multiple_usefixtures():
pass
"#;
let conftest_path = PathBuf::from("/tmp/test_usefixtures/conftest.py");
let test_path = PathBuf::from("/tmp/test_usefixtures/test_example.py");
db.analyze_file(conftest_path, conftest_content);
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "db_connection"),
"db_connection should be detected as usage from usefixtures"
);
assert!(
usages.iter().any(|u| u.name == "auth_user"),
"auth_user should be detected as usage from usefixtures"
);
let db_conn_count = usages.iter().filter(|u| u.name == "db_connection").count();
assert_eq!(
db_conn_count, 2,
"db_connection should be used twice (once in each test)"
);
}
#[test]
#[timeout(30000)]
fn test_usefixtures_decorator_on_class() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def setup_database():
return "db"
"#;
let test_content = r#"
import pytest
@pytest.mark.usefixtures("setup_database")
class TestWithSetup:
def test_first(self):
pass
def test_second(self):
pass
"#;
let conftest_path = PathBuf::from("/tmp/test_usefixtures/conftest.py");
let test_path = PathBuf::from("/tmp/test_usefixtures/test_class.py");
db.analyze_file(conftest_path, conftest_content);
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "setup_database"),
"setup_database should be detected as usage from class usefixtures"
);
}
#[test]
#[timeout(30000)]
fn test_usefixtures_goto_definition() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
"#;
let test_content = r#"
import pytest
@pytest.mark.usefixtures("my_fixture")
def test_something():
pass
"#;
let conftest_path = PathBuf::from("/tmp/test_usefixtures/conftest.py");
let test_path = PathBuf::from("/tmp/test_usefixtures/test_goto.py");
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let definition = db.find_fixture_definition(&test_path, 3, 27);
assert!(
definition.is_some(),
"Definition should be found for fixture used in usefixtures"
);
let def = definition.unwrap();
assert_eq!(def.name, "my_fixture");
assert_eq!(def.file_path, conftest_path);
}
#[test]
#[timeout(30000)]
fn test_usefixtures_affects_unused_detection() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def used_via_usefixtures():
return "used"
@pytest.fixture
def actually_unused():
return "unused"
"#;
let test_content = r#"
import pytest
@pytest.mark.usefixtures("used_via_usefixtures")
def test_something():
pass
"#;
let conftest_path = PathBuf::from("/tmp/test_usefixtures/conftest.py");
let test_path = PathBuf::from("/tmp/test_usefixtures/test_unused.py");
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let mut all_usages: Vec<String> = Vec::new();
for entry in db.usages.iter() {
for usage in entry.value().iter() {
all_usages.push(usage.name.clone());
}
}
assert!(
all_usages.contains(&"used_via_usefixtures".to_string()),
"Fixture used via usefixtures should be tracked as used"
);
}
#[test]
#[timeout(30000)]
fn test_usefixtures_with_mark_import() {
let db = FixtureDatabase::new();
let test_content = r#"
from pytest import mark, fixture
@fixture
def my_fix():
return 1
@mark.usefixtures("my_fix")
def test_with_mark():
pass
"#;
let test_path = PathBuf::from("/tmp/test_usefixtures/test_mark.py");
db.analyze_file(test_path.clone(), test_content);
assert!(
db.definitions.contains_key("my_fix"),
"my_fix fixture should be detected"
);
let usages = db.usages.get(&test_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "my_fix"),
"my_fix should be detected as usage from mark.usefixtures"
);
}
#[test]
#[timeout(30000)]
fn test_pytestmark_usefixtures_single() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def db_connection():
return "connection"
"#;
let test_content = r#"
import pytest
pytestmark = pytest.mark.usefixtures("db_connection")
def test_something():
pass
"#;
let conftest_path = PathBuf::from("/tmp/test_pytestmark/conftest.py");
let test_path = PathBuf::from("/tmp/test_pytestmark/test_single.py");
db.analyze_file(conftest_path, conftest_content);
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "db_connection"),
"db_connection should be detected from pytestmark = pytest.mark.usefixtures(...)"
);
}
#[test]
#[timeout(30000)]
fn test_pytestmark_usefixtures_list() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def db_connection():
return "connection"
@pytest.fixture
def auth_user():
return "user"
"#;
let test_content = r#"
import pytest
pytestmark = [pytest.mark.usefixtures("db_connection", "auth_user"), pytest.mark.skip]
def test_something():
pass
"#;
let conftest_path = PathBuf::from("/tmp/test_pytestmark_list/conftest.py");
let test_path = PathBuf::from("/tmp/test_pytestmark_list/test_list.py");
db.analyze_file(conftest_path, conftest_content);
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "db_connection"),
"db_connection should be detected from pytestmark list"
);
assert!(
usages.iter().any(|u| u.name == "auth_user"),
"auth_user should be detected from pytestmark list"
);
}
#[test]
#[timeout(30000)]
fn test_pytestmark_usefixtures_tuple() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def fix1():
return 1
@pytest.fixture
def fix2():
return 2
"#;
let test_content = r#"
import pytest
pytestmark = (pytest.mark.usefixtures("fix1"), pytest.mark.usefixtures("fix2"))
def test_something():
pass
"#;
let conftest_path = PathBuf::from("/tmp/test_pytestmark_tuple/conftest.py");
let test_path = PathBuf::from("/tmp/test_pytestmark_tuple/test_tuple.py");
db.analyze_file(conftest_path, conftest_content);
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "fix1"),
"fix1 should be detected from pytestmark tuple"
);
assert!(
usages.iter().any(|u| u.name == "fix2"),
"fix2 should be detected from pytestmark tuple"
);
}
#[test]
#[timeout(30000)]
fn test_pytestmark_usefixtures_in_class() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def setup_fixture():
return "setup"
class TestWithPytestmark:
pytestmark = [pytest.mark.usefixtures("setup_fixture")]
def test_something(self):
pass
"#;
let file_path = PathBuf::from("/tmp/test_pytestmark_class/test_class.py");
db.analyze_file(file_path.clone(), content);
let usages = db.usages.get(&file_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "setup_fixture"),
"setup_fixture should be detected from pytestmark inside class"
);
}
#[test]
#[timeout(30000)]
fn test_pytestmark_usefixtures_affects_unused_detection() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def used_via_pytestmark():
return "used"
@pytest.fixture
def truly_unused():
return "unused"
pytestmark = pytest.mark.usefixtures("used_via_pytestmark")
def test_something():
pass
"#;
let file_path = PathBuf::from("/tmp/test_pytestmark_unused/test_unused.py");
db.analyze_file(file_path.clone(), content);
let unused = db.get_unused_fixtures();
assert!(
!unused.iter().any(|(_, name)| name == "used_via_pytestmark"),
"used_via_pytestmark should NOT be in unused list"
);
assert!(
unused.iter().any(|(_, name)| name == "truly_unused"),
"truly_unused should be in unused list"
);
}
#[test]
#[timeout(30000)]
fn test_pytestmark_usefixtures_annotated_assignment() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def db_connection():
return "connection"
@pytest.fixture
def auth_user():
return "user"
"#;
let test_content = r#"
import pytest
from pytest import MarkDecorator
pytestmark: list[MarkDecorator] = [pytest.mark.usefixtures("db_connection", "auth_user")]
def test_something():
pass
"#;
let conftest_path = PathBuf::from("/tmp/test_pytestmark_annassign/conftest.py");
let test_path = PathBuf::from("/tmp/test_pytestmark_annassign/test_annotated.py");
db.analyze_file(conftest_path, conftest_content);
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "db_connection"),
"db_connection should be detected from annotated pytestmark assignment"
);
assert!(
usages.iter().any(|u| u.name == "auth_user"),
"auth_user should be detected from annotated pytestmark assignment"
);
}
#[test]
#[timeout(30000)]
fn test_pytestmark_bare_annotation_no_panic() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
from pytest import MarkDecorator
pytestmark: list[MarkDecorator]
def test_something():
pass
"#;
let file_path = PathBuf::from("/tmp/test_pytestmark_bare_ann/test_bare.py");
db.analyze_file(file_path.clone(), content);
}
#[test]
#[timeout(30000)]
fn test_is_parameter_true_for_test_function_args() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_db() -> str:
return "db"
def test_uses_fixture(my_db):
pass
"#;
let path = PathBuf::from("/tmp/test_is_param/test_param_true.py");
db.analyze_file(path.clone(), content);
let usages = db.usages.get(&path).unwrap();
let usage = usages
.iter()
.find(|u| u.name == "my_db")
.expect("my_db usage should be detected");
assert!(
usage.is_parameter,
"Test function parameter usages must have is_parameter = true"
);
}
#[test]
#[timeout(30000)]
fn test_is_parameter_true_for_fixture_function_args() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def base_db() -> str:
return "db"
@pytest.fixture
def extended_db(base_db) -> str:
return base_db + "_ext"
"#;
let path = PathBuf::from("/tmp/test_is_param/test_fixture_param_true.py");
db.analyze_file(path.clone(), content);
let usages = db.usages.get(&path).unwrap();
let usage = usages
.iter()
.find(|u| u.name == "base_db")
.expect("base_db usage in extended_db should be detected");
assert!(
usage.is_parameter,
"Fixture function parameter usages must have is_parameter = true"
);
}
#[test]
#[timeout(30000)]
fn test_is_parameter_false_for_usefixtures_on_function() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_db() -> str:
return "db"
@pytest.mark.usefixtures("my_db")
def test_with_usefixtures():
pass
"#;
let path = PathBuf::from("/tmp/test_is_param/test_usefixtures_func.py");
db.analyze_file(path.clone(), content);
let usages = db.usages.get(&path).unwrap();
let usage = usages
.iter()
.find(|u| u.name == "my_db")
.expect("my_db usage from usefixtures should be detected");
assert!(
!usage.is_parameter,
"usefixtures string usages on functions must have is_parameter = false"
);
}
#[test]
#[timeout(30000)]
fn test_is_parameter_false_for_usefixtures_multiple_args() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def fix_a():
return "a"
@pytest.fixture
def fix_b():
return "b"
@pytest.mark.usefixtures("fix_a", "fix_b")
def test_multi_usefixtures():
pass
"#;
let path = PathBuf::from("/tmp/test_is_param/test_usefixtures_multi.py");
db.analyze_file(path.clone(), content);
let usages = db.usages.get(&path).unwrap();
for name in &["fix_a", "fix_b"] {
let usage = usages
.iter()
.find(|u| u.name == *name)
.unwrap_or_else(|| panic!("{} usage should be detected", name));
assert!(
!usage.is_parameter,
"{} from usefixtures must have is_parameter = false",
name
);
}
}
#[test]
#[timeout(30000)]
fn test_is_parameter_false_for_usefixtures_on_class() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_db() -> str:
return "db"
@pytest.mark.usefixtures("my_db")
class TestSomething:
def test_method(self):
pass
"#;
let path = PathBuf::from("/tmp/test_is_param/test_usefixtures_class.py");
db.analyze_file(path.clone(), content);
let usages = db.usages.get(&path).unwrap();
let usage = usages
.iter()
.find(|u| u.name == "my_db")
.expect("my_db usage from usefixtures on class should be detected");
assert!(
!usage.is_parameter,
"usefixtures string usages on classes must have is_parameter = false"
);
}
#[test]
#[timeout(30000)]
fn test_is_parameter_false_for_pytestmark_usefixtures_assignment() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
pytestmark = pytest.mark.usefixtures("my_db")
@pytest.fixture
def my_db() -> str:
return "db"
def test_something():
pass
"#;
let path = PathBuf::from("/tmp/test_is_param/test_pytestmark_usefixtures.py");
db.analyze_file(path.clone(), content);
let usages = db.usages.get(&path).unwrap();
let usage = usages
.iter()
.find(|u| u.name == "my_db")
.expect("my_db usage from pytestmark should be detected");
assert!(
!usage.is_parameter,
"pytestmark usefixtures string usages must have is_parameter = false"
);
}
#[test]
#[timeout(30000)]
fn test_is_parameter_false_for_pytestmark_usefixtures_list() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
pytestmark = [pytest.mark.usefixtures("fix_a", "fix_b")]
@pytest.fixture
def fix_a():
return "a"
@pytest.fixture
def fix_b():
return "b"
def test_something():
pass
"#;
let path = PathBuf::from("/tmp/test_is_param/test_pytestmark_list.py");
db.analyze_file(path.clone(), content);
let usages = db.usages.get(&path).unwrap();
for name in &["fix_a", "fix_b"] {
let usage = usages
.iter()
.find(|u| u.name == *name)
.unwrap_or_else(|| panic!("{} usage should be detected in pytestmark list", name));
assert!(
!usage.is_parameter,
"{} from pytestmark list must have is_parameter = false",
name
);
}
}
#[test]
#[timeout(30000)]
fn test_is_parameter_false_for_parametrize_indirect() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture(request):
return request.param
@pytest.mark.parametrize("my_fixture", [1, 2], indirect=True)
def test_indirect(my_fixture):
pass
"#;
let path = PathBuf::from("/tmp/test_is_param/test_parametrize_indirect.py");
db.analyze_file(path.clone(), content);
let usages = db.usages.get(&path).unwrap();
let indirect_usage = usages
.iter()
.find(|u| u.name == "my_fixture" && !u.is_parameter);
assert!(
indirect_usage.is_some(),
"my_fixture from parametrize indirect should have is_parameter = false"
);
let param_usage = usages
.iter()
.find(|u| u.name == "my_fixture" && u.is_parameter);
assert!(
param_usage.is_some(),
"my_fixture as a function parameter should have is_parameter = true"
);
}
#[test]
#[timeout(30000)]
fn test_is_parameter_mixed_param_and_marker_usages_in_same_file() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_db() -> str:
return "db"
# Marker usage — string, not a parameter.
@pytest.mark.usefixtures("my_db")
def test_marker_usage():
pass
# Parameter usage — should receive a type annotation.
def test_param_usage(my_db):
pass
"#;
let path = PathBuf::from("/tmp/test_is_param/test_mixed.py");
db.analyze_file(path.clone(), content);
let usages = db.usages.get(&path).unwrap();
let marker_usage = usages
.iter()
.find(|u| u.name == "my_db" && !u.is_parameter)
.expect("marker usage of my_db should have is_parameter = false");
assert!(!marker_usage.is_parameter);
let param_usage = usages
.iter()
.find(|u| u.name == "my_db" && u.is_parameter)
.expect("parameter usage of my_db should have is_parameter = true");
assert!(param_usage.is_parameter);
}
#[test]
#[timeout(30000)]
fn test_pytest_plugins_declaration_detected() {
let db = FixtureDatabase::new();
let conftest_content = r#"
pytest_plugins = ["myapp.fixtures", "other.fixtures"]
import pytest
@pytest.fixture
def local_fixture():
return "local"
"#;
let conftest_path = PathBuf::from("/tmp/test_plugins/conftest.py");
db.analyze_file(conftest_path, conftest_content);
assert!(
db.definitions.contains_key("local_fixture"),
"local_fixture should be detected even with pytest_plugins"
);
}
#[test]
#[timeout(30000)]
fn test_pytest_plugins_tuple_declaration_detected() {
let db = FixtureDatabase::new();
let conftest_content = r#"
pytest_plugins = ("plugin1", "plugin2")
import pytest
@pytest.fixture
def another_fixture():
return "value"
"#;
let conftest_path = PathBuf::from("/tmp/test_plugins/conftest.py");
db.analyze_file(conftest_path, conftest_content);
assert!(
db.definitions.contains_key("another_fixture"),
"another_fixture should be detected"
);
}
#[test]
#[timeout(30000)]
fn test_parametrize_indirect_true() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def my_fixture(request):
return request.param * 2
"#;
let test_content = r#"
import pytest
@pytest.mark.parametrize("my_fixture", [1, 2, 3], indirect=True)
def test_with_indirect(my_fixture):
assert my_fixture in [2, 4, 6]
"#;
let conftest_path = PathBuf::from("/tmp/test_indirect/conftest.py");
let test_path = PathBuf::from("/tmp/test_indirect/test_indirect.py");
db.analyze_file(conftest_path, conftest_content);
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
let fixture_usages: Vec<_> = usages.iter().filter(|u| u.name == "my_fixture").collect();
assert!(
fixture_usages.len() >= 2,
"my_fixture should be used at least twice (indirect + parameter)"
);
}
#[test]
#[timeout(30000)]
fn test_parametrize_indirect_multiple_fixtures() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def fixture_a(request):
return request.param
@pytest.fixture
def fixture_b(request):
return request.param
@pytest.mark.parametrize("fixture_a,fixture_b", [(1, 2), (3, 4)], indirect=True)
def test_multiple_indirect(fixture_a, fixture_b):
pass
"#;
let test_path = PathBuf::from("/tmp/test_indirect/test_multiple.py");
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
assert!(
usages.iter().any(|u| u.name == "fixture_a"),
"fixture_a should be detected as indirect usage"
);
assert!(
usages.iter().any(|u| u.name == "fixture_b"),
"fixture_b should be detected as indirect usage"
);
}
#[test]
#[timeout(30000)]
fn test_parametrize_indirect_list_selective() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def indirect_fix(request):
return request.param
@pytest.fixture
def direct_fix():
return "direct"
@pytest.mark.parametrize("indirect_fix,direct_fix", [(1, 2)], indirect=["indirect_fix"])
def test_selective_indirect(indirect_fix, direct_fix):
pass
"#;
let test_path = PathBuf::from("/tmp/test_indirect/test_selective.py");
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
let indirect_usages: Vec<_> = usages.iter().filter(|u| u.name == "indirect_fix").collect();
assert!(
indirect_usages.len() >= 2,
"indirect_fix should have at least 2 usages (from indirect list + parameter)"
);
}
#[test]
#[timeout(30000)]
fn test_parametrize_without_indirect() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.mark.parametrize("value", [1, 2, 3])
def test_normal_parametrize(value):
pass
"#;
let test_path = PathBuf::from("/tmp/test_indirect/test_normal.py");
db.analyze_file(test_path.clone(), test_content);
let usages = db.usages.get(&test_path).unwrap();
let value_usages: Vec<_> = usages.iter().filter(|u| u.name == "value").collect();
assert_eq!(
value_usages.len(),
1,
"value should only have 1 usage (from parameter, not indirect)"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_scoping_sibling_files() {
let db = FixtureDatabase::new();
let test1_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return "example"
"#;
let test1_path = PathBuf::from("/tmp/test_scope/test_example_2.py");
db.analyze_file(test1_path.clone(), test1_content);
let test2_content = r#"
def test_example_fixture(my_fixture):
assert my_fixture == "example"
"#;
let test2_path = PathBuf::from("/tmp/test_scope/test_example.py");
db.analyze_file(test2_path.clone(), test2_content);
let fixture_defs = db.definitions.get("my_fixture").unwrap();
assert_eq!(fixture_defs.len(), 1);
let fixture_def = &fixture_defs[0];
assert_eq!(fixture_def.file_path, test1_path);
let refs = db.find_references_for_definition(fixture_def);
assert_eq!(
refs.len(),
0,
"Fixture defined in test_example_2.py should have 0 references \
because test_example.py cannot access it (not in conftest.py hierarchy)"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_scoping_with_conftest() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "shared"
"#;
let conftest_path = PathBuf::from("/tmp/test_scope2/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_uses_shared(shared_fixture):
assert shared_fixture == "shared"
"#;
let test_path = PathBuf::from("/tmp/test_scope2/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let fixture_defs = db.definitions.get("shared_fixture").unwrap();
let fixture_def = &fixture_defs[0];
let refs = db.find_references_for_definition(fixture_def);
assert_eq!(
refs.len(),
1,
"Fixture in conftest.py should have 1 reference from sibling test file"
);
assert_eq!(refs[0].file_path, test_path);
}
#[test]
#[timeout(30000)]
fn test_fixture_scoping_same_file() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def local_fixture():
return "local"
def test_uses_local(local_fixture):
assert local_fixture == "local"
"#;
let test_path = PathBuf::from("/tmp/test_scope3/test_local.py");
db.analyze_file(test_path.clone(), test_content);
let fixture_defs = db.definitions.get("local_fixture").unwrap();
let fixture_def = &fixture_defs[0];
let refs = db.find_references_for_definition(fixture_def);
assert_eq!(
refs.len(),
1,
"Fixture defined in same file should have 1 reference"
);
assert_eq!(refs[0].file_path, test_path);
}
#[test]
#[timeout(30000)]
fn test_get_scoped_usage_count() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def global_fixture():
return "global"
"#;
let conftest_path = PathBuf::from("/tmp/test_scope4/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test1_content = r#"
import pytest
@pytest.fixture
def global_fixture():
return "local override"
def test_uses_local(global_fixture):
pass
"#;
let test1_path = PathBuf::from("/tmp/test_scope4/subdir/test_override.py");
db.analyze_file(test1_path.clone(), test1_content);
let test2_content = r#"
def test_uses_global(global_fixture):
pass
"#;
let test2_path = PathBuf::from("/tmp/test_scope4/test_global.py");
db.analyze_file(test2_path.clone(), test2_content);
let conftest_defs = db.definitions.get("global_fixture").unwrap();
let conftest_def = conftest_defs
.iter()
.find(|d| d.file_path == conftest_path)
.unwrap();
let conftest_refs = db.find_references_for_definition(conftest_def);
assert_eq!(
conftest_refs.len(),
1,
"Conftest fixture should have 1 reference (from test_global.py)"
);
assert_eq!(conftest_refs[0].file_path, test2_path);
let local_def = conftest_defs
.iter()
.find(|d| d.file_path == test1_path)
.unwrap();
let local_refs = db.find_references_for_definition(local_def);
assert_eq!(
local_refs.len(),
1,
"Local override fixture should have 1 reference"
);
assert_eq!(local_refs[0].file_path, test1_path);
}
#[test]
#[timeout(30000)]
fn test_completion_context_function_signature() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
def test_something():
pass
"#;
let test_path = PathBuf::from("/tmp/test/test_completion.py");
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 7, 18);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
declared_params,
..
} => {
assert_eq!(function_name, "test_something");
assert!(!is_fixture);
assert!(declared_params.is_empty());
}
_ => panic!("Expected FunctionSignature context"),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_function_signature_with_params() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
def test_something(my_fixture, ):
pass
"#;
let test_path = PathBuf::from("/tmp/test/test_completion.py");
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 7, 31);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
declared_params,
..
} => {
assert_eq!(function_name, "test_something");
assert!(!is_fixture);
assert_eq!(declared_params, vec!["my_fixture"]);
}
_ => panic!("Expected FunctionSignature context"),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_function_body() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
def test_something(my_fixture):
pass
"#;
let test_path = PathBuf::from("/tmp/test/test_completion.py");
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 8, 4);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionBody {
function_name,
is_fixture,
declared_params,
..
} => {
assert_eq!(function_name, "test_something");
assert!(!is_fixture);
assert_eq!(declared_params, vec!["my_fixture"]);
}
_ => panic!("Expected FunctionBody context"),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_fixture_function() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def base_fixture():
return 42
@pytest.fixture
def dependent_fixture():
pass
"#;
let test_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 8, 22);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
declared_params,
..
} => {
assert_eq!(function_name, "dependent_fixture");
assert!(is_fixture);
assert!(declared_params.is_empty());
}
_ => panic!("Expected FunctionSignature context"),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_usefixtures_decorator() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
@pytest.mark.usefixtures("")
def test_something():
pass
"#;
let test_path = PathBuf::from("/tmp/test/test_completion.py");
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 7, 27);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::UsefixturesDecorator => {}
_ => panic!("Expected UsefixturesDecorator context"),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_outside_function() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
# A comment
def test_something():
pass
"#;
let test_path = PathBuf::from("/tmp/test/test_completion.py");
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 3, 5);
assert!(ctx.is_none());
}
#[test]
#[timeout(30000)]
fn test_completion_context_pytestmark_usefixtures_single() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
pytestmark = pytest.mark.usefixtures("")
"#;
let test_path = PathBuf::from("/tmp/test_completion_pytestmark/test_single.py");
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 3, 38);
assert!(
ctx.is_some(),
"Expected a completion context inside pytestmark usefixtures"
);
match ctx.unwrap() {
CompletionContext::UsefixturesDecorator => {}
other => panic!("Expected UsefixturesDecorator, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_pytestmark_usefixtures_in_list() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
pytestmark = [pytest.mark.usefixtures(""), pytest.mark.skip]
"#;
let test_path = PathBuf::from("/tmp/test_completion_pytestmark/test_list.py");
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 3, 39);
assert!(
ctx.is_some(),
"Expected a completion context inside pytestmark list usefixtures"
);
match ctx.unwrap() {
CompletionContext::UsefixturesDecorator => {}
other => panic!("Expected UsefixturesDecorator, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_pytestmark_outside_usefixtures() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
pytestmark = [pytest.mark.skip]
"#;
let test_path = PathBuf::from("/tmp/test_completion_pytestmark/test_outside.py");
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 3, 20);
assert!(
!matches!(
ctx,
Some(pytest_language_server::CompletionContext::UsefixturesDecorator)
),
"Should not return UsefixturesDecorator for non-usefixtures marks"
);
}
#[test]
#[timeout(30000)]
fn test_completion_context_pytestmark_annotated_assignment() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
pytestmark: list = [pytest.mark.usefixtures("")]
"#;
let test_path = PathBuf::from("/tmp/test_completion_pytestmark/test_annotated.py");
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 3, 45);
assert!(
ctx.is_some(),
"Expected completion context inside annotated pytestmark usefixtures"
);
match ctx.unwrap() {
CompletionContext::UsefixturesDecorator => {}
other => panic!("Expected UsefixturesDecorator, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_get_function_param_insertion_info_empty_params() {
let db = FixtureDatabase::new();
let test_content = r#"
def test_something():
pass
"#;
let test_path = PathBuf::from("/tmp/test/test_completion.py");
db.analyze_file(test_path.clone(), test_content);
let info = db.get_function_param_insertion_info(&test_path, 2);
assert!(info.is_some());
let info = info.unwrap();
assert_eq!(info.line, 2);
assert!(!info.needs_comma);
}
#[test]
#[timeout(30000)]
fn test_get_function_param_insertion_info_with_params() {
let db = FixtureDatabase::new();
let test_content = r#"
def test_something(existing_param):
pass
"#;
let test_path = PathBuf::from("/tmp/test/test_completion.py");
db.analyze_file(test_path.clone(), test_content);
let info = db.get_function_param_insertion_info(&test_path, 2);
assert!(info.is_some());
let info = info.unwrap();
assert_eq!(info.line, 2);
assert!(info.needs_comma);
}
#[test]
#[timeout(30000)]
fn test_cycle_detection_simple_cycle() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def fixture_a(fixture_b):
return "a"
@pytest.fixture
def fixture_b(fixture_a):
return "b"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let cycles = db.detect_fixture_cycles();
assert!(!cycles.is_empty(), "Should detect the A->B->A cycle");
let cycle = &cycles[0];
assert!(cycle.cycle_path.contains(&"fixture_a".to_string()));
assert!(cycle.cycle_path.contains(&"fixture_b".to_string()));
}
#[test]
#[timeout(30000)]
fn test_cycle_detection_three_node_cycle() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def fixture_a(fixture_b):
return "a"
@pytest.fixture
def fixture_b(fixture_c):
return "b"
@pytest.fixture
def fixture_c(fixture_a):
return "c"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let cycles = db.detect_fixture_cycles();
assert!(!cycles.is_empty(), "Should detect the A->B->C->A cycle");
let cycle = &cycles[0];
assert!(
cycle.cycle_path.len() >= 3,
"Cycle should have at least 3 nodes"
);
}
#[test]
#[timeout(30000)]
fn test_cycle_detection_no_cycle() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def base_fixture():
return "base"
@pytest.fixture
def dependent_fixture(base_fixture):
return base_fixture + "_dep"
@pytest.fixture
def top_fixture(dependent_fixture):
return dependent_fixture + "_top"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let cycles = db.detect_fixture_cycles();
assert!(cycles.is_empty(), "Should not detect any cycles in a DAG");
}
#[test]
#[timeout(30000)]
fn test_cycle_detection_self_referencing() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture(my_fixture):
return my_fixture + "_modified"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let cycles = db.detect_fixture_cycles();
assert!(
!cycles.is_empty(),
"Should detect self-referencing as a cycle"
);
}
#[test]
#[timeout(30000)]
fn test_cycle_detection_caching() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def fixture_a(fixture_b):
return "a"
@pytest.fixture
def fixture_b(fixture_a):
return "b"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let cycles1 = db.detect_fixture_cycles();
assert!(!cycles1.is_empty());
let cycles2 = db.detect_fixture_cycles();
assert_eq!(cycles1.len(), cycles2.len());
let content2 = r#"
import pytest
@pytest.fixture
def standalone():
return "standalone"
"#;
let path2 = PathBuf::from("/tmp/test/other.py");
db.analyze_file(path2, content2);
let cycles3 = db.detect_fixture_cycles();
assert!(!cycles3.is_empty());
}
#[test]
#[timeout(30000)]
fn test_cycle_detection_in_file() {
let db = FixtureDatabase::new();
let content1 = r#"
import pytest
@pytest.fixture
def fixture_a(fixture_b):
return "a"
@pytest.fixture
def fixture_b(fixture_a):
return "b"
"#;
let content2 = r#"
import pytest
@pytest.fixture
def standalone():
return "standalone"
"#;
let path1 = PathBuf::from("/tmp/test/conftest.py");
let path2 = PathBuf::from("/tmp/test/other.py");
db.analyze_file(path1.clone(), content1);
db.analyze_file(path2.clone(), content2);
let cycles_file1 = db.detect_fixture_cycles_in_file(&path1);
assert!(
!cycles_file1.is_empty(),
"Should find cycles in conftest.py"
);
let cycles_file2 = db.detect_fixture_cycles_in_file(&path2);
assert!(
cycles_file2.is_empty(),
"Should not find cycles in other.py"
);
}
#[test]
#[timeout(30000)]
fn test_cycle_detection_with_external_dependencies() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture(unknown_fixture, another_unknown):
return "my"
@pytest.fixture
def other_fixture(my_fixture):
return "other"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let cycles = db.detect_fixture_cycles();
assert!(
cycles.is_empty(),
"Unknown fixtures should not cause false positive cycles"
);
}
#[test]
#[timeout(30000)]
fn test_cycle_detection_multiple_independent_cycles() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
# Cycle 1: a -> b -> a
@pytest.fixture
def cycle1_a(cycle1_b):
return "1a"
@pytest.fixture
def cycle1_b(cycle1_a):
return "1b"
# Cycle 2: x -> y -> z -> x
@pytest.fixture
def cycle2_x(cycle2_y):
return "2x"
@pytest.fixture
def cycle2_y(cycle2_z):
return "2y"
@pytest.fixture
def cycle2_z(cycle2_x):
return "2z"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let cycles = db.detect_fixture_cycles();
assert!(
cycles.len() >= 2,
"Should detect multiple independent cycles, got {}",
cycles.len()
);
}
#[test]
#[timeout(30000)]
fn test_scope_mismatch_session_depends_on_function() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def function_fixture():
return "function"
@pytest.fixture(scope="session")
def session_fixture(function_fixture):
return function_fixture + "_session"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let mismatches = db.detect_scope_mismatches_in_file(&path);
assert_eq!(
mismatches.len(),
1,
"Should detect session->function scope mismatch"
);
let mismatch = &mismatches[0];
assert_eq!(mismatch.fixture.name, "session_fixture");
assert_eq!(mismatch.dependency.name, "function_fixture");
}
#[test]
#[timeout(30000)]
fn test_scope_mismatch_module_depends_on_function() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def func_fixture():
return "func"
@pytest.fixture(scope="module")
def mod_fixture(func_fixture):
return func_fixture + "_mod"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let mismatches = db.detect_scope_mismatches_in_file(&path);
assert_eq!(
mismatches.len(),
1,
"Should detect module->function scope mismatch"
);
}
#[test]
#[timeout(30000)]
fn test_scope_no_mismatch_valid_hierarchy() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(scope="session")
def session_fixture():
return "session"
@pytest.fixture(scope="module")
def module_fixture(session_fixture):
return session_fixture + "_module"
@pytest.fixture
def function_fixture(module_fixture):
return module_fixture + "_function"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let mismatches = db.detect_scope_mismatches_in_file(&path);
assert!(
mismatches.is_empty(),
"Should not detect mismatches in valid hierarchy"
);
}
#[test]
#[timeout(30000)]
fn test_scope_same_scope_no_mismatch() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(scope="module")
def mod_fixture_a():
return "a"
@pytest.fixture(scope="module")
def mod_fixture_b(mod_fixture_a):
return mod_fixture_a + "_b"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let mismatches = db.detect_scope_mismatches_in_file(&path);
assert!(mismatches.is_empty(), "Same scope should not be a mismatch");
}
#[test]
#[timeout(30000)]
fn test_scope_class_depends_on_function() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def func_fixture():
return "func"
@pytest.fixture(scope="class")
def class_fixture(func_fixture):
return func_fixture + "_class"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let mismatches = db.detect_scope_mismatches_in_file(&path);
assert_eq!(
mismatches.len(),
1,
"Should detect class->function scope mismatch"
);
}
#[test]
#[timeout(30000)]
fn test_scope_package_depends_on_module() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(scope="module")
def mod_fixture():
return "module"
@pytest.fixture(scope="package")
def pkg_fixture(mod_fixture):
return mod_fixture + "_pkg"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let mismatches = db.detect_scope_mismatches_in_file(&path);
assert_eq!(
mismatches.len(),
1,
"Should detect package->module scope mismatch"
);
}
#[test]
#[timeout(30000)]
fn test_scope_multiple_mismatches() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def func_a():
return "a"
@pytest.fixture
def func_b():
return "b"
@pytest.fixture(scope="session")
def session_fixture(func_a, func_b):
return func_a + func_b
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let mismatches = db.detect_scope_mismatches_in_file(&path);
assert_eq!(mismatches.len(), 2, "Should detect two scope mismatches");
}
#[test]
#[timeout(30000)]
fn test_scope_default_is_function() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def default_fixture():
return "default"
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let defs = db.definitions.get("default_fixture").unwrap();
assert_eq!(
defs[0].scope,
pytest_language_server::FixtureScope::Function
);
}
#[test]
#[timeout(30000)]
fn test_scope_extraction_all_scopes() {
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(scope="function")
def func_fix():
pass
@pytest.fixture(scope="class")
def class_fix():
pass
@pytest.fixture(scope="module")
def mod_fix():
pass
@pytest.fixture(scope="package")
def pkg_fix():
pass
@pytest.fixture(scope="session")
def session_fix():
pass
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
assert_eq!(
db.definitions.get("func_fix").unwrap()[0].scope,
FixtureScope::Function
);
assert_eq!(
db.definitions.get("class_fix").unwrap()[0].scope,
FixtureScope::Class
);
assert_eq!(
db.definitions.get("mod_fix").unwrap()[0].scope,
FixtureScope::Module
);
assert_eq!(
db.definitions.get("pkg_fix").unwrap()[0].scope,
FixtureScope::Package
);
assert_eq!(
db.definitions.get("session_fix").unwrap()[0].scope,
FixtureScope::Session
);
}
#[test]
#[timeout(30000)]
fn test_assignment_fixture_default_scope_is_function() {
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let content = r#"
import pytest
def _mocker():
pass
mocker = pytest.fixture()(_mocker)
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
assert!(
db.definitions.contains_key("mocker"),
"mocker fixture should be detected"
);
assert_eq!(
db.definitions.get("mocker").unwrap()[0].scope,
FixtureScope::Function,
"mocker with no scope argument should default to function scope"
);
}
#[test]
#[timeout(30000)]
fn test_assignment_fixture_class_scope() {
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let content = r#"
import pytest
def _mocker():
pass
class_mocker = pytest.fixture(scope="class")(_mocker)
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
assert!(
db.definitions.contains_key("class_mocker"),
"class_mocker fixture should be detected"
);
assert_eq!(
db.definitions.get("class_mocker").unwrap()[0].scope,
FixtureScope::Class,
"class_mocker should have class scope"
);
}
#[test]
#[timeout(30000)]
fn test_assignment_fixture_module_scope() {
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let content = r#"
import pytest
def _mocker():
pass
module_mocker = pytest.fixture(scope="module")(_mocker)
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
assert!(
db.definitions.contains_key("module_mocker"),
"module_mocker fixture should be detected"
);
assert_eq!(
db.definitions.get("module_mocker").unwrap()[0].scope,
FixtureScope::Module,
"module_mocker should have module scope"
);
}
#[test]
#[timeout(30000)]
fn test_assignment_fixture_package_scope() {
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let content = r#"
import pytest
def _mocker():
pass
package_mocker = pytest.fixture(scope="package")(_mocker)
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
assert!(
db.definitions.contains_key("package_mocker"),
"package_mocker fixture should be detected"
);
assert_eq!(
db.definitions.get("package_mocker").unwrap()[0].scope,
FixtureScope::Package,
"package_mocker should have package scope"
);
}
#[test]
#[timeout(30000)]
fn test_assignment_fixture_session_scope() {
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let content = r#"
import pytest
def _mocker():
pass
session_mocker = pytest.fixture(scope="session")(_mocker)
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
assert!(
db.definitions.contains_key("session_mocker"),
"session_mocker fixture should be detected"
);
assert_eq!(
db.definitions.get("session_mocker").unwrap()[0].scope,
FixtureScope::Session,
"session_mocker should have session scope"
);
}
#[test]
#[timeout(30000)]
fn test_assignment_fixture_all_scopes_together() {
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let content = r#"
import pytest
def _mocker():
pass
mocker = pytest.fixture()(_mocker)
class_mocker = pytest.fixture(scope="class")(_mocker)
module_mocker = pytest.fixture(scope="module")(_mocker)
package_mocker = pytest.fixture(scope="package")(_mocker)
session_mocker = pytest.fixture(scope="session")(_mocker)
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
assert_eq!(
db.definitions.get("mocker").unwrap()[0].scope,
FixtureScope::Function,
"mocker (no scope arg) should be function scope"
);
assert_eq!(
db.definitions.get("class_mocker").unwrap()[0].scope,
FixtureScope::Class,
"class_mocker should be class scope"
);
assert_eq!(
db.definitions.get("module_mocker").unwrap()[0].scope,
FixtureScope::Module,
"module_mocker should be module scope"
);
assert_eq!(
db.definitions.get("package_mocker").unwrap()[0].scope,
FixtureScope::Package,
"package_mocker should be package scope"
);
assert_eq!(
db.definitions.get("session_mocker").unwrap()[0].scope,
FixtureScope::Session,
"session_mocker should be session scope"
);
}
#[test]
#[timeout(30000)]
fn test_yield_line_simple_generator_fixture() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def db_connection():
conn = connect()
yield conn
conn.close()
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let fixture = &db.definitions.get("db_connection").unwrap()[0];
assert_eq!(fixture.yield_line, Some(7));
}
#[test]
#[timeout(30000)]
fn test_yield_line_no_yield() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def simple_fixture():
return 42
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let fixture = &db.definitions.get("simple_fixture").unwrap()[0];
assert_eq!(fixture.yield_line, None);
}
#[test]
#[timeout(30000)]
fn test_yield_line_in_with_block() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def resource():
with open("/tmp/file") as f:
yield f
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let fixture = &db.definitions.get("resource").unwrap()[0];
assert_eq!(fixture.yield_line, Some(7));
}
#[test]
#[timeout(30000)]
fn test_yield_line_in_try_block() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def safe_resource():
try:
resource = create()
yield resource
finally:
cleanup()
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let fixture = &db.definitions.get("safe_resource").unwrap()[0];
assert_eq!(fixture.yield_line, Some(8));
}
#[test]
#[timeout(30000)]
fn test_yield_line_in_if_block() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def conditional_fixture():
if True:
yield 42
else:
yield 0
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let fixture = &db.definitions.get("conditional_fixture").unwrap()[0];
assert_eq!(fixture.yield_line, Some(7));
}
#[test]
#[timeout(30000)]
fn test_find_containing_function_simple() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
def test_something(my_fixture):
assert my_fixture == 42
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path.clone(), content);
assert_eq!(
db.find_containing_function(&path, 9),
Some("test_something".to_string())
);
assert_eq!(
db.find_containing_function(&path, 8),
Some("test_something".to_string())
);
assert_eq!(
db.find_containing_function(&path, 6),
Some("my_fixture".to_string())
);
assert_eq!(db.find_containing_function(&path, 10), None);
}
#[test]
#[timeout(30000)]
fn test_resolve_fixture_for_file_same_file() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "parent"
"#;
let test_content = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "local"
def test_it(shared_fixture):
pass
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "shared_fixture");
assert!(resolved.is_some());
assert_eq!(resolved.unwrap().file_path, test_path);
}
#[test]
#[timeout(30000)]
fn test_resolve_fixture_for_file_conftest() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def parent_fixture():
return "parent"
"#;
let test_content = r#"
def test_it(parent_fixture):
pass
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "parent_fixture");
assert!(resolved.is_some());
assert_eq!(resolved.unwrap().file_path, conftest_path);
}
#[test]
#[timeout(30000)]
fn test_star_import_fixtures_are_resolved() {
let db = FixtureDatabase::new();
let fixture_module_content = r#"
import pytest
@pytest.fixture
def imported_fixture():
return "imported_value"
@pytest.fixture
def another_imported_fixture():
return 42
"#;
let conftest_content = r#"
from .fixture_module import *
import pytest
@pytest.fixture
def local_fixture():
return "local_value"
"#;
let test_content = r#"
def test_uses_imported(imported_fixture, local_fixture):
assert imported_fixture == "imported_value"
assert local_fixture == "local_value"
"#;
let fixture_module_path = PathBuf::from("/tmp/test_import/fixture_module.py");
let conftest_path = PathBuf::from("/tmp/test_import/conftest.py");
let test_path = PathBuf::from("/tmp/test_import/test_example.py");
db.analyze_file(fixture_module_path.clone(), fixture_module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "imported_fixture");
assert!(
resolved.is_some(),
"imported_fixture should be resolvable via conftest star import"
);
let def = resolved.unwrap();
assert_eq!(def.name, "imported_fixture");
assert_eq!(def.file_path, fixture_module_path);
}
#[test]
#[timeout(30000)]
fn test_explicit_import_fixtures_are_resolved() {
let db = FixtureDatabase::new();
let fixture_module_content = r#"
import pytest
@pytest.fixture
def explicitly_imported():
return "explicit"
@pytest.fixture
def not_imported():
return "should not be available"
"#;
let conftest_content = r#"
from .fixture_module import explicitly_imported
import pytest
@pytest.fixture
def local_fixture():
return "local"
"#;
let test_content = r#"
def test_uses_explicit(explicitly_imported, local_fixture):
pass
"#;
let fixture_module_path = PathBuf::from("/tmp/test_explicit/fixture_module.py");
let conftest_path = PathBuf::from("/tmp/test_explicit/conftest.py");
let test_path = PathBuf::from("/tmp/test_explicit/test_example.py");
db.analyze_file(fixture_module_path.clone(), fixture_module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "explicitly_imported");
assert!(
resolved.is_some(),
"explicitly_imported should be resolvable via explicit import"
);
}
#[test]
#[timeout(30000)]
fn test_circular_import_handling() {
let db = FixtureDatabase::new();
let module_a_content = r#"
from .module_b import *
import pytest
@pytest.fixture
def fixture_a():
return "a"
"#;
let module_b_content = r#"
from .module_a import *
import pytest
@pytest.fixture
def fixture_b():
return "b"
"#;
let module_a_path = PathBuf::from("/tmp/test_circular/module_a.py");
let module_b_path = PathBuf::from("/tmp/test_circular/module_b.py");
db.analyze_file(module_a_path.clone(), module_a_content);
db.analyze_file(module_b_path.clone(), module_b_content);
use std::collections::HashSet;
let mut visited = HashSet::new();
let _imported_a = db.get_imported_fixtures(&module_a_path, &mut visited);
assert!(visited.len() <= 2, "Should have visited at most 2 modules");
}
#[test]
#[timeout(30000)]
fn test_transitive_imports() {
let db = FixtureDatabase::new();
let module_c_content = r#"
import pytest
@pytest.fixture
def deep_fixture():
return "deep"
"#;
let module_b_content = r#"
from .module_c import *
import pytest
@pytest.fixture
def mid_fixture():
return "mid"
"#;
let conftest_content = r#"
from .module_b import *
import pytest
@pytest.fixture
def local_fixture():
return "local"
"#;
let test_content = r#"
def test_uses_deep(deep_fixture, mid_fixture, local_fixture):
pass
"#;
let module_c_path = PathBuf::from("/tmp/test_transitive/module_c.py");
let module_b_path = PathBuf::from("/tmp/test_transitive/module_b.py");
let conftest_path = PathBuf::from("/tmp/test_transitive/conftest.py");
let test_path = PathBuf::from("/tmp/test_transitive/test_example.py");
db.analyze_file(module_c_path.clone(), module_c_content);
db.analyze_file(module_b_path.clone(), module_b_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "deep_fixture");
assert!(
resolved.is_some(),
"deep_fixture should be resolvable via transitive imports (C -> B -> conftest)"
);
assert_eq!(resolved.unwrap().file_path, module_c_path);
}
#[test]
#[timeout(30000)]
fn test_available_fixtures_includes_imported() {
let db = FixtureDatabase::new();
let fixture_module_content = r#"
import pytest
@pytest.fixture
def module_fixture():
return "from module"
"#;
let conftest_content = r#"
from .fixture_module import *
import pytest
@pytest.fixture
def conftest_fixture():
return "from conftest"
"#;
let test_content = r#"
def test_something():
pass
"#;
let fixture_module_path = PathBuf::from("/tmp/test_available/fixture_module.py");
let conftest_path = PathBuf::from("/tmp/test_available/conftest.py");
let test_path = PathBuf::from("/tmp/test_available/test_example.py");
db.analyze_file(fixture_module_path.clone(), fixture_module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let available = db.get_available_fixtures(&test_path);
let names: Vec<&str> = available.iter().map(|f| f.name.as_str()).collect();
assert!(
names.contains(&"conftest_fixture"),
"conftest_fixture should be in available fixtures"
);
assert!(
names.contains(&"module_fixture"),
"module_fixture should be in available fixtures (via import)"
);
}
#[test]
#[timeout(30000)]
fn test_find_definition_for_imported_fixture() {
let db = FixtureDatabase::new();
let fixture_module_content = r#"
import pytest
@pytest.fixture
def imported_fixture():
return "imported"
"#;
let conftest_content = r#"
from .fixture_module import *
"#;
let test_content = r#"
def test_uses_imported(imported_fixture):
pass
"#;
let fixture_module_path = PathBuf::from("/tmp/test_def/fixture_module.py");
let conftest_path = PathBuf::from("/tmp/test_def/conftest.py");
let test_path = PathBuf::from("/tmp/test_def/test_example.py");
db.analyze_file(fixture_module_path.clone(), fixture_module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let definition = db.find_fixture_definition(&test_path, 1, 24);
assert!(
definition.is_some(),
"Should find definition for imported_fixture from test file"
);
let def = definition.unwrap();
assert_eq!(def.name, "imported_fixture");
assert_eq!(
def.file_path, fixture_module_path,
"Definition should be in fixture_module.py, not conftest.py"
);
}
#[test]
#[timeout(30000)]
fn test_find_references_for_imported_fixture() {
let db = FixtureDatabase::new();
let fixture_module_content = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "shared"
"#;
let conftest_content = r#"
from .fixture_module import *
"#;
let test_content = r#"
def test_one(shared_fixture):
pass
def test_two(shared_fixture):
pass
"#;
let fixture_module_path = PathBuf::from("/tmp/test_refs/fixture_module.py");
let conftest_path = PathBuf::from("/tmp/test_refs/conftest.py");
let test_path = PathBuf::from("/tmp/test_refs/test_example.py");
db.analyze_file(fixture_module_path.clone(), fixture_module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let references = db.find_fixture_references("shared_fixture");
assert_eq!(
references.len(),
2,
"Should find 2 references to shared_fixture"
);
}
#[test]
#[timeout(30000)]
fn test_multi_level_relative_import() {
let db = FixtureDatabase::new();
let utils_fixtures_content = r#"
import pytest
@pytest.fixture
def util_fixture():
return "from utils"
"#;
let conftest_content = r#"
from ..utils.fixtures import *
"#;
let test_content = r#"
def test_uses_util(util_fixture):
pass
"#;
let utils_fixtures_path = PathBuf::from("/tmp/test_multi_level/tests/utils/fixtures.py");
let conftest_path = PathBuf::from("/tmp/test_multi_level/tests/subdir/conftest.py");
let test_path = PathBuf::from("/tmp/test_multi_level/tests/subdir/test_example.py");
db.analyze_file(utils_fixtures_path.clone(), utils_fixtures_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "util_fixture");
assert!(
resolved.is_some(),
"util_fixture should be resolvable via multi-level relative import"
);
assert_eq!(resolved.unwrap().file_path, utils_fixtures_path);
}
#[test]
#[timeout(30000)]
fn test_mixed_star_and_explicit_imports() {
let db = FixtureDatabase::new();
let module_a_content = r#"
import pytest
@pytest.fixture
def fixture_a():
return "a"
"#;
let module_b_content = r#"
import pytest
@pytest.fixture
def fixture_b():
return "b"
@pytest.fixture
def fixture_b2():
return "b2"
"#;
let conftest_content = r#"
from .module_a import *
from .module_b import fixture_b # Only import fixture_b, not fixture_b2
import pytest
@pytest.fixture
def local_fixture():
return "local"
"#;
let test_content = r#"
def test_uses_all(fixture_a, fixture_b, local_fixture):
pass
"#;
let module_a_path = PathBuf::from("/tmp/test_mixed/module_a.py");
let module_b_path = PathBuf::from("/tmp/test_mixed/module_b.py");
let conftest_path = PathBuf::from("/tmp/test_mixed/conftest.py");
let test_path = PathBuf::from("/tmp/test_mixed/test_example.py");
db.analyze_file(module_a_path.clone(), module_a_content);
db.analyze_file(module_b_path.clone(), module_b_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved_a = db.resolve_fixture_for_file(&test_path, "fixture_a");
assert!(resolved_a.is_some(), "fixture_a should be available");
assert_eq!(resolved_a.unwrap().file_path, module_a_path);
let resolved_b = db.resolve_fixture_for_file(&test_path, "fixture_b");
assert!(resolved_b.is_some(), "fixture_b should be available");
assert_eq!(resolved_b.unwrap().file_path, module_b_path);
}
#[test]
#[timeout(30000)]
fn test_import_with_alias() {
let db = FixtureDatabase::new();
let module_content = r#"
import pytest
@pytest.fixture
def original_name():
return "original"
"#;
let conftest_content = r#"
from .module import original_name as aliased_fixture
"#;
let test_content = r#"
def test_uses_alias(aliased_fixture):
pass
"#;
let module_path = PathBuf::from("/tmp/test_alias/module.py");
let conftest_path = PathBuf::from("/tmp/test_alias/conftest.py");
let test_path = PathBuf::from("/tmp/test_alias/test_example.py");
db.analyze_file(module_path.clone(), module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "aliased_fixture");
if resolved.is_some() {
assert_eq!(
resolved.unwrap().name,
"original_name",
"Aliased import should resolve to original fixture"
);
}
}
#[test]
#[timeout(30000)]
fn test_nested_package_imports() {
let db = FixtureDatabase::new();
let nested_module_content = r#"
import pytest
@pytest.fixture
def nested_fixture():
return "nested"
"#;
let conftest_content = r#"
from .subpackage.module import *
"#;
let test_content = r#"
def test_uses_nested(nested_fixture):
pass
"#;
let nested_path = PathBuf::from("/tmp/test_nested/tests/subpackage/module.py");
let conftest_path = PathBuf::from("/tmp/test_nested/tests/conftest.py");
let test_path = PathBuf::from("/tmp/test_nested/tests/test_example.py");
db.analyze_file(nested_path.clone(), nested_module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "nested_fixture");
assert!(
resolved.is_some(),
"nested_fixture should be resolvable via nested package import"
);
assert_eq!(resolved.unwrap().file_path, nested_path);
}
#[test]
#[timeout(30000)]
fn test_imported_fixture_with_dependencies() {
let db = FixtureDatabase::new();
let module_content = r#"
import pytest
@pytest.fixture
def base_fixture():
return "base"
@pytest.fixture
def dependent_fixture(base_fixture):
return f"depends on {base_fixture}"
"#;
let conftest_content = r#"
from .module import *
"#;
let test_content = r#"
def test_uses_dependent(dependent_fixture):
pass
"#;
let module_path = PathBuf::from("/tmp/test_deps/module.py");
let conftest_path = PathBuf::from("/tmp/test_deps/conftest.py");
let test_path = PathBuf::from("/tmp/test_deps/test_example.py");
db.analyze_file(module_path.clone(), module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved_base = db.resolve_fixture_for_file(&test_path, "base_fixture");
let resolved_dependent = db.resolve_fixture_for_file(&test_path, "dependent_fixture");
assert!(resolved_base.is_some(), "base_fixture should be resolvable");
assert!(
resolved_dependent.is_some(),
"dependent_fixture should be resolvable"
);
let dep_def = resolved_dependent.unwrap();
assert!(
dep_def.dependencies.contains(&"base_fixture".to_string()),
"dependent_fixture should list base_fixture as a dependency"
);
}
#[test]
#[timeout(30000)]
fn test_import_from_init_py() {
let db = FixtureDatabase::new();
let init_content = r#"
import pytest
@pytest.fixture
def package_fixture():
return "from package init"
"#;
let conftest_content = r#"
from .fixtures import *
"#;
let test_content = r#"
def test_uses_package_fixture(package_fixture):
pass
"#;
let init_path = PathBuf::from("/tmp/test_init/tests/fixtures/__init__.py");
let conftest_path = PathBuf::from("/tmp/test_init/tests/conftest.py");
let test_path = PathBuf::from("/tmp/test_init/tests/test_example.py");
db.analyze_file(init_path.clone(), init_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "package_fixture");
assert!(
resolved.is_some(),
"package_fixture should be resolvable from __init__.py"
);
assert_eq!(resolved.unwrap().file_path, init_path);
}
#[test]
#[timeout(30000)]
fn test_shadowed_imported_fixture() {
let db = FixtureDatabase::new();
let module_content = r#"
import pytest
@pytest.fixture
def shared_name():
return "from module"
"#;
let conftest_content = r#"
from .module import *
import pytest
@pytest.fixture
def shared_name():
return "from conftest"
"#;
let test_content = r#"
def test_uses_shared(shared_name):
pass
"#;
let module_path = PathBuf::from("/tmp/test_shadow/module.py");
let conftest_path = PathBuf::from("/tmp/test_shadow/conftest.py");
let test_path = PathBuf::from("/tmp/test_shadow/test_example.py");
db.analyze_file(module_path.clone(), module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "shared_name");
assert!(resolved.is_some(), "shared_name should be resolvable");
assert_eq!(
resolved.unwrap().file_path,
conftest_path,
"Local conftest fixture should shadow imported fixture"
);
}
#[test]
#[timeout(30000)]
fn test_import_in_test_file() {
let db = FixtureDatabase::new();
let module_content = r#"
import pytest
@pytest.fixture
def module_fixture():
return "from module"
"#;
let test_content = r#"
from .fixture_module import *
def test_uses_imported(module_fixture):
pass
"#;
let module_path = PathBuf::from("/tmp/test_in_test/fixture_module.py");
let test_path = PathBuf::from("/tmp/test_in_test/test_example.py");
db.analyze_file(module_path.clone(), module_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "module_fixture");
if resolved.is_some() {
assert_eq!(resolved.unwrap().file_path, module_path);
}
}
#[test]
#[timeout(30000)]
fn test_conftest_hierarchy_with_imports() {
let db = FixtureDatabase::new();
let module_content = r#"
import pytest
@pytest.fixture
def parent_imported():
return "imported in parent"
"#;
let parent_conftest_content = r#"
from .fixture_module import *
import pytest
@pytest.fixture
def parent_local():
return "local in parent"
"#;
let child_conftest_content = r#"
import pytest
@pytest.fixture
def child_local():
return "local in child"
"#;
let test_content = r#"
def test_all_available(parent_imported, parent_local, child_local):
pass
"#;
let module_path = PathBuf::from("/tmp/test_hier/tests/fixture_module.py");
let parent_conftest_path = PathBuf::from("/tmp/test_hier/tests/conftest.py");
let child_conftest_path = PathBuf::from("/tmp/test_hier/tests/subdir/conftest.py");
let test_path = PathBuf::from("/tmp/test_hier/tests/subdir/test_example.py");
db.analyze_file(module_path.clone(), module_content);
db.analyze_file(parent_conftest_path.clone(), parent_conftest_content);
db.analyze_file(child_conftest_path.clone(), child_conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved_imported = db.resolve_fixture_for_file(&test_path, "parent_imported");
let resolved_parent = db.resolve_fixture_for_file(&test_path, "parent_local");
let resolved_child = db.resolve_fixture_for_file(&test_path, "child_local");
assert!(
resolved_imported.is_some(),
"parent_imported should be resolvable from child test"
);
assert!(
resolved_parent.is_some(),
"parent_local should be resolvable from child test"
);
assert!(
resolved_child.is_some(),
"child_local should be resolvable from child test"
);
assert_eq!(resolved_imported.unwrap().file_path, module_path);
assert_eq!(resolved_parent.unwrap().file_path, parent_conftest_path);
assert_eq!(resolved_child.unwrap().file_path, child_conftest_path);
}
#[test]
#[timeout(30000)]
fn test_diagnostics_for_imported_fixtures() {
let db = FixtureDatabase::new();
let module_content = r#"
import pytest
@pytest.fixture
def imported_fixture():
return "imported"
"#;
let conftest_content = r#"
from .fixture_module import *
"#;
let test_content = r#"
def test_uses_imported(imported_fixture):
pass
"#;
let module_path = PathBuf::from("/tmp/test_diag/fixture_module.py");
let conftest_path = PathBuf::from("/tmp/test_diag/conftest.py");
let test_path = PathBuf::from("/tmp/test_diag/test_example.py");
db.analyze_file(module_path.clone(), module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let undeclared = db.get_undeclared_fixtures(&test_path);
let undeclared_names: Vec<&str> = undeclared.iter().map(|u| u.name.as_str()).collect();
assert!(
!undeclared_names.contains(&"imported_fixture"),
"imported_fixture should not be flagged as undeclared"
);
}
#[test]
#[timeout(30000)]
fn test_completion_includes_imported_fixtures() {
let db = FixtureDatabase::new();
let module_content = r#"
import pytest
@pytest.fixture
def completion_fixture():
return "for completion"
"#;
let conftest_content = r#"
from .fixture_module import *
import pytest
@pytest.fixture
def local_completion():
return "local"
"#;
let test_content = r#"
def test_something():
pass
"#;
let module_path = PathBuf::from("/tmp/test_compl/fixture_module.py");
let conftest_path = PathBuf::from("/tmp/test_compl/conftest.py");
let test_path = PathBuf::from("/tmp/test_compl/test_example.py");
db.analyze_file(module_path.clone(), module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let available = db.get_available_fixtures(&test_path);
let names: Vec<&str> = available.iter().map(|f| f.name.as_str()).collect();
assert!(
names.contains(&"completion_fixture"),
"completion_fixture should be in available fixtures for completion"
);
assert!(
names.contains(&"local_completion"),
"local_completion should be in available fixtures for completion"
);
}
#[test]
#[timeout(30000)]
fn test_empty_module_import() {
let db = FixtureDatabase::new();
let empty_module_content = r#"
# This module has no fixtures
def helper():
return "helper"
"#;
let conftest_content = r#"
from .empty_module import *
import pytest
@pytest.fixture
def local_fixture():
return "local"
"#;
let test_content = r#"
def test_something(local_fixture):
pass
"#;
let empty_module_path = PathBuf::from("/tmp/test_empty/empty_module.py");
let conftest_path = PathBuf::from("/tmp/test_empty/conftest.py");
let test_path = PathBuf::from("/tmp/test_empty/test_example.py");
db.analyze_file(empty_module_path.clone(), empty_module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "local_fixture");
assert!(
resolved.is_some(),
"local_fixture should still be resolvable"
);
}
#[test]
#[timeout(30000)]
fn test_parse_error_keeps_previous_data() {
let db = FixtureDatabase::new();
let valid_content = r#"
import pytest
@pytest.fixture
def my_fixture():
return "test value"
"#;
let path = PathBuf::from("/tmp/test_parse_error/conftest.py");
db.analyze_file(path.clone(), valid_content);
assert!(
db.definitions.contains_key("my_fixture"),
"my_fixture should be detected initially"
);
let invalid_content = r#"
import pytest
@pytest.fixture
def my_fixture(
# Missing closing parenthesis - syntax error
return "test value"
"#;
db.analyze_file(path.clone(), invalid_content);
assert!(
db.definitions.contains_key("my_fixture"),
"my_fixture should still be present after parse error"
);
}
#[test]
#[timeout(30000)]
fn test_fixture_end_line_tracking() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def short_fixture():
return 1
@pytest.fixture
def multiline_fixture():
x = 1
y = 2
z = 3
return x + y + z
"#;
let path = PathBuf::from("/tmp/test_end_line/conftest.py");
db.analyze_file(path.clone(), content);
let short_fixture = db.definitions.get("short_fixture").unwrap();
assert_eq!(short_fixture[0].line, 5); assert!(
short_fixture[0].end_line >= short_fixture[0].line,
"end_line should be >= line"
);
assert!(
short_fixture[0].end_line <= 7,
"short_fixture end_line should be around line 6-7"
);
let multiline_fixture = db.definitions.get("multiline_fixture").unwrap();
assert_eq!(multiline_fixture[0].line, 9); assert!(
multiline_fixture[0].end_line >= 13,
"multiline_fixture end_line should be at least line 13"
);
}
#[test]
#[timeout(30000)]
fn test_multi_level_relative_import_levels() {
let db = FixtureDatabase::new();
let single_dot_conftest = r#"
from .fixtures import *
"#;
let double_dot_conftest = r#"
from ..shared.fixtures import *
"#;
let triple_dot_conftest = r#"
from ...common.fixtures import *
"#;
let path1 = PathBuf::from("/tmp/test_levels/pkg/subpkg/conftest.py");
let path2 = PathBuf::from("/tmp/test_levels/pkg/subpkg/deep/conftest.py");
let path3 = PathBuf::from("/tmp/test_levels/pkg/subpkg/deep/deeper/conftest.py");
db.analyze_file(path1.clone(), single_dot_conftest);
db.analyze_file(path2.clone(), double_dot_conftest);
db.analyze_file(path3.clone(), triple_dot_conftest);
assert!(
db.file_cache.contains_key(&path1),
"single dot import file should be cached"
);
assert!(
db.file_cache.contains_key(&path2),
"double dot import file should be cached"
);
assert!(
db.file_cache.contains_key(&path3),
"triple dot import file should be cached"
);
}
#[test]
#[timeout(30000)]
fn test_pytest_plugins_single_string() {
let db = FixtureDatabase::new();
let fixture_module_content = r#"
import pytest
@pytest.fixture
def plugin_fixture():
return "from plugin"
"#;
let conftest_content = r#"
pytest_plugins = "fixture_module"
"#;
let test_content = r#"
def test_uses_plugin(plugin_fixture):
pass
"#;
let fixture_module_path = PathBuf::from("/tmp/test_pytest_plugins/fixture_module.py");
let conftest_path = PathBuf::from("/tmp/test_pytest_plugins/conftest.py");
let test_path = PathBuf::from("/tmp/test_pytest_plugins/test_example.py");
db.analyze_file(fixture_module_path.clone(), fixture_module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "plugin_fixture");
assert!(
resolved.is_some(),
"plugin_fixture should be resolvable via pytest_plugins single string"
);
assert_eq!(resolved.unwrap().file_path, fixture_module_path);
}
#[test]
#[timeout(30000)]
fn test_pytest_plugins_list_syntax() {
let db = FixtureDatabase::new();
let module_a_content = r#"
import pytest
@pytest.fixture
def fixture_a():
return "a"
"#;
let module_b_content = r#"
import pytest
@pytest.fixture
def fixture_b():
return "b"
"#;
let conftest_content = r#"
pytest_plugins = ["module_a", "module_b"]
"#;
let test_content = r#"
def test_uses_both(fixture_a, fixture_b):
pass
"#;
let module_a_path = PathBuf::from("/tmp/test_pp_list/module_a.py");
let module_b_path = PathBuf::from("/tmp/test_pp_list/module_b.py");
let conftest_path = PathBuf::from("/tmp/test_pp_list/conftest.py");
let test_path = PathBuf::from("/tmp/test_pp_list/test_example.py");
db.analyze_file(module_a_path.clone(), module_a_content);
db.analyze_file(module_b_path.clone(), module_b_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved_a = db.resolve_fixture_for_file(&test_path, "fixture_a");
assert!(
resolved_a.is_some(),
"fixture_a should be resolvable via pytest_plugins list"
);
assert_eq!(resolved_a.unwrap().file_path, module_a_path);
let resolved_b = db.resolve_fixture_for_file(&test_path, "fixture_b");
assert!(
resolved_b.is_some(),
"fixture_b should be resolvable via pytest_plugins list"
);
assert_eq!(resolved_b.unwrap().file_path, module_b_path);
}
#[test]
#[timeout(30000)]
fn test_pytest_plugins_tuple_resolution() {
let db = FixtureDatabase::new();
let module_content = r#"
import pytest
@pytest.fixture
def tuple_fixture():
return "from tuple"
"#;
let conftest_content = r#"
pytest_plugins = ("fixture_module",)
"#;
let test_content = r#"
def test_uses_tuple(tuple_fixture):
pass
"#;
let fixture_module_path = PathBuf::from("/tmp/test_pp_tuple/fixture_module.py");
let conftest_path = PathBuf::from("/tmp/test_pp_tuple/conftest.py");
let test_path = PathBuf::from("/tmp/test_pp_tuple/test_example.py");
db.analyze_file(fixture_module_path.clone(), module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "tuple_fixture");
assert!(
resolved.is_some(),
"tuple_fixture should be resolvable via pytest_plugins tuple"
);
assert_eq!(resolved.unwrap().file_path, fixture_module_path);
}
#[test]
#[timeout(30000)]
fn test_pytest_plugins_dotted_path() {
let db = FixtureDatabase::new();
let fixture_content = r#"
import pytest
@pytest.fixture
def nested_fixture():
return "nested"
"#;
let conftest_content = r#"
pytest_plugins = "myapp.sub.fixtures"
"#;
let test_content = r#"
def test_uses_nested(nested_fixture):
pass
"#;
let fixture_path = PathBuf::from("/tmp/test_pp_dotted/myapp/sub/fixtures.py");
let conftest_path = PathBuf::from("/tmp/test_pp_dotted/conftest.py");
let test_path = PathBuf::from("/tmp/test_pp_dotted/test_example.py");
db.analyze_file(fixture_path.clone(), fixture_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "nested_fixture");
assert!(
resolved.is_some(),
"nested_fixture should be resolvable via dotted pytest_plugins path"
);
assert_eq!(resolved.unwrap().file_path, fixture_path);
}
#[test]
#[timeout(30000)]
fn test_pytest_plugins_in_test_file() {
let db = FixtureDatabase::new();
let module_content = r#"
import pytest
@pytest.fixture
def test_file_plugin_fixture():
return "from test file plugin"
"#;
let test_content = r#"
pytest_plugins = "fixture_module"
def test_uses_plugin(test_file_plugin_fixture):
pass
"#;
let fixture_module_path = PathBuf::from("/tmp/test_pp_testfile/fixture_module.py");
let test_path = PathBuf::from("/tmp/test_pp_testfile/test_example.py");
db.analyze_file(fixture_module_path.clone(), module_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "test_file_plugin_fixture");
assert!(
resolved.is_some(),
"test_file_plugin_fixture should be resolvable via pytest_plugins in test file"
);
assert_eq!(resolved.unwrap().file_path, fixture_module_path);
}
#[test]
#[timeout(30000)]
fn test_pytest_plugins_transitive() {
let db = FixtureDatabase::new();
let module_c_content = r#"
import pytest
@pytest.fixture
def deep_plugin_fixture():
return "deep"
"#;
let module_b_content = r#"
pytest_plugins = ["module_c"]
import pytest
@pytest.fixture
def mid_plugin_fixture():
return "mid"
"#;
let conftest_content = r#"
pytest_plugins = ["module_b"]
"#;
let test_content = r#"
def test_uses_deep(deep_plugin_fixture, mid_plugin_fixture):
pass
"#;
let module_c_path = PathBuf::from("/tmp/test_pp_transitive/module_c.py");
let module_b_path = PathBuf::from("/tmp/test_pp_transitive/module_b.py");
let conftest_path = PathBuf::from("/tmp/test_pp_transitive/conftest.py");
let test_path = PathBuf::from("/tmp/test_pp_transitive/test_example.py");
db.analyze_file(module_c_path.clone(), module_c_content);
db.analyze_file(module_b_path.clone(), module_b_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved_deep = db.resolve_fixture_for_file(&test_path, "deep_plugin_fixture");
assert!(
resolved_deep.is_some(),
"deep_plugin_fixture should be resolvable via transitive pytest_plugins"
);
assert_eq!(resolved_deep.unwrap().file_path, module_c_path);
let resolved_mid = db.resolve_fixture_for_file(&test_path, "mid_plugin_fixture");
assert!(
resolved_mid.is_some(),
"mid_plugin_fixture should be resolvable via pytest_plugins"
);
assert_eq!(resolved_mid.unwrap().file_path, module_b_path);
}
#[test]
#[timeout(30000)]
fn test_pytest_plugins_dynamic_value_ignored() {
let db = FixtureDatabase::new();
let conftest_content = r#"
pytest_plugins = get_plugins()
import pytest
@pytest.fixture
def local_fixture():
return "local"
"#;
let test_content = r#"
def test_local(local_fixture):
pass
"#;
let conftest_path = PathBuf::from("/tmp/test_pp_dynamic/conftest.py");
let test_path = PathBuf::from("/tmp/test_pp_dynamic/test_example.py");
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "local_fixture");
assert!(
resolved.is_some(),
"local_fixture should still be resolvable even with dynamic pytest_plugins"
);
}
#[test]
#[timeout(30000)]
fn test_pytest_plugins_available_fixtures() {
let db = FixtureDatabase::new();
let module_content = r#"
import pytest
@pytest.fixture
def plugin_avail_fixture():
return "available"
"#;
let conftest_content = r#"
pytest_plugins = ["fixture_module"]
import pytest
@pytest.fixture
def conftest_fixture():
return "conftest"
"#;
let test_content = r#"
def test_something():
pass
"#;
let fixture_module_path = PathBuf::from("/tmp/test_pp_avail/fixture_module.py");
let conftest_path = PathBuf::from("/tmp/test_pp_avail/conftest.py");
let test_path = PathBuf::from("/tmp/test_pp_avail/test_example.py");
db.analyze_file(fixture_module_path.clone(), module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let available = db.get_available_fixtures(&test_path);
let names: Vec<&str> = available.iter().map(|f| f.name.as_str()).collect();
assert!(
names.contains(&"conftest_fixture"),
"conftest_fixture should be in available fixtures"
);
assert!(
names.contains(&"plugin_avail_fixture"),
"plugin_avail_fixture should be in available fixtures (via pytest_plugins)"
);
}
#[test]
#[timeout(30000)]
fn test_pytest_plugins_in_venv_plugin_module() {
let db = FixtureDatabase::new();
let plugin_init_content = r#"
pytest_plugins = ["my_plugin.internal_fixtures"]
"#;
let internal_fixtures_content = r#"
import pytest
@pytest.fixture
def venv_internal_fixture():
return "from internal sub-module"
"#;
let conftest_content = r#"
import pytest
@pytest.fixture
def local_fixture():
return "local"
"#;
let test_content = r#"
def test_uses_venv_fixture(venv_internal_fixture):
pass
"#;
let site_packages = PathBuf::from("/tmp/test_venv_pp/venv/lib/python3.11/site-packages");
let plugin_init_path = site_packages.join("my_plugin/__init__.py");
let internal_path = site_packages.join("my_plugin/internal_fixtures.py");
let conftest_path = PathBuf::from("/tmp/test_venv_pp/conftest.py");
let test_path = PathBuf::from("/tmp/test_venv_pp/test_example.py");
db.site_packages_paths
.lock()
.unwrap()
.push(site_packages.clone());
db.analyze_file(plugin_init_path.clone(), plugin_init_content);
db.analyze_file(internal_path.clone(), internal_fixtures_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "venv_internal_fixture");
assert!(
resolved.is_some(),
"venv_internal_fixture should be resolvable via venv plugin pytest_plugins"
);
assert_eq!(resolved.unwrap().file_path, internal_path);
}
#[test]
#[timeout(30000)]
fn test_pytest_plugins_annotated_assignment() {
let db = FixtureDatabase::new();
let fixture_module_content = r#"
import pytest
@pytest.fixture
def annotated_plugin_fixture():
return "from annotated plugin"
"#;
let conftest_content = r#"
pytest_plugins: list[str] = ["fixture_module"]
"#;
let test_content = r#"
def test_uses_annotated(annotated_plugin_fixture):
pass
"#;
let fixture_module_path = PathBuf::from("/tmp/test_annassign/fixture_module.py");
let conftest_path = PathBuf::from("/tmp/test_annassign/conftest.py");
let test_path = PathBuf::from("/tmp/test_annassign/test_example.py");
db.analyze_file(fixture_module_path.clone(), fixture_module_content);
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let resolved = db.resolve_fixture_for_file(&test_path, "annotated_plugin_fixture");
assert!(
resolved.is_some(),
"annotated_plugin_fixture should be resolvable via annotated pytest_plugins"
);
assert_eq!(resolved.unwrap().file_path, fixture_module_path);
}
#[test]
#[timeout(30000)]
fn test_pytest_plugins_last_assignment_wins() {
let db = FixtureDatabase::new();
let module_a_content = r#"
import pytest
@pytest.fixture
def fixture_a():
return "a"
"#;
let module_b_content = r#"
import pytest
@pytest.fixture
def fixture_b():
return "b"
"#;
let conftest_content = r#"
pytest_plugins = ["module_a"]
pytest_plugins = ["module_b"]
"#;
let module_a_path = PathBuf::from("/tmp/test_last_wins/module_a.py");
let module_b_path = PathBuf::from("/tmp/test_last_wins/module_b.py");
let conftest_path = PathBuf::from("/tmp/test_last_wins/conftest.py");
db.analyze_file(module_a_path.clone(), module_a_content);
db.analyze_file(module_b_path.clone(), module_b_content);
db.analyze_file(conftest_path.clone(), conftest_content);
let imported = db.is_fixture_imported_in_file("fixture_b", &conftest_path);
assert!(
imported,
"fixture_b should be imported (last pytest_plugins assignment)"
);
let imported_a = db.is_fixture_imported_in_file("fixture_a", &conftest_path);
assert!(
!imported_a,
"fixture_a should NOT be imported (overwritten by second pytest_plugins)"
);
}
#[test]
#[timeout(30000)]
fn test_editable_install_is_third_party() {
use std::fs;
use tempfile::tempdir;
let db = FixtureDatabase::new();
let workspace = tempdir().unwrap();
let workspace_canonical = workspace.path().canonicalize().unwrap();
*db.workspace_root.lock().unwrap() = Some(workspace_canonical);
let external_src = tempdir().unwrap();
let external_src_canonical = external_src.path().canonicalize().unwrap();
db.editable_install_roots.lock().unwrap().push(
pytest_language_server::fixtures::EditableInstall {
package_name: "external_pkg".to_string(),
raw_package_name: "external_pkg".to_string(),
source_root: external_src_canonical,
site_packages: PathBuf::from("/fake/site-packages"),
},
);
let fixture_file = external_src.path().join("plugin.py");
let fixture_content = r#"
import pytest
@pytest.fixture
def ext_editable_fixture():
return "from external editable"
"#;
fs::write(&fixture_file, fixture_content).unwrap();
db.analyze_file(fixture_file.clone(), fixture_content);
let defs = db.definitions.get("ext_editable_fixture").unwrap();
assert!(
defs[0].is_third_party,
"Fixture from external editable install should be third-party"
);
}
#[test]
#[timeout(30000)]
fn test_editable_install_in_workspace_not_third_party() {
use std::fs;
use tempfile::tempdir;
let db = FixtureDatabase::new();
let workspace = tempdir().unwrap();
let workspace_canonical = workspace.path().canonicalize().unwrap();
let editable_src = workspace_canonical
.join("packages")
.join("mylib")
.join("src");
std::fs::create_dir_all(&editable_src).unwrap();
*db.workspace_root.lock().unwrap() = Some(workspace_canonical);
db.editable_install_roots.lock().unwrap().push(
pytest_language_server::fixtures::EditableInstall {
package_name: "mylib".to_string(),
raw_package_name: "mylib".to_string(),
source_root: editable_src.clone(),
site_packages: PathBuf::from("/fake/site-packages"),
},
);
let fixture_file = editable_src.join("conftest.py");
let fixture_content = r#"
import pytest
@pytest.fixture
def local_editable_fixture():
return "from local editable"
"#;
fs::write(&fixture_file, fixture_content).unwrap();
db.analyze_file(fixture_file.clone(), fixture_content);
let defs = db.definitions.get("local_editable_fixture").unwrap();
assert!(
!defs[0].is_third_party,
"Fixture from in-workspace editable install should NOT be third-party"
);
}
#[test]
#[timeout(30000)]
fn test_editable_install_unused_fixtures_excluded() {
use std::fs;
use tempfile::tempdir;
let db = FixtureDatabase::new();
let workspace = tempdir().unwrap();
let workspace_canonical = workspace.path().canonicalize().unwrap();
let external_src = tempdir().unwrap();
let external_src_canonical = external_src.path().canonicalize().unwrap();
*db.workspace_root.lock().unwrap() = Some(workspace_canonical);
db.editable_install_roots.lock().unwrap().push(
pytest_language_server::fixtures::EditableInstall {
package_name: "ext_pkg".to_string(),
raw_package_name: "ext_pkg".to_string(),
source_root: external_src_canonical,
site_packages: PathBuf::from("/fake/site-packages"),
},
);
let fixture_file = external_src.path().join("plugin.py");
let fixture_content = r#"
import pytest
@pytest.fixture
def third_party_editable_fixture():
return "external"
"#;
fs::write(&fixture_file, fixture_content).unwrap();
db.analyze_file(fixture_file, fixture_content);
let local_file = workspace.path().join("conftest.py");
let local_content = r#"
import pytest
@pytest.fixture
def unused_local_fixture():
return "local"
"#;
fs::write(&local_file, local_content).unwrap();
db.analyze_file(local_file, local_content);
let unused = db.get_unused_fixtures();
let unused_names: Vec<&str> = unused.iter().map(|(_, name)| name.as_str()).collect();
assert!(
!unused_names.contains(&"third_party_editable_fixture"),
"Third-party editable fixture should be excluded from unused report"
);
assert!(
unused_names.contains(&"unused_local_fixture"),
"Local unused fixture should appear in unused report"
);
}
#[test]
#[timeout(30000)]
fn test_autouse_fixture_not_reported_as_unused() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture(autouse=True)
def auto_setup():
yield
@pytest.fixture
def regular_fixture():
return 42
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path.clone(), conftest_content);
let test_content = r#"
def test_something():
assert True
"#;
let test_path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(test_path.clone(), test_content);
let unused = db.get_unused_fixtures();
let unused_names: Vec<&str> = unused.iter().map(|(_, name)| name.as_str()).collect();
assert!(
!unused_names.contains(&"auto_setup"),
"autouse fixture should NOT be reported as unused"
);
assert!(
unused_names.contains(&"regular_fixture"),
"regular unused fixture should be reported as unused"
);
}
#[test]
#[timeout(30000)]
fn test_autouse_with_scope_not_reported_unused() {
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(scope="session", autouse=True)
def session_auto():
yield
"#;
let file_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(file_path.clone(), content);
let unused = db.get_unused_fixtures();
let unused_names: Vec<&str> = unused.iter().map(|(_, name)| name.as_str()).collect();
assert!(
!unused_names.contains(&"session_auto"),
"autouse fixture with scope should NOT be reported as unused"
);
}
#[test]
#[timeout(30000)]
fn test_extract_fixture_autouse() {
use rustpython_parser::{parse, Mode};
let cases = vec![
("@pytest.fixture(autouse=True)\ndef f(): pass", true),
("@pytest.fixture(autouse=False)\ndef f(): pass", false),
("@pytest.fixture\ndef f(): pass", false),
("@pytest.fixture()\ndef f(): pass", false),
(
"@pytest.fixture(scope=\"session\", autouse=True)\ndef f(): pass",
true,
),
];
for (source, expected) in cases {
let parsed = parse(source, Mode::Module, "<test>").unwrap();
let module = parsed.as_module().unwrap();
let stmt = &module.body[0];
if let rustpython_parser::ast::Stmt::FunctionDef(func) = stmt {
let decorator = &func.decorator_list[0];
let result =
pytest_language_server::fixtures::decorators::extract_fixture_autouse(decorator);
assert_eq!(
result, expected,
"extract_fixture_autouse({:?}) should be {}, got {}",
source, expected, result
);
} else {
panic!("Expected FunctionDef, got {:?}", stmt);
}
}
}
#[test]
#[timeout(30000)]
fn test_workspace_editable_plugin_fixture_is_plugin_flag() {
use std::fs;
use tempfile::tempdir;
let db = FixtureDatabase::new();
let workspace = tempdir().unwrap();
let workspace_canonical = workspace.path().canonicalize().unwrap();
*db.workspace_root.lock().unwrap() = Some(workspace_canonical.clone());
let pkg_dir = workspace_canonical.join("mypackage");
fs::create_dir_all(&pkg_dir).unwrap();
let plugin_file = pkg_dir.join("plugin.py");
let plugin_content = r#"
import pytest
@pytest.fixture
def ws_plugin_fixture():
return "from workspace plugin"
"#;
fs::write(&plugin_file, plugin_content).unwrap();
let canonical_plugin = plugin_file.canonicalize().unwrap();
db.plugin_fixture_files.insert(canonical_plugin.clone(), ());
db.analyze_file(canonical_plugin.clone(), plugin_content);
let defs = db.definitions.get("ws_plugin_fixture").unwrap();
assert_eq!(defs.len(), 1);
assert!(
defs[0].is_plugin,
"Fixture from plugin file should have is_plugin=true"
);
assert!(
!defs[0].is_third_party,
"Fixture from workspace plugin should NOT be is_third_party"
);
}
#[test]
#[timeout(30000)]
fn test_workspace_editable_plugin_fixture_resolution() {
use std::fs;
use tempfile::tempdir;
let db = FixtureDatabase::new();
let workspace = tempdir().unwrap();
let workspace_canonical = workspace.path().canonicalize().unwrap();
*db.workspace_root.lock().unwrap() = Some(workspace_canonical.clone());
let pkg_dir = workspace_canonical.join("mypackage");
fs::create_dir_all(&pkg_dir).unwrap();
let plugin_file = pkg_dir.join("plugin.py");
let plugin_content = r#"
import pytest
@pytest.fixture
def plugin_fixture():
return "from plugin"
"#;
fs::write(&plugin_file, plugin_content).unwrap();
let canonical_plugin = plugin_file.canonicalize().unwrap();
db.plugin_fixture_files.insert(canonical_plugin.clone(), ());
db.analyze_file(canonical_plugin.clone(), plugin_content);
let tests_dir = workspace_canonical.join("tests");
fs::create_dir_all(&tests_dir).unwrap();
let test_file = tests_dir.join("test_foo.py");
let test_content = r#"
def test_something(plugin_fixture):
assert plugin_fixture == "from plugin"
"#;
fs::write(&test_file, test_content).unwrap();
let canonical_test = test_file.canonicalize().unwrap();
db.analyze_file(canonical_test.clone(), test_content);
let resolved = db.find_fixture_definition(&canonical_test, 1, 19);
assert!(
resolved.is_some(),
"Plugin fixture should be resolvable from test file via find_closest_definition"
);
let resolved = resolved.unwrap();
assert_eq!(resolved.name, "plugin_fixture");
assert_eq!(resolved.file_path, canonical_plugin);
}
#[test]
#[timeout(30000)]
fn test_workspace_editable_plugin_available_fixtures() {
use std::fs;
use tempfile::tempdir;
let db = FixtureDatabase::new();
let workspace = tempdir().unwrap();
let workspace_canonical = workspace.path().canonicalize().unwrap();
*db.workspace_root.lock().unwrap() = Some(workspace_canonical.clone());
let pkg_dir = workspace_canonical.join("mypackage");
fs::create_dir_all(&pkg_dir).unwrap();
let plugin_file = pkg_dir.join("plugin.py");
let plugin_content = r#"
import pytest
@pytest.fixture
def available_plugin_fixture():
return "available"
"#;
fs::write(&plugin_file, plugin_content).unwrap();
let canonical_plugin = plugin_file.canonicalize().unwrap();
db.plugin_fixture_files.insert(canonical_plugin.clone(), ());
db.analyze_file(canonical_plugin, plugin_content);
let tests_dir = workspace_canonical.join("tests");
fs::create_dir_all(&tests_dir).unwrap();
let test_file = tests_dir.join("test_bar.py");
let test_content = r#"
def test_bar(available_plugin_fixture):
pass
"#;
fs::write(&test_file, test_content).unwrap();
let canonical_test = test_file.canonicalize().unwrap();
db.analyze_file(canonical_test.clone(), test_content);
let available = db.get_available_fixtures(&canonical_test);
let available_names: Vec<&str> = available.iter().map(|d| d.name.as_str()).collect();
assert!(
available_names.contains(&"available_plugin_fixture"),
"Plugin fixture should appear in available fixtures for test file. Got: {:?}",
available_names
);
}
#[test]
#[timeout(30000)]
fn test_workspace_editable_plugin_conftest_wins_over_plugin() {
use std::fs;
use tempfile::tempdir;
let db = FixtureDatabase::new();
let workspace = tempdir().unwrap();
let workspace_canonical = workspace.path().canonicalize().unwrap();
*db.workspace_root.lock().unwrap() = Some(workspace_canonical.clone());
let pkg_dir = workspace_canonical.join("mypackage");
fs::create_dir_all(&pkg_dir).unwrap();
let plugin_file = pkg_dir.join("plugin.py");
let plugin_content = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "from plugin"
"#;
fs::write(&plugin_file, plugin_content).unwrap();
let canonical_plugin = plugin_file.canonicalize().unwrap();
db.plugin_fixture_files.insert(canonical_plugin.clone(), ());
db.analyze_file(canonical_plugin.clone(), plugin_content);
let tests_dir = workspace_canonical.join("tests");
fs::create_dir_all(&tests_dir).unwrap();
let conftest_file = tests_dir.join("conftest.py");
let conftest_content = r#"
import pytest
@pytest.fixture
def shared_fixture():
return "from conftest"
"#;
fs::write(&conftest_file, conftest_content).unwrap();
let canonical_conftest = conftest_file.canonicalize().unwrap();
db.analyze_file(canonical_conftest.clone(), conftest_content);
let test_file = tests_dir.join("test_priority.py");
let test_content = r#"
def test_priority(shared_fixture):
pass
"#;
fs::write(&test_file, test_content).unwrap();
let canonical_test = test_file.canonicalize().unwrap();
db.analyze_file(canonical_test.clone(), test_content);
let resolved = db.find_fixture_definition(&canonical_test, 1, 20);
assert!(resolved.is_some(), "shared_fixture should be resolvable");
let resolved = resolved.unwrap();
assert_eq!(
resolved.file_path, canonical_conftest,
"conftest.py fixture should win over plugin fixture"
);
}
#[test]
#[timeout(30000)]
fn test_workspace_editable_plugin_fixture_is_available_for_undeclared_check() {
use std::fs;
use tempfile::tempdir;
let db = FixtureDatabase::new();
let workspace = tempdir().unwrap();
let workspace_canonical = workspace.path().canonicalize().unwrap();
*db.workspace_root.lock().unwrap() = Some(workspace_canonical.clone());
let pkg_dir = workspace_canonical.join("mypackage");
fs::create_dir_all(&pkg_dir).unwrap();
let plugin_file = pkg_dir.join("plugin.py");
let plugin_content = r#"
import pytest
@pytest.fixture
def undeclared_check_fixture():
return "plugin"
"#;
fs::write(&plugin_file, plugin_content).unwrap();
let canonical_plugin = plugin_file.canonicalize().unwrap();
db.plugin_fixture_files.insert(canonical_plugin.clone(), ());
db.analyze_file(canonical_plugin, plugin_content);
let tests_dir = workspace_canonical.join("tests");
fs::create_dir_all(&tests_dir).unwrap();
let test_file = tests_dir.join("test_x.py");
let test_content = "def test_x(): pass\n";
fs::write(&test_file, test_content).unwrap();
let canonical_test = test_file.canonicalize().unwrap();
db.analyze_file(canonical_test.clone(), test_content);
let available = db.get_available_fixtures(&canonical_test);
let available_names: Vec<&str> = available.iter().map(|d| d.name.as_str()).collect();
assert!(
available_names.contains(&"undeclared_check_fixture"),
"Plugin fixture should be recognized as available (used by undeclared fixture checker). Got: {:?}",
available_names
);
}
#[test]
#[timeout(30000)]
fn test_workspace_editable_plugin_resolve_fixture_for_file() {
use std::fs;
use tempfile::tempdir;
let db = FixtureDatabase::new();
let workspace = tempdir().unwrap();
let workspace_canonical = workspace.path().canonicalize().unwrap();
*db.workspace_root.lock().unwrap() = Some(workspace_canonical.clone());
let pkg_dir = workspace_canonical.join("mypackage");
fs::create_dir_all(&pkg_dir).unwrap();
let plugin_file = pkg_dir.join("plugin.py");
let plugin_content = r#"
import pytest
@pytest.fixture
def resolve_for_file_fixture():
return "plugin"
"#;
fs::write(&plugin_file, plugin_content).unwrap();
let canonical_plugin = plugin_file.canonicalize().unwrap();
db.plugin_fixture_files.insert(canonical_plugin.clone(), ());
db.analyze_file(canonical_plugin.clone(), plugin_content);
let test_file = workspace_canonical.join("tests").join("test_resolve.py");
let resolved = db.resolve_fixture_for_file(&test_file, "resolve_for_file_fixture");
assert!(
resolved.is_some(),
"resolve_fixture_for_file should find plugin fixtures"
);
assert_eq!(resolved.unwrap().file_path, canonical_plugin);
}
#[test]
#[timeout(30000)]
fn test_external_editable_plugin_is_third_party_and_resolvable() {
use std::fs;
use tempfile::tempdir;
let db = FixtureDatabase::new();
let workspace = tempdir().unwrap();
let workspace_canonical = workspace.path().canonicalize().unwrap();
*db.workspace_root.lock().unwrap() = Some(workspace_canonical.clone());
let external_src = tempdir().unwrap();
let external_src_canonical = external_src.path().canonicalize().unwrap();
db.editable_install_roots.lock().unwrap().push(
pytest_language_server::fixtures::EditableInstall {
package_name: "ext_plugin".to_string(),
raw_package_name: "ext_plugin".to_string(),
source_root: external_src_canonical.clone(),
site_packages: PathBuf::from("/fake/site-packages"),
},
);
let plugin_file = external_src_canonical.join("plugin.py");
let plugin_content = r#"
import pytest
@pytest.fixture
def ext_plugin_fixture():
return "external"
"#;
fs::write(&plugin_file, plugin_content).unwrap();
let canonical_plugin = plugin_file.canonicalize().unwrap();
db.plugin_fixture_files.insert(canonical_plugin.clone(), ());
db.analyze_file(canonical_plugin.clone(), plugin_content);
let defs = db.definitions.get("ext_plugin_fixture").unwrap();
assert!(
defs[0].is_third_party,
"External editable should be third-party"
);
assert!(defs[0].is_plugin, "Should also be marked as plugin");
let test_file = workspace_canonical.join("tests").join("test_ext.py");
let resolved = db.resolve_fixture_for_file(&test_file, "ext_plugin_fixture");
assert!(
resolved.is_some(),
"External editable plugin fixture should be resolvable as third-party"
);
}
#[test]
#[timeout(30000)]
fn test_non_plugin_file_fixture_not_marked_is_plugin() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture
def regular_fixture():
return "regular"
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(conftest_path, conftest_content);
let defs = db.definitions.get("regular_fixture").unwrap();
assert!(
!defs[0].is_plugin,
"Regular conftest fixture should NOT be marked as plugin"
);
assert!(
!defs[0].is_third_party,
"Regular conftest fixture should NOT be third-party"
);
}
#[test]
#[timeout(30000)]
fn test_cli_and_resolver_agree_on_workspace_editable_plugin_fixtures() {
use std::fs;
use tempfile::tempdir;
let db = FixtureDatabase::new();
let workspace = tempdir().unwrap();
let workspace_canonical = workspace.path().canonicalize().unwrap();
*db.workspace_root.lock().unwrap() = Some(workspace_canonical.clone());
let conftest_file = workspace_canonical.join("conftest.py");
let conftest_content = r#"
import pytest
@pytest.fixture
def conftest_fixture():
return "conftest"
"#;
fs::write(&conftest_file, conftest_content).unwrap();
let canonical_conftest = conftest_file.canonicalize().unwrap();
db.analyze_file(canonical_conftest, conftest_content);
let pkg_dir = workspace_canonical.join("mypackage");
fs::create_dir_all(&pkg_dir).unwrap();
let plugin_file = pkg_dir.join("plugin.py");
let plugin_content = r#"
import pytest
@pytest.fixture
def plugin_only_fixture():
return "plugin"
"#;
fs::write(&plugin_file, plugin_content).unwrap();
let canonical_plugin = plugin_file.canonicalize().unwrap();
db.plugin_fixture_files.insert(canonical_plugin.clone(), ());
db.analyze_file(canonical_plugin, plugin_content);
let test_file = workspace_canonical.join("test_agree.py");
let test_content = r#"
def test_agree(conftest_fixture, plugin_only_fixture):
pass
"#;
fs::write(&test_file, test_content).unwrap();
let canonical_test = test_file.canonicalize().unwrap();
db.analyze_file(canonical_test.clone(), test_content);
let all_fixture_names: std::collections::HashSet<String> = db
.definitions
.iter()
.map(|entry| entry.key().clone())
.collect();
let available = db.get_available_fixtures(&canonical_test);
let available_names: std::collections::HashSet<String> =
available.iter().map(|d| d.name.clone()).collect();
assert!(
available_names.contains("conftest_fixture"),
"conftest_fixture should be in available fixtures"
);
assert!(
available_names.contains("plugin_only_fixture"),
"plugin_only_fixture should be in available fixtures (was missing before fix)"
);
for name in &all_fixture_names {
assert!(
available_names.contains(name),
"Fixture '{}' is in definitions (CLI view) but NOT in available fixtures (LSP view)",
name
);
}
}
#[test]
#[timeout(30000)]
fn test_e2e_scan_workspace_editable_plugin_entry_point() {
use std::fs;
use tempfile::tempdir;
let workspace = tempdir().unwrap();
let ws = workspace.path().canonicalize().unwrap();
let pkg_dir = ws.join("mypackage");
fs::create_dir_all(&pkg_dir).unwrap();
fs::write(pkg_dir.join("__init__.py"), "").unwrap();
let plugin_content = r#"
import pytest
pytest_plugins = ["mypackage.helpers"]
@pytest.fixture
def direct_plugin_fixture():
"""Fixture defined directly in the plugin entry point module."""
return "direct"
"#;
fs::write(pkg_dir.join("plugin.py"), plugin_content).unwrap();
let helpers_content = r#"
import pytest
@pytest.fixture
def transitive_plugin_fixture():
"""Fixture imported transitively via pytest_plugins in plugin.py."""
return "transitive"
"#;
fs::write(pkg_dir.join("helpers.py"), helpers_content).unwrap();
let conftest_content = r#"
import pytest
@pytest.fixture
def root_conftest_fixture():
return "conftest"
"#;
fs::write(ws.join("conftest.py"), conftest_content).unwrap();
let tests_dir = ws.join("tests");
fs::create_dir_all(&tests_dir).unwrap();
let test_content = r#"
def test_uses_plugin(direct_plugin_fixture, transitive_plugin_fixture, root_conftest_fixture):
pass
"#;
fs::write(tests_dir.join("test_foo.py"), test_content).unwrap();
let site_packages = ws
.join(".venv")
.join("lib")
.join("python3.12")
.join("site-packages");
fs::create_dir_all(&site_packages).unwrap();
let dist_info = site_packages.join("mypackage-0.1.0.dist-info");
fs::create_dir_all(&dist_info).unwrap();
fs::write(
dist_info.join("entry_points.txt"),
"[pytest11]\nmyplugin = mypackage.plugin\n",
)
.unwrap();
let direct_url = serde_json::json!({
"url": format!("file://{}", ws.display()),
"dir_info": { "editable": true }
});
fs::write(
dist_info.join("direct_url.json"),
serde_json::to_string(&direct_url).unwrap(),
)
.unwrap();
fs::write(
site_packages.join("mypackage.pth"),
format!("{}\n", ws.display()),
)
.unwrap();
let db = FixtureDatabase::new();
db.scan_workspace(&ws);
let canonical_test = ws.join("tests").join("test_foo.py");
assert!(
db.definitions.contains_key("direct_plugin_fixture"),
"direct_plugin_fixture should be in definitions after scan_workspace. \
definitions keys: {:?}",
db.definitions
.iter()
.map(|e| e.key().clone())
.collect::<Vec<_>>()
);
{
let defs = db.definitions.get("direct_plugin_fixture").unwrap();
assert!(
!defs[0].is_third_party,
"Workspace-local editable plugin fixture should NOT be third_party"
);
}
let resolved = db.resolve_fixture_for_file(&canonical_test, "direct_plugin_fixture");
assert!(
resolved.is_some(),
"direct_plugin_fixture should be resolvable from test file via resolve_fixture_for_file. \
definitions: {:?}",
db.definitions
.get("direct_plugin_fixture")
.map(|d| d.value().clone())
);
let available = db.get_available_fixtures(&canonical_test);
let available_names: Vec<&str> = available.iter().map(|d| d.name.as_str()).collect();
assert!(
available_names.contains(&"direct_plugin_fixture"),
"direct_plugin_fixture should be in available fixtures for test file. Got: {:?}",
available_names
);
assert!(
available_names.contains(&"root_conftest_fixture"),
"root_conftest_fixture should be in available fixtures. Got: {:?}",
available_names
);
let transitive_available = available_names.contains(&"transitive_plugin_fixture");
let transitive_in_defs = db.definitions.contains_key("transitive_plugin_fixture");
assert!(
transitive_in_defs,
"transitive_plugin_fixture should be in definitions (discovered via pytest_plugins). \
All definitions: {:?}",
db.definitions
.iter()
.map(|e| e.key().clone())
.collect::<Vec<_>>()
);
assert!(
transitive_available,
"transitive_plugin_fixture should be available for the test file. Got: {:?}",
available_names
);
let undeclared = db.get_undeclared_fixtures(&canonical_test);
let undeclared_names: Vec<&str> = undeclared.iter().map(|u| u.name.as_str()).collect();
assert!(
!undeclared_names.contains(&"direct_plugin_fixture"),
"direct_plugin_fixture should NOT be reported as undeclared. Undeclared: {:?}",
undeclared_names
);
assert!(
!undeclared_names.contains(&"transitive_plugin_fixture"),
"transitive_plugin_fixture should NOT be reported as undeclared. Undeclared: {:?}",
undeclared_names
);
let goto = db.find_fixture_definition(&canonical_test, 1, 21);
assert!(
goto.is_some(),
"find_fixture_definition should resolve direct_plugin_fixture from the test file"
);
let goto_def = goto.unwrap();
assert_eq!(goto_def.name, "direct_plugin_fixture");
}
#[test]
#[timeout(30000)]
fn test_completion_context_fixture_scope_default() {
use pytest_language_server::CompletionContext;
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture():
return 42
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 4, 15);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
fixture_scope,
..
} => {
assert_eq!(function_name, "my_fixture");
assert!(is_fixture);
assert_eq!(fixture_scope, Some(FixtureScope::Function));
}
_ => panic!("Expected FunctionSignature context"),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_fixture_scope_session() {
use pytest_language_server::CompletionContext;
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(scope="session")
def my_session_fixture():
return 42
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 4, 22);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
fixture_scope,
..
} => {
assert_eq!(function_name, "my_session_fixture");
assert!(is_fixture);
assert_eq!(fixture_scope, Some(FixtureScope::Session));
}
_ => panic!("Expected FunctionSignature context"),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_fixture_scope_module() {
use pytest_language_server::CompletionContext;
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(scope="module")
def my_module_fixture():
return 42
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 4, 22);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature { fixture_scope, .. } => {
assert_eq!(fixture_scope, Some(FixtureScope::Module));
}
_ => panic!("Expected FunctionSignature context"),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_test_function_no_scope() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = r#"
import pytest
def test_something():
pass
"#;
let path = PathBuf::from("/tmp/test/test_example.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 3, 18);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
fixture_scope,
..
} => {
assert_eq!(function_name, "test_something");
assert!(!is_fixture);
assert_eq!(fixture_scope, None);
}
_ => panic!("Expected FunctionSignature context"),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_fixture_body_has_scope() {
use pytest_language_server::CompletionContext;
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture(scope="session")
def my_session_fixture():
x = 1
return x
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 5, 4);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionBody {
function_name,
is_fixture,
fixture_scope,
..
} => {
assert_eq!(function_name, "my_session_fixture");
assert!(is_fixture);
assert_eq!(fixture_scope, Some(FixtureScope::Session));
}
_ => panic!("Expected FunctionBody context"),
}
}
#[test]
#[timeout(30000)]
fn test_completion_scope_filtering_session_fixture() {
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture(scope="function")
def func_fixture():
return "func"
@pytest.fixture(scope="class")
def class_fixture():
return "class"
@pytest.fixture(scope="module")
def module_fixture():
return "module"
@pytest.fixture(scope="session")
def session_fixture():
return "session"
"#;
let test_content = r#"
import pytest
@pytest.fixture(scope="session")
def my_session_fixture():
pass
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
let test_path = PathBuf::from("/tmp/test/test_scope.py");
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 4, 22);
assert!(ctx.is_some());
let fixture_scope = match ctx.unwrap() {
pytest_language_server::CompletionContext::FunctionSignature { fixture_scope, .. } => {
fixture_scope
}
_ => panic!("Expected FunctionSignature"),
};
assert_eq!(fixture_scope, Some(FixtureScope::Session));
let available = db.get_available_fixtures(&test_path);
let filtered: Vec<_> = available
.into_iter()
.filter(|f| f.scope >= FixtureScope::Session)
.collect();
let filtered_names: Vec<&str> = filtered.iter().map(|f| f.name.as_str()).collect();
assert!(
filtered_names.contains(&"session_fixture"),
"session_fixture should be included, got: {:?}",
filtered_names
);
assert!(
!filtered_names.contains(&"func_fixture"),
"func_fixture should be excluded"
);
assert!(
!filtered_names.contains(&"class_fixture"),
"class_fixture should be excluded"
);
assert!(
!filtered_names.contains(&"module_fixture"),
"module_fixture should be excluded"
);
}
#[test]
#[timeout(30000)]
fn test_completion_scope_filtering_module_fixture() {
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture(scope="function")
def func_fixture():
return "func"
@pytest.fixture(scope="class")
def class_fixture():
return "class"
@pytest.fixture(scope="module")
def module_fixture():
return "module"
@pytest.fixture(scope="session")
def session_fixture():
return "session"
"#;
let test_content = r#"
import pytest
@pytest.fixture(scope="module")
def my_module_fixture():
pass
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
let test_path = PathBuf::from("/tmp/test/test_scope.py");
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 4, 22);
assert!(ctx.is_some());
let fixture_scope = match ctx.unwrap() {
pytest_language_server::CompletionContext::FunctionSignature { fixture_scope, .. } => {
fixture_scope
}
_ => panic!("Expected FunctionSignature"),
};
assert_eq!(fixture_scope, Some(FixtureScope::Module));
let available = db.get_available_fixtures(&test_path);
let filtered: Vec<_> = available
.into_iter()
.filter(|f| f.scope >= FixtureScope::Module)
.collect();
let filtered_names: Vec<&str> = filtered.iter().map(|f| f.name.as_str()).collect();
assert!(
filtered_names.contains(&"module_fixture"),
"module_fixture should be included, got: {:?}",
filtered_names
);
assert!(
filtered_names.contains(&"session_fixture"),
"session_fixture should be included, got: {:?}",
filtered_names
);
assert!(
!filtered_names.contains(&"func_fixture"),
"func_fixture should be excluded"
);
assert!(
!filtered_names.contains(&"class_fixture"),
"class_fixture should be excluded"
);
}
#[test]
#[timeout(30000)]
fn test_completion_scope_filtering_function_fixture_allows_all() {
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture(scope="function")
def func_fixture():
return "func"
@pytest.fixture(scope="class")
def class_fixture():
return "class"
@pytest.fixture(scope="module")
def module_fixture():
return "module"
@pytest.fixture(scope="session")
def session_fixture():
return "session"
"#;
let test_content = r#"
import pytest
@pytest.fixture
def my_func_fixture():
pass
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
let test_path = PathBuf::from("/tmp/test/test_scope.py");
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 4, 20);
assert!(ctx.is_some());
let fixture_scope = match ctx.unwrap() {
pytest_language_server::CompletionContext::FunctionSignature { fixture_scope, .. } => {
fixture_scope
}
_ => panic!("Expected FunctionSignature"),
};
assert_eq!(fixture_scope, Some(FixtureScope::Function));
let available = db.get_available_fixtures(&test_path);
let filtered: Vec<_> = available
.into_iter()
.filter(|f| f.scope >= FixtureScope::Function)
.collect();
let filtered_names: Vec<&str> = filtered.iter().map(|f| f.name.as_str()).collect();
assert!(
filtered_names.contains(&"func_fixture"),
"func_fixture should be included"
);
assert!(
filtered_names.contains(&"class_fixture"),
"class_fixture should be included"
);
assert!(
filtered_names.contains(&"module_fixture"),
"module_fixture should be included"
);
assert!(
filtered_names.contains(&"session_fixture"),
"session_fixture should be included"
);
}
#[test]
#[timeout(30000)]
fn test_completion_scope_filtering_test_function_allows_all() {
let db = FixtureDatabase::new();
let conftest_content = r#"
import pytest
@pytest.fixture(scope="function")
def func_fixture():
return "func"
@pytest.fixture(scope="session")
def session_fixture():
return "session"
"#;
let test_content = r#"
import pytest
def test_something():
pass
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
let test_path = PathBuf::from("/tmp/test/test_scope.py");
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let ctx = db.get_completion_context(&test_path, 3, 18);
assert!(ctx.is_some());
let fixture_scope = match ctx.unwrap() {
pytest_language_server::CompletionContext::FunctionSignature { fixture_scope, .. } => {
fixture_scope
}
_ => panic!("Expected FunctionSignature"),
};
assert_eq!(fixture_scope, None);
let available = db.get_available_fixtures(&test_path);
let names: Vec<&str> = available.iter().map(|f| f.name.as_str()).collect();
assert!(
names.contains(&"func_fixture"),
"func_fixture should be visible to test functions"
);
assert!(
names.contains(&"session_fixture"),
"session_fixture should be visible to test functions"
);
}
#[test]
#[timeout(30000)]
fn test_completion_fixture_proximity_same_file_first() {
let db = FixtureDatabase::new();
let test_content = r#"
import pytest
@pytest.fixture
def local_fixture():
return "local"
def test_something():
pass
"#;
let conftest_content = r#"
import pytest
@pytest.fixture
def conftest_fixture():
return "conftest"
"#;
let conftest_path = PathBuf::from("/tmp/test/conftest.py");
let test_path = PathBuf::from("/tmp/test/test_proximity.py");
db.analyze_file(conftest_path.clone(), conftest_content);
db.analyze_file(test_path.clone(), test_content);
let available = db.get_available_fixtures(&test_path);
let local = available.iter().find(|f| f.name == "local_fixture");
let conftest = available.iter().find(|f| f.name == "conftest_fixture");
assert!(local.is_some(), "Should find local fixture");
assert!(conftest.is_some(), "Should find conftest fixture");
let local = local.unwrap();
let conftest = conftest.unwrap();
assert_eq!(local.file_path, test_path);
assert_eq!(conftest.file_path, conftest_path);
}
#[test]
#[timeout(30000)]
fn test_completion_third_party_fixture_has_flag() {
let db = FixtureDatabase::new();
let third_party_path =
PathBuf::from("/tmp/venv/lib/python3.11/site-packages/pytest_django/fixtures.py");
db.definitions.insert(
"tp_fixture".to_string(),
vec![pytest_language_server::FixtureDefinition {
name: "tp_fixture".to_string(),
file_path: third_party_path.clone(),
line: 10,
end_line: 15,
start_char: 4,
end_char: 14,
docstring: Some("A third-party fixture".to_string()),
is_third_party: true,
scope: pytest_language_server::FixtureScope::Session,
..Default::default()
}],
);
let test_content = r#"
import pytest
@pytest.fixture
def local_fixture():
return "local"
def test_something():
pass
"#;
let test_path = PathBuf::from("/tmp/test/test_third_party.py");
db.analyze_file(test_path.clone(), test_content);
let available = db.get_available_fixtures(&test_path);
let tp = available.iter().find(|f| f.name == "tp_fixture");
assert!(tp.is_some(), "Should find third-party fixture");
let tp = tp.unwrap();
assert!(tp.is_third_party, "Should be flagged as third-party");
assert_eq!(tp.scope, pytest_language_server::FixtureScope::Session);
let local = available.iter().find(|f| f.name == "local_fixture");
assert!(local.is_some(), "Should find local fixture");
assert!(
!local.unwrap().is_third_party,
"Local should not be third-party"
);
}
#[test]
#[timeout(30000)]
fn test_completion_context_multiline_signature() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = r#"
import pytest
def test_foo(
a,
b,
):
pass
"#;
let path = PathBuf::from("/tmp/test/test_multiline.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 3, 13);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature { function_name, .. } => {
assert_eq!(function_name, "test_foo");
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
let ctx = db.get_completion_context(&path, 4, 5);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature { function_name, .. } => {
assert_eq!(function_name, "test_foo");
}
other => panic!("Expected FunctionSignature on param line, got {:?}", other),
}
let ctx = db.get_completion_context(&path, 5, 5);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature { function_name, .. } => {
assert_eq!(function_name, "test_foo");
}
other => panic!(
"Expected FunctionSignature on second param line, got {:?}",
other
),
}
let ctx = db.get_completion_context(&path, 6, 1);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature { function_name, .. } => {
assert_eq!(function_name, "test_foo");
}
other => panic!(
"Expected FunctionSignature on closing line, got {:?}",
other
),
}
let ctx = db.get_completion_context(&path, 7, 4);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionBody { function_name, .. } => {
assert_eq!(function_name, "test_foo");
}
other => panic!("Expected FunctionBody, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_return_type_annotation() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture(a) -> int:
return 42
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 4, 15);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature { function_name, .. } => {
assert_eq!(function_name, "my_fixture");
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
let ctx = db.get_completion_context(&path, 5, 4);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionBody { function_name, .. } => {
assert_eq!(function_name, "my_fixture");
}
other => panic!("Expected FunctionBody, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_multiline_signature_with_return_type() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture(
a,
b,
) -> int:
return 42
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 7, 1);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature { function_name, .. } => {
assert_eq!(function_name, "my_fixture");
}
other => panic!(
"Expected FunctionSignature on return type line, got {:?}",
other
),
}
let ctx = db.get_completion_context(&path, 8, 4);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionBody { function_name, .. } => {
assert_eq!(function_name, "my_fixture");
}
other => panic!("Expected FunctionBody, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_single_line_def() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = r#"
import pytest
def test_foo(a):
pass
"#;
let path = PathBuf::from("/tmp/test/test_single.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 3, 13);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature { function_name, .. } => {
assert_eq!(function_name, "test_foo");
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
let ctx = db.get_completion_context(&path, 4, 4);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionBody { function_name, .. } => {
assert_eq!(function_name, "test_foo");
}
other => panic!("Expected FunctionBody, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_fixture_with_many_params_multiline() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture(
param_a,
param_b,
param_c,
param_d,
param_e,
):
return 42
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
for line in 4..=10 {
let ctx = db.get_completion_context(&path, line, 4);
assert!(ctx.is_some(), "Expected context on line {}", line);
match ctx.unwrap() {
CompletionContext::FunctionSignature { function_name, .. } => {
assert_eq!(function_name, "my_fixture", "Wrong name on line {}", line);
}
other => panic!(
"Expected FunctionSignature on line {}, got {:?}",
line, other
),
}
}
let ctx = db.get_completion_context(&path, 11, 4);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionBody { function_name, .. } => {
assert_eq!(function_name, "my_fixture");
}
other => panic!("Expected FunctionBody, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_async_fixture() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
async def my_async_fixture(
a,
) -> int:
return 42
"#;
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 6, 1);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature { function_name, .. } => {
assert_eq!(function_name, "my_async_fixture");
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
let ctx = db.get_completion_context(&path, 7, 4);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionBody { function_name, .. } => {
assert_eq!(function_name, "my_async_fixture");
}
other => panic!("Expected FunctionBody, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_empty_line_between_signature_and_body() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = r#"
import pytest
def test_foo(a):
pass
"#;
let path = PathBuf::from("/tmp/test/test_empty.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 4, 0);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionBody { function_name, .. } => {
assert_eq!(function_name, "test_foo");
}
other => panic!("Expected FunctionBody on blank line, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_ast_test_function_open_paren() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "def test_foo(";
let path = PathBuf::from("/tmp/test/test_incomplete.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 13);
assert!(ctx.is_some(), "Should get context from text fallback");
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
declared_params,
..
} => {
assert_eq!(function_name, "test_foo");
assert!(!is_fixture);
assert!(declared_params.is_empty());
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_ast_fixture_function_open_paren() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.fixture\ndef bar(";
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 1, 8);
assert!(
ctx.is_some(),
"Should get context from text fallback for fixture"
);
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
..
} => {
assert_eq!(function_name, "bar");
assert!(is_fixture);
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_ast_fixture_with_scope() {
use pytest_language_server::CompletionContext;
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let content = "@pytest.fixture(scope=\"session\")\ndef bar(";
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 1, 8);
assert!(
ctx.is_some(),
"Should get context with scope from text fallback"
);
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
fixture_scope,
..
} => {
assert_eq!(function_name, "bar");
assert!(is_fixture);
assert_eq!(fixture_scope, Some(FixtureScope::Session));
}
other => panic!(
"Expected FunctionSignature with session scope, got {:?}",
other
),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_ast_with_existing_params() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "def test_foo(existing_fixture, ";
let path = PathBuf::from("/tmp/test/test_params.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 31);
assert!(ctx.is_some(), "Should get context with existing params");
match ctx.unwrap() {
CompletionContext::FunctionSignature {
declared_params, ..
} => {
assert!(
declared_params.contains(&"existing_fixture".to_string()),
"Should contain existing_fixture in declared_params, got {:?}",
declared_params
);
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_ast_multiline_params() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "def test_foo(\n a,\n b,\n";
let path = PathBuf::from("/tmp/test/test_multiline.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 3, 0);
assert!(
ctx.is_some(),
"Should get context for multiline incomplete sig"
);
match ctx.unwrap() {
CompletionContext::FunctionSignature {
declared_params, ..
} => {
assert!(
declared_params.contains(&"a".to_string()),
"Should contain 'a', got {:?}",
declared_params
);
assert!(
declared_params.contains(&"b".to_string()),
"Should contain 'b', got {:?}",
declared_params
);
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_ast_async_test() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "async def test_foo(";
let path = PathBuf::from("/tmp/test/test_async.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 19);
assert!(ctx.is_some(), "Should get context for async test");
match ctx.unwrap() {
CompletionContext::FunctionSignature { function_name, .. } => {
assert_eq!(function_name, "test_foo");
}
other => panic!("Expected FunctionSignature for async test, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_ast_regular_function_no_completions() {
let db = FixtureDatabase::new();
let content = "def regular_func(";
let path = PathBuf::from("/tmp/test/test_regular.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 17);
assert!(
ctx.is_none(),
"Regular function should not get completion context"
);
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_ast_with_prior_complete_code() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = r#"import pytest
@pytest.fixture
def existing_fixture():
return 42
def test_new("#;
let path = PathBuf::from("/tmp/test/test_prior.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 6, 14);
assert!(
ctx.is_some(),
"Should get context from text fallback with prior valid code"
);
match ctx.unwrap() {
CompletionContext::FunctionSignature { function_name, .. } => {
assert_eq!(function_name, "test_new");
}
other => panic!("Expected FunctionSignature for test_new, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_fixture_bar_exact_user_scenario() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.fixture\ndef bar(";
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 1, 8);
assert!(
ctx.is_some(),
"Exact user scenario should produce completions"
);
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
..
} => {
assert_eq!(function_name, "bar");
assert!(is_fixture);
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_ast_fixture_with_existing_param() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.fixture\ndef bar(other_fixture, ";
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 1, 23);
assert!(ctx.is_some(), "Should get context with existing param");
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
declared_params,
..
} => {
assert_eq!(function_name, "bar");
assert!(is_fixture);
assert!(
declared_params.contains(&"other_fixture".to_string()),
"Should contain other_fixture, got {:?}",
declared_params
);
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_usefixtures_decorator() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.mark.usefixtures(";
let path = PathBuf::from("/tmp/test/test_usefixtures.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 25);
assert!(
ctx.is_some(),
"Should get usefixtures context from text fallback"
);
match ctx.unwrap() {
CompletionContext::UsefixturesDecorator => {}
other => panic!("Expected UsefixturesDecorator, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_usefixtures_with_function_below() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.mark.usefixtures(\ndef test_something():";
let path = PathBuf::from("/tmp/test/test_usefixtures.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 25);
assert!(
ctx.is_some(),
"Should get usefixtures context with function below"
);
match ctx.unwrap() {
CompletionContext::UsefixturesDecorator => {}
other => panic!("Expected UsefixturesDecorator, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_pytestmark_usefixtures() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "pytestmark = [\n pytest.mark.usefixtures(";
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 1, 28);
assert!(
ctx.is_some(),
"Should get usefixtures context in pytestmark list"
);
match ctx.unwrap() {
CompletionContext::UsefixturesDecorator => {}
other => panic!("Expected UsefixturesDecorator, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_pytestmark_usefixtures_unclosed_bracket() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "pytestmark = [\n pytest.mark.usefixtures(\n]";
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 1, 28);
assert!(
ctx.is_some(),
"Should get usefixtures context with unclosed bracket"
);
match ctx.unwrap() {
CompletionContext::UsefixturesDecorator => {}
other => panic!("Expected UsefixturesDecorator, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_pytestmark_usefixtures_closed_paren() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "pytestmark = [\n pytest.mark.usefixtures()\n]";
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 1, 28);
assert!(
ctx.is_some(),
"Should get usefixtures context when cursor inside ()"
);
match ctx.unwrap() {
CompletionContext::UsefixturesDecorator => {}
other => panic!("Expected UsefixturesDecorator, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_fixture_trailing_newline() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.fixture\ndef bar(\n";
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 2, 0);
assert!(
ctx.is_some(),
"Should get context on trailing newline phantom line"
);
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
..
} => {
assert_eq!(function_name, "bar");
assert!(is_fixture);
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_fixture_newline_with_indent() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.fixture\ndef bar(\n ";
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 2, 4);
assert!(
ctx.is_some(),
"Should get context on indented line after newline"
);
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
..
} => {
assert_eq!(function_name, "bar");
assert!(is_fixture);
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_fixture_no_paren() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.fixture\ndef bar";
let path = PathBuf::from("/tmp/test/conftest.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 1, 7);
assert!(ctx.is_some(), "Fixture without paren should get context");
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
..
} => {
assert_eq!(function_name, "bar");
assert!(is_fixture);
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_test_no_paren() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "def test_foo";
let path = PathBuf::from("/tmp/test/test_noparen.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 12);
assert!(
ctx.is_some(),
"Test function without paren should get context"
);
match ctx.unwrap() {
CompletionContext::FunctionSignature { function_name, .. } => {
assert_eq!(function_name, "test_foo");
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_incomplete_regular_func_no_paren_no_completions() {
let db = FixtureDatabase::new();
let content = "def regular";
let path = PathBuf::from("/tmp/test/test_regular.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 11);
assert!(
ctx.is_none(),
"Regular function without paren should not get context"
);
}
#[test]
fn test_completion_context_incomplete_closed_parens_with_colon_no_body() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "def test_bla():";
let path = PathBuf::from("/tmp/test/test_closed_parens.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 13);
assert!(
ctx.is_some(),
"Should get completion context inside closed parens with no body"
);
if let Some(CompletionContext::FunctionSignature {
function_name,
is_fixture,
..
}) = ctx
{
assert_eq!(function_name, "test_bla");
assert!(!is_fixture);
} else {
panic!("Expected FunctionSignature context");
}
}
#[test]
fn test_completion_context_incomplete_closed_parens_no_colon_no_body() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "def test_bla()";
let path = PathBuf::from("/tmp/test/test_no_colon.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 13);
assert!(
ctx.is_some(),
"Should get completion context inside closed parens without colon"
);
if let Some(CompletionContext::FunctionSignature {
function_name,
is_fixture,
..
}) = ctx
{
assert_eq!(function_name, "test_bla");
assert!(!is_fixture);
} else {
panic!("Expected FunctionSignature context");
}
}
#[test]
fn test_completion_context_incomplete_closed_parens_colon_trailing_newline() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "def test_bla():\n";
let path = PathBuf::from("/tmp/test/test_trailing_nl.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 13);
assert!(
ctx.is_some(),
"Should get completion context inside parens even with trailing newline"
);
if let Some(CompletionContext::FunctionSignature { function_name, .. }) = ctx {
assert_eq!(function_name, "test_bla");
} else {
panic!("Expected FunctionSignature context");
}
}
#[test]
fn test_completion_context_incomplete_fixture_closed_parens_no_body() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.fixture\ndef my_fixture():";
let path = PathBuf::from("/tmp/test/conftest_closed.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 1, 15);
assert!(
ctx.is_some(),
"Fixture with closed parens but no body should get context"
);
if let Some(CompletionContext::FunctionSignature {
function_name,
is_fixture,
..
}) = ctx
{
assert_eq!(function_name, "my_fixture");
assert!(is_fixture);
} else {
panic!("Expected FunctionSignature context");
}
}
#[test]
fn test_completion_context_incomplete_closed_parens_with_existing_params_no_body() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "def test_bla(my_fixture, ):";
let path = PathBuf::from("/tmp/test/test_with_params.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 25);
assert!(
ctx.is_some(),
"Should get context inside closed parens with existing params"
);
if let Some(CompletionContext::FunctionSignature {
function_name,
declared_params,
..
}) = ctx
{
assert_eq!(function_name, "test_bla");
assert!(
declared_params.contains(&"my_fixture".to_string()),
"Should list existing param 'my_fixture', got: {:?}",
declared_params
);
} else {
panic!("Expected FunctionSignature context");
}
}
#[test]
fn test_completion_context_complete_function_with_body_no_text_fallback() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "def test_bla():\n pass\n";
let path = PathBuf::from("/tmp/test/test_complete.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 13);
assert!(
ctx.is_some(),
"Complete function should still provide completions via AST path"
);
if let Some(CompletionContext::FunctionSignature { function_name, .. }) = ctx {
assert_eq!(function_name, "test_bla");
} else {
panic!("Expected FunctionSignature context");
}
}
#[test]
fn test_completion_context_valid_code_then_incomplete_closed_parens() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = r#"import pytest
@pytest.fixture
def existing():
return 1
def test_new():"#;
let path = PathBuf::from("/tmp/test/test_mixed.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 6, 13);
assert!(
ctx.is_some(),
"Should get context for incomplete function after valid code"
);
if let Some(CompletionContext::FunctionSignature { function_name, .. }) = ctx {
assert_eq!(function_name, "test_new");
} else {
panic!("Expected FunctionSignature context");
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_text_fallback_usefixtures_balanced_with_content() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.mark.usefixtures(\"a\")\ndef test_foo(";
let path = PathBuf::from("/tmp/test/test_balanced_usefixtures.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 28);
if let Some(CompletionContext::UsefixturesDecorator) = ctx {
panic!("Should NOT return UsefixturesDecorator for balanced usefixtures with content");
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_text_fallback_usefixtures_empty_parens() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.mark.usefixtures()\ndef test_foo(";
let path = PathBuf::from("/tmp/test/test_empty_usefixtures.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 25);
assert!(
ctx.is_some(),
"Empty usefixtures() should offer completions via text fallback"
);
match ctx.unwrap() {
CompletionContext::UsefixturesDecorator => {}
other => panic!("Expected UsefixturesDecorator, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_text_fallback_usefixtures_no_close_paren_on_line() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.mark.usefixtures(func())\ndef test_foo(";
let path = PathBuf::from("/tmp/test/test_nested_usefixtures.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 0, 30);
if let Some(CompletionContext::UsefixturesDecorator) = ctx {
panic!("Should NOT return UsefixturesDecorator for balanced usefixtures(func())");
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_text_fallback_usefixtures_multiline_paren_counting() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.mark.usefixtures(\n ";
let path = PathBuf::from("/tmp/test/test_multiline_usefixtures.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 1, 4);
assert!(
ctx.is_some(),
"Should get usefixtures context on continuation line"
);
match ctx.unwrap() {
CompletionContext::UsefixturesDecorator => {}
other => panic!("Expected UsefixturesDecorator, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_text_fallback_usefixtures_multiline_closed() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.mark.usefixtures(\n)\ndef test_foo(";
let path = PathBuf::from("/tmp/test/test_multiline_usefixtures_closed.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 1, 1);
if let Some(CompletionContext::UsefixturesDecorator) = ctx {
panic!("Should NOT return UsefixturesDecorator after balanced multiline usefixtures");
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_text_fallback_signature_closed_cursor_after() {
let db = FixtureDatabase::new();
let content = "def test_foo():\n \ndef broken";
let path = PathBuf::from("/tmp/test/test_sig_closed.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 1, 4);
assert!(
ctx.is_none(),
"Cursor after closed signature should not get completion context, got: {:?}",
ctx
);
}
#[test]
#[timeout(30000)]
fn test_completion_context_text_fallback_multiline_cursor_inside_parens() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "def test_foo(\n existing,\n ";
let path = PathBuf::from("/tmp/test/test_multiline_inside.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 2, 4);
assert!(
ctx.is_some(),
"Cursor inside multiline open parens should get context"
);
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
declared_params,
..
} => {
assert_eq!(function_name, "test_foo");
assert!(
declared_params.contains(&"existing".to_string()),
"Should contain 'existing', got {:?}",
declared_params
);
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_text_fallback_non_fixture_decorator_above() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = "@pytest.fixture\n@some_other_decorator\ndef bar(";
let path = PathBuf::from("/tmp/test/conftest_stacked.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 2, 8);
assert!(
ctx.is_some(),
"Should find fixture decorator above non-fixture decorator"
);
match ctx.unwrap() {
CompletionContext::FunctionSignature {
function_name,
is_fixture,
..
} => {
assert_eq!(function_name, "bar");
assert!(
is_fixture,
"Should be detected as fixture despite intermediate decorator"
);
}
other => panic!("Expected FunctionSignature, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_text_fallback_only_non_fixture_decorators() {
let db = FixtureDatabase::new();
let content = "@some_decorator\n@another_decorator\ndef helper_func(";
let path = PathBuf::from("/tmp/test/test_non_fixture_decs.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 2, 16);
assert!(
ctx.is_none(),
"Non-test, non-fixture function with only non-fixture decorators should not get context"
);
}
#[test]
#[timeout(30000)]
fn test_completion_context_multiline_signature_colon_on_separate_line() {
use pytest_language_server::CompletionContext;
let db = FixtureDatabase::new();
let content = r#"
import pytest
@pytest.fixture
def my_fixture(
param_a,
param_b
):
return 42
"#;
let path = PathBuf::from("/tmp/test/conftest_colon_separate.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 7, 1);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionSignature { function_name, .. } => {
assert_eq!(function_name, "my_fixture");
}
other => panic!("Expected FunctionSignature on '):', got {:?}", other),
}
let ctx = db.get_completion_context(&path, 8, 4);
assert!(ctx.is_some());
match ctx.unwrap() {
CompletionContext::FunctionBody { function_name, .. } => {
assert_eq!(function_name, "my_fixture");
}
other => panic!("Expected FunctionBody, got {:?}", other),
}
}
#[test]
#[timeout(30000)]
fn test_completion_context_text_fallback_fixture_scope_single_quotes() {
use pytest_language_server::CompletionContext;
use pytest_language_server::FixtureScope;
let db = FixtureDatabase::new();
let content = "@pytest.fixture(scope='module')\ndef bar(";
let path = PathBuf::from("/tmp/test/conftest_single_quote.py");
db.analyze_file(path.clone(), content);
let ctx = db.get_completion_context(&path, 1, 8);
assert!(ctx.is_some(), "Should get context with single-quoted scope");
match ctx.unwrap() {
CompletionContext::FunctionSignature {
fixture_scope,
is_fixture,
..
} => {
assert!(is_fixture);
assert_eq!(fixture_scope, Some(FixtureScope::Module));
}
other => panic!(
"Expected FunctionSignature with module scope, got {:?}",
other
),
}
}