#![allow(deprecated)]
use assert_cmd::Command;
use insta::assert_snapshot;
use ntest::timeout;
use predicates::prelude::*;
use pytest_language_server::FixtureDatabase;
use std::path::PathBuf;
use tempfile::tempdir;
fn normalize_path_in_output(output: &str) -> String {
let test_project_path = std::env::current_dir()
.unwrap()
.join("tests/test_project")
.canonicalize()
.unwrap();
output.replace(
&test_project_path.to_string_lossy().to_string(),
"<TEST_PROJECT_PATH>",
)
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_list_full_output() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("list")
.arg("tests/test_project")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let normalized = normalize_path_in_output(&stdout);
assert_snapshot!("cli_fixtures_list_full", normalized);
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_list_skip_unused() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("list")
.arg("tests/test_project")
.arg("--skip-unused")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let normalized = normalize_path_in_output(&stdout);
assert_snapshot!("cli_fixtures_list_skip_unused", normalized);
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_list_only_unused() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("list")
.arg("tests/test_project")
.arg("--only-unused")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let normalized = normalize_path_in_output(&stdout);
assert_snapshot!("cli_fixtures_list_only_unused", normalized);
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_list_nonexistent_path() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
cmd.arg("fixtures")
.arg("list")
.arg("/nonexistent/path/to/project")
.assert()
.failure();
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_list_empty_directory() {
let temp_dir = std::env::temp_dir().join("empty_test_dir");
std::fs::create_dir_all(&temp_dir).ok();
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("list")
.arg(&temp_dir)
.output()
.expect("Failed to execute command");
assert!(output.status.success());
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
#[timeout(30000)]
fn test_cli_help_message() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
cmd.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Language Server Protocol"))
.stdout(predicate::str::contains("fixtures"))
.stdout(predicate::str::contains("Fixture-related"));
}
#[test]
#[timeout(30000)]
fn test_cli_version() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
cmd.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_help() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
cmd.arg("fixtures")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("list"))
.stdout(predicate::str::contains("List all fixtures"));
}
#[test]
#[timeout(30000)]
fn test_cli_invalid_subcommand() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
cmd.arg("invalid").assert().failure();
}
#[test]
#[timeout(30000)]
fn test_cli_conflicting_flags() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
cmd.arg("fixtures")
.arg("list")
.arg("tests/test_project")
.arg("--skip-unused")
.arg("--only-unused")
.assert()
.failure();
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_unused_text_output() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("unused")
.arg("tests/test_project")
.output()
.expect("Failed to execute command");
assert!(!output.status.success());
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Found") && stdout.contains("unused fixture"));
assert!(
stdout.contains("iterator_fixture"),
"iterator_fixture should be reported as unused"
);
assert!(
!stdout.contains("auto_cleanup"),
"autouse fixture auto_cleanup should NOT be reported as unused"
);
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_unused_json_output() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("unused")
.arg("tests/test_project")
.arg("--format")
.arg("json")
.output()
.expect("Failed to execute command");
assert!(!output.status.success());
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&stdout);
assert!(parsed.is_ok(), "Output should be valid JSON: {}", stdout);
let json = parsed.unwrap();
assert!(json.is_array(), "JSON output should be an array");
let arr = json.as_array().unwrap();
assert!(!arr.is_empty(), "Should have at least one unused fixture");
for item in arr {
assert!(
item.get("file").is_some(),
"Each item should have 'file' key"
);
assert!(
item.get("fixture").is_some(),
"Each item should have 'fixture' key"
);
}
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_unused_exit_code_zero_when_all_used() {
let temp_dir = std::env::temp_dir().join("test_all_used");
std::fs::create_dir_all(&temp_dir).ok();
std::fs::write(
temp_dir.join("conftest.py"),
r#"
import pytest
@pytest.fixture
def my_fixture():
return "value"
"#,
)
.ok();
std::fs::write(
temp_dir.join("test_example.py"),
r#"
def test_something(my_fixture):
assert my_fixture == "value"
"#,
)
.ok();
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("unused")
.arg(&temp_dir)
.output()
.expect("Failed to execute command");
assert!(output.status.success());
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("No unused fixtures found"));
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_unused_nonexistent_path() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
cmd.arg("fixtures")
.arg("unused")
.arg("/nonexistent/path/to/project")
.assert()
.failure();
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_unused_help() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
cmd.arg("fixtures")
.arg("unused")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("unused fixtures"))
.stdout(predicate::str::contains("--format"));
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_list_only_unused_excludes_autouse() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("list")
.arg("tests/test_project")
.arg("--only-unused")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("auto_cleanup"),
"autouse fixture auto_cleanup should NOT appear in --only-unused output"
);
assert!(
stdout.contains("iterator_fixture"),
"non-autouse unused fixture should appear"
);
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_list_skip_unused_includes_autouse() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("list")
.arg("tests/test_project")
.arg("--skip-unused")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("auto_cleanup"),
"autouse fixture auto_cleanup should appear in --skip-unused output"
);
assert!(
!stdout.contains("iterator_fixture"),
"non-autouse unused fixture should NOT appear in --skip-unused output"
);
}
#[test]
#[timeout(30000)]
fn test_cli_fixtures_list_shows_autouse_label() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("list")
.arg("tests/test_project")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("autouse=True"),
"full list should show 'autouse=True' label for autouse fixtures"
);
assert!(
!stdout.contains("auto_cleanup") || !stdout.contains("auto_cleanup (unused)"),
"auto_cleanup should not be shown as 'unused'"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_scan_expanded_test_project() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
assert!(db.definitions.get("sample_fixture").is_some());
assert!(db.definitions.get("api_client").is_some());
assert!(db.definitions.get("api_token").is_some());
assert!(db.definitions.get("mock_response").is_some());
assert!(db.definitions.get("db_connection").is_some());
assert!(db.definitions.get("db_cursor").is_some());
assert!(db.definitions.get("transaction").is_some());
assert!(db.definitions.get("temp_file").is_some());
assert!(db.definitions.get("temp_dir").is_some());
assert!(db.definitions.get("auto_cleanup").is_some());
assert!(db.definitions.get("session_fixture").is_some());
assert!(db.definitions.get("module_fixture").is_some());
assert!(db.definitions.get("local_fixture").is_some());
}
#[test]
#[timeout(30000)]
fn test_e2e_fixture_hierarchy_resolution() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let test_file = project_path.join("api/test_endpoints.py");
let test_file_canonical = test_file.canonicalize().unwrap();
let available = db.get_available_fixtures(&test_file_canonical);
let names: Vec<&str> = available.iter().map(|f| f.name.as_str()).collect();
assert!(names.contains(&"api_client"));
assert!(names.contains(&"api_token"));
assert!(names.contains(&"sample_fixture"));
assert!(!names.contains(&"db_connection"));
}
#[test]
#[timeout(30000)]
fn test_e2e_fixture_dependency_chain() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let transaction = db.definitions.get("transaction").unwrap();
assert_eq!(transaction.len(), 1);
let db_cursor = db.definitions.get("db_cursor").unwrap();
assert_eq!(db_cursor.len(), 1);
let db_connection = db.definitions.get("db_connection").unwrap();
assert_eq!(db_connection.len(), 1);
}
#[test]
#[timeout(30000)]
fn test_e2e_autouse_fixture_detection() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let autouse = db.definitions.get("auto_cleanup");
assert!(autouse.is_some());
let auto_cleanup = &autouse.unwrap()[0];
assert!(
auto_cleanup.autouse,
"auto_cleanup should have autouse=true"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_autouse_fixture_not_reported_unused() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let unused = db.get_unused_fixtures();
let unused_names: Vec<&str> = unused.iter().map(|(_, name)| name.as_str()).collect();
assert!(
!unused_names.contains(&"auto_cleanup"),
"autouse fixture auto_cleanup should NOT be reported as unused"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_scoped_fixtures() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
assert!(db.definitions.get("session_fixture").is_some());
assert!(db.definitions.get("module_fixture").is_some());
}
#[test]
#[timeout(30000)]
fn test_e2e_fixture_usage_in_test_file() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let test_file = project_path.join("api/test_endpoints.py");
let test_file_canonical = test_file.canonicalize().unwrap();
let usages = db.usages.get(&test_file_canonical);
assert!(
usages.is_some(),
"No usages found for {:?}",
test_file_canonical
);
let usages = usages.unwrap();
assert!(
usages.len() >= 3,
"Expected at least 3 usages, found {}",
usages.len()
);
let usage_names: Vec<&str> = usages.iter().map(|u| u.name.as_str()).collect();
assert!(usage_names.contains(&"api_client"));
assert!(usage_names.contains(&"api_token"));
}
#[test]
#[timeout(30000)]
fn test_e2e_find_references_across_project() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let references = db.find_fixture_references("api_client");
assert!(!references.is_empty());
}
#[test]
#[timeout(30000)]
fn test_e2e_fixture_override_in_subdirectory() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let test_file = project_path.join("subdir/test_override.py");
if test_file.exists() {
let test_file_canonical = test_file.canonicalize().unwrap();
let available = db.get_available_fixtures(&test_file_canonical);
let names: Vec<&str> = available.iter().map(|f| f.name.as_str()).collect();
assert!(!names.is_empty());
}
}
#[test]
#[timeout(30000)]
fn test_e2e_scan_performance() {
use std::time::Instant;
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
let start = Instant::now();
db.scan_workspace(&project_path);
let duration = start.elapsed();
assert!(
duration.as_secs() < 1,
"Scanning took too long: {:?}",
duration
);
}
#[test]
#[timeout(30000)]
fn test_e2e_repeated_analysis() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
use std::time::Instant;
let start = Instant::now();
db.scan_workspace(&project_path);
let duration = start.elapsed();
assert!(duration.as_millis() < 500, "Re-scanning took too long");
}
#[test]
#[timeout(30000)]
fn test_e2e_malformed_python_file() {
let db = FixtureDatabase::new();
let temp_dir = std::env::temp_dir().join("test_malformed");
std::fs::create_dir_all(&temp_dir).ok();
let bad_file = temp_dir.join("test_bad.py");
std::fs::write(
&bad_file,
"def test_something(\n this is not valid python",
)
.ok();
db.scan_workspace(&temp_dir);
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
#[timeout(30000)]
fn test_e2e_mixed_valid_and_invalid_files() {
let db = FixtureDatabase::new();
let temp_dir = std::env::temp_dir().join("test_mixed");
std::fs::create_dir_all(&temp_dir).ok();
std::fs::write(
temp_dir.join("test_valid.py"),
r#"
import pytest
@pytest.fixture
def valid_fixture():
return "valid"
def test_something(valid_fixture):
pass
"#,
)
.ok();
std::fs::write(
temp_dir.join("test_invalid.py"),
"def test_broken(\n invalid syntax here",
)
.ok();
db.scan_workspace(&temp_dir);
assert!(db.definitions.get("valid_fixture").is_some());
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
#[timeout(30000)]
fn test_e2e_renamed_fixtures_in_test_project() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
assert!(
db.definitions.contains_key("renamed_db"),
"Should find fixture by alias 'renamed_db'"
);
assert!(
db.definitions.contains_key("user"),
"Should find fixture by alias 'user'"
);
assert!(
db.definitions.contains_key("normal_fixture"),
"Should find normal fixture by function name"
);
assert!(
!db.definitions.contains_key("internal_database_fixture"),
"Internal function name should not be registered"
);
assert!(
!db.definitions.contains_key("create_user_fixture"),
"Internal function name should not be registered"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_renamed_fixture_references() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let renamed_db_defs = db.definitions.get("renamed_db");
assert!(renamed_db_defs.is_some());
let def = &renamed_db_defs.unwrap()[0];
let refs = db.find_references_for_definition(def);
assert!(
refs.len() >= 3,
"Should have at least 3 references to renamed_db, got {}",
refs.len()
);
assert!(
refs.iter().all(|r| r.name == "renamed_db"),
"All references should use alias name"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_renamed_fixture_goto_definition() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let test_file = project_path
.join("test_renamed_fixtures.py")
.canonicalize()
.unwrap();
let fixture_name = db.find_fixture_at_position(&test_file, 23, 30);
assert_eq!(
fixture_name,
Some("renamed_db".to_string()),
"Should find fixture name at position"
);
let definition = db.find_fixture_definition(&test_file, 23, 30);
assert!(definition.is_some(), "Should find fixture definition");
let def = definition.unwrap();
assert_eq!(def.name, "renamed_db", "Definition should have alias name");
}
#[test]
#[timeout(30000)]
fn test_e2e_cli_fixtures_list_with_renamed() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("list")
.arg("tests/test_project")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("renamed_db"),
"Output should contain 'renamed_db'"
);
assert!(stdout.contains("user"), "Output should contain 'user'");
assert!(
stdout.contains("normal_fixture"),
"Output should contain 'normal_fixture'"
);
assert!(
!stdout.contains("internal_database_fixture"),
"Should not show internal function name"
);
assert!(
!stdout.contains("create_user_fixture"),
"Should not show internal function name"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_class_based_tests_fixture_usage() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
assert!(
db.definitions.contains_key("shared_fixture"),
"Should find shared_fixture"
);
assert!(
db.definitions.contains_key("another_fixture"),
"Should find another_fixture"
);
let test_file = project_path
.join("test_class_based.py")
.canonicalize()
.unwrap();
let usages = db.usages.get(&test_file);
assert!(
usages.is_some(),
"Should have usages in test_class_based.py"
);
let usages = usages.unwrap();
let shared_usages: Vec<_> = usages
.iter()
.filter(|u| u.name == "shared_fixture")
.collect();
assert!(
shared_usages.len() >= 4,
"shared_fixture should be used at least 4 times (by test methods in classes), got {}",
shared_usages.len()
);
let another_usages: Vec<_> = usages
.iter()
.filter(|u| u.name == "another_fixture")
.collect();
assert!(
another_usages.len() >= 2,
"another_fixture should be used at least 2 times, got {}",
another_usages.len()
);
}
#[test]
#[timeout(30000)]
fn test_e2e_class_based_fixture_references() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let shared_defs = db.definitions.get("shared_fixture");
assert!(shared_defs.is_some());
let def = &shared_defs.unwrap()[0];
let refs = db.find_references_for_definition(def);
assert!(
refs.len() >= 4,
"shared_fixture should have at least 4 references from class test methods, got {}",
refs.len()
);
}
#[test]
#[timeout(30000)]
fn test_e2e_cli_class_based_fixtures_shown_as_used() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("list")
.arg("tests/test_project")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("shared_fixture") && !stdout.contains("shared_fixture (unused)"),
"shared_fixture should be marked as used"
);
assert!(
stdout.contains("another_fixture") && !stdout.contains("another_fixture (unused)"),
"another_fixture should be marked as used"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_keyword_only_fixture_detection() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let test_file = project_path
.join("test_kwonly_fixtures.py")
.canonicalize()
.unwrap();
let usages = db.usages.get(&test_file);
assert!(
usages.is_some(),
"Usages should be detected for test_kwonly_fixtures.py"
);
let usages = usages.unwrap();
assert!(
usages.iter().any(|u| u.name == "sample_fixture"),
"sample_fixture should be detected as used"
);
assert!(
usages.iter().any(|u| u.name == "another_fixture"),
"another_fixture should be detected as used"
);
assert!(
usages.iter().any(|u| u.name == "shared_resource"),
"shared_resource should be detected as used"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_keyword_only_no_undeclared_fixtures() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let test_file = project_path
.join("test_kwonly_fixtures.py")
.canonicalize()
.unwrap();
let undeclared = db.get_undeclared_fixtures(&test_file);
assert_eq!(
undeclared.len(),
0,
"Keyword-only fixtures should not be flagged as undeclared. Found: {:?}",
undeclared.iter().map(|u| &u.name).collect::<Vec<_>>()
);
}
#[test]
#[timeout(30000)]
fn test_e2e_keyword_only_go_to_definition() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let test_file = project_path
.join("test_kwonly_fixtures.py")
.canonicalize()
.unwrap();
let conftest_file = project_path.join("conftest.py").canonicalize().unwrap();
let usages = db.usages.get(&test_file);
assert!(usages.is_some());
let usages = usages.unwrap();
let sample_usage = usages.iter().find(|u| u.name == "sample_fixture");
assert!(
sample_usage.is_some(),
"sample_fixture usage should be found"
);
let sample_usage = sample_usage.unwrap();
let definition = db.find_fixture_definition(
&test_file,
(sample_usage.line - 1) as u32,
sample_usage.start_char as u32,
);
assert!(
definition.is_some(),
"Definition should be found for keyword-only fixture"
);
let def = definition.unwrap();
assert_eq!(def.name, "sample_fixture");
assert_eq!(def.file_path, conftest_file);
}
#[test]
#[timeout(30000)]
fn test_e2e_imported_fixtures_are_detected() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let imported = db.definitions.get("imported_fixture");
assert!(
imported.is_some(),
"imported_fixture should be detected from fixture_module.py"
);
let another_imported = db.definitions.get("another_imported_fixture");
assert!(
another_imported.is_some(),
"another_imported_fixture should be detected from fixture_module.py"
);
let explicit = db.definitions.get("explicitly_imported");
assert!(
explicit.is_some(),
"explicitly_imported should be detected from fixture_module.py"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_imported_fixtures_available_in_test_file() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let test_file = project_path.join("imported_fixtures/test_uses_imported.py");
let test_file_canonical = test_file.canonicalize().unwrap();
let available = db.get_available_fixtures(&test_file_canonical);
let names: Vec<&str> = available.iter().map(|f| f.name.as_str()).collect();
assert!(
names.contains(&"imported_fixture"),
"imported_fixture should be available in test file"
);
assert!(
names.contains(&"another_imported_fixture"),
"another_imported_fixture should be available in test file"
);
assert!(
names.contains(&"explicitly_imported"),
"explicitly_imported should be available in test file"
);
assert!(
names.contains(&"local_fixture"),
"local_fixture should be available in test file"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_imported_fixtures_go_to_definition() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let test_file = project_path.join("imported_fixtures/test_uses_imported.py");
let test_file_canonical = test_file.canonicalize().unwrap();
let fixture_module = project_path.join("imported_fixtures/fixture_module.py");
let fixture_module_canonical = fixture_module.canonicalize().unwrap();
let usages = db.usages.get(&test_file_canonical);
assert!(usages.is_some(), "Test file should have fixture usages");
let usages = usages.unwrap();
let imported_usage = usages
.iter()
.find(|u| u.name == "imported_fixture")
.expect("Should find imported_fixture usage");
let definition = db.find_fixture_definition(
&test_file_canonical,
(imported_usage.line - 1) as u32,
imported_usage.start_char as u32,
);
assert!(
definition.is_some(),
"Should find definition for imported_fixture"
);
let def = definition.unwrap();
assert_eq!(def.name, "imported_fixture");
assert_eq!(
def.file_path, fixture_module_canonical,
"Definition should be in fixture_module.py"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_imported_fixtures_find_references() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let definitions = db.definitions.get("imported_fixture");
assert!(definitions.is_some());
let def = definitions.unwrap().first().unwrap().clone();
let references = db.find_references_for_definition(&def);
assert!(
!references.is_empty(),
"Should find references to imported_fixture"
);
let test_file = project_path.join("imported_fixtures/test_uses_imported.py");
let test_file_canonical = test_file.canonicalize().unwrap();
let has_test_ref = references
.iter()
.any(|r| r.file_path == test_file_canonical);
assert!(
has_test_ref,
"Should have a reference in test_uses_imported.py"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_imported_fixtures_no_undeclared_warning() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let test_file = project_path.join("imported_fixtures/test_uses_imported.py");
let test_file_canonical = test_file.canonicalize().unwrap();
let undeclared = db.get_undeclared_fixtures(&test_file_canonical);
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"
);
assert!(
!undeclared_names.contains(&"another_imported_fixture"),
"another_imported_fixture should not be flagged as undeclared"
);
assert!(
!undeclared_names.contains(&"local_fixture"),
"local_fixture should not be flagged as undeclared"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_imported_fixtures_cache_performance() {
use std::time::Instant;
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let conftest_file = project_path.join("imported_fixtures/conftest.py");
let conftest_canonical = conftest_file.canonicalize().unwrap();
let start = Instant::now();
let mut visited = std::collections::HashSet::new();
let first_result = db.get_imported_fixtures(&conftest_canonical, &mut visited);
let first_duration = start.elapsed();
let start = Instant::now();
let mut visited = std::collections::HashSet::new();
let second_result = db.get_imported_fixtures(&conftest_canonical, &mut visited);
let second_duration = start.elapsed();
assert_eq!(first_result, second_result);
eprintln!(
"Import cache: first call {:?}, second call {:?}",
first_duration, second_duration
);
}
#[test]
#[timeout(30000)]
fn test_e2e_imported_fixtures_cli_shows_them() {
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("list")
.arg("tests/test_project/imported_fixtures")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("imported_fixture"),
"CLI output should list imported_fixture"
);
assert!(
stdout.contains("another_imported_fixture"),
"CLI output should list another_imported_fixture"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_transitive_imported_fixtures() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let deep_fixture = db.definitions.get("deep_nested_fixture");
assert!(
deep_fixture.is_some(),
"deep_nested_fixture should be detected from nested/deep_fixtures.py"
);
let another_deep = db.definitions.get("another_deep_fixture");
assert!(
another_deep.is_some(),
"another_deep_fixture should be detected from nested/deep_fixtures.py"
);
let test_file = project_path.join("imported_fixtures/test_uses_imported.py");
let test_file_canonical = test_file.canonicalize().unwrap();
let available = db.get_available_fixtures(&test_file_canonical);
let names: Vec<&str> = available.iter().map(|f| f.name.as_str()).collect();
assert!(
names.contains(&"deep_nested_fixture"),
"deep_nested_fixture should be available via transitive import"
);
assert!(
names.contains(&"another_deep_fixture"),
"another_deep_fixture should be available via transitive import"
);
}
#[test]
#[timeout(30000)]
fn test_e2e_transitive_imports_go_to_definition() {
let db = FixtureDatabase::new();
let project_path = PathBuf::from("tests/test_project");
db.scan_workspace(&project_path);
let test_file = project_path.join("imported_fixtures/test_uses_imported.py");
let test_file_canonical = test_file.canonicalize().unwrap();
let deep_fixtures_file = project_path.join("imported_fixtures/nested/deep_fixtures.py");
let deep_fixtures_canonical = deep_fixtures_file.canonicalize().unwrap();
let usages = db.usages.get(&test_file_canonical);
assert!(usages.is_some(), "Test file should have fixture usages");
let usages = usages.unwrap();
let deep_usage = usages
.iter()
.find(|u| u.name == "deep_nested_fixture")
.expect("Should find deep_nested_fixture usage");
let definition = db.find_fixture_definition(
&test_file_canonical,
(deep_usage.line - 1) as u32,
deep_usage.start_char as u32,
);
assert!(
definition.is_some(),
"Should find definition for deep_nested_fixture"
);
let def = definition.unwrap();
assert_eq!(def.name, "deep_nested_fixture");
assert_eq!(
def.file_path, deep_fixtures_canonical,
"Definition should be in nested/deep_fixtures.py"
);
}
fn setup_editable_install_workspace() -> (tempfile::TempDir, tempfile::TempDir) {
use std::fs;
let workspace = tempdir().unwrap();
let external_src = tempdir().unwrap();
let conftest = r#"
import pytest
@pytest.fixture
def local_fixture():
"""A fixture defined in the workspace."""
return "local"
"#;
fs::write(workspace.path().join("conftest.py"), conftest).unwrap();
let test_file = r#"
def test_uses_both(local_fixture, editable_plugin_fixture):
pass
"#;
fs::write(workspace.path().join("test_example.py"), test_file).unwrap();
let pkg_dir = external_src.path().join("myplugin");
fs::create_dir_all(&pkg_dir).unwrap();
fs::write(pkg_dir.join("__init__.py"), "").unwrap();
let plugin_content = r#"
import pytest
@pytest.fixture
def editable_plugin_fixture():
"""Fixture from an external editable install."""
return "editable"
"#;
fs::write(pkg_dir.join("plugin.py"), plugin_content).unwrap();
let site_packages = workspace.path().join(".venv/lib/python3.12/site-packages");
fs::create_dir_all(&site_packages).unwrap();
let dist_info = site_packages.join("myplugin-0.1.0.dist-info");
fs::create_dir_all(&dist_info).unwrap();
let direct_url = serde_json::json!({
"url": format!("file://{}", external_src.path().display()),
"dir_info": { "editable": true }
});
fs::write(
dist_info.join("direct_url.json"),
serde_json::to_string(&direct_url).unwrap(),
)
.unwrap();
let entry_points = "[pytest11]\nmyplugin = myplugin.plugin\n";
fs::write(dist_info.join("entry_points.txt"), entry_points).unwrap();
let pth_content = format!("{}\n", external_src.path().display());
fs::write(
site_packages.join("__editable__.myplugin-0.1.0.pth"),
&pth_content,
)
.unwrap();
(workspace, external_src)
}
#[test]
#[timeout(30000)]
fn test_e2e_editable_install_fixtures_in_tree() {
let (workspace, _src) = setup_editable_install_workspace();
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("list")
.arg(workspace.path())
.output()
.expect("Failed to execute command");
assert!(output.status.success(), "CLI should exit successfully");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("editable_plugin_fixture"),
"Editable install fixture should appear in `fixtures list` output.\nGot:\n{}",
stdout
);
assert!(
stdout.contains("local_fixture"),
"Local fixture should still appear in output.\nGot:\n{}",
stdout
);
assert!(
stdout.contains("(editable install)"),
"Editable install directory should be labelled.\nGot:\n{}",
stdout
);
}
#[test]
#[timeout(30000)]
fn test_e2e_editable_install_fixtures_skip_unused() {
let (workspace, _src) = setup_editable_install_workspace();
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("list")
.arg("--skip-unused")
.arg(workspace.path())
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("editable_plugin_fixture"),
"Used editable fixture should appear with --skip-unused.\nGot:\n{}",
stdout
);
assert!(
stdout.contains("local_fixture"),
"Used local fixture should appear with --skip-unused.\nGot:\n{}",
stdout
);
}
#[test]
#[timeout(30000)]
fn test_e2e_editable_install_fixtures_only_unused() {
let (workspace, _src) = setup_editable_install_workspace();
let mut cmd = Command::cargo_bin("pytest-language-server").unwrap();
let output = cmd
.arg("fixtures")
.arg("list")
.arg("--only-unused")
.arg(workspace.path())
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("editable_plugin_fixture"),
"Used editable fixture should NOT appear with --only-unused.\nGot:\n{}",
stdout
);
assert!(
!stdout.contains("local_fixture"),
"Used local fixture should NOT appear with --only-unused.\nGot:\n{}",
stdout
);
}