use async_trait::async_trait;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use crate::clangd::config::ClangdConfig;
use crate::clangd::error::ClangdSessionError;
use crate::clangd::session::ClangdSessionTrait;
use crate::lsp::traits::{LspClientTrait, MockLspClientTrait};
use crate::project::{ProjectComponent, ProjectError, ProjectWorkspace};
pub struct MockClangdSession {
config: ClangdConfig,
mock_client: MockLspClientTrait,
started_at: Instant,
stderr_handler: Option<Arc<dyn Fn(String) + Send + Sync>>,
should_fail_close: bool,
}
impl MockClangdSession {
pub fn new(config: ClangdConfig) -> Self {
let mut mock_client = MockLspClientTrait::new();
mock_client.expect_is_initialized().returning(|| true);
mock_client
.expect_shutdown()
.returning(|| Box::pin(async { Ok(()) }));
mock_client
.expect_close()
.returning(|| Box::pin(async { Ok(()) }));
mock_client
.expect_open_text_document()
.returning(|_, _, _, _| Box::pin(async { Ok(()) }));
Self {
config,
mock_client,
started_at: Instant::now(),
stderr_handler: None,
should_fail_close: false,
}
}
pub async fn new_with_failure(
config: ClangdConfig,
should_fail: bool,
) -> Result<Self, ClangdSessionError> {
if should_fail {
Err(ClangdSessionError::startup_failed(
"Mock constructor failure",
))
} else {
Ok(Self::new(config))
}
}
pub fn set_close_failure(&mut self, should_fail: bool) {
self.should_fail_close = should_fail;
}
pub fn set_stderr_handler<F>(&mut self, handler: F)
where
F: Fn(String) + Send + Sync + 'static,
{
self.stderr_handler = Some(Arc::new(handler));
}
pub fn simulate_stderr(&self, line: impl Into<String>) {
if let Some(handler) = &self.stderr_handler {
handler(line.into());
}
}
}
#[async_trait]
impl ClangdSessionTrait for MockClangdSession {
type Error = ClangdSessionError;
type Client = MockLspClientTrait;
async fn close(self) -> Result<(), Self::Error> {
if self.should_fail_close {
Err(ClangdSessionError::shutdown_failed("Mock close failure"))
} else {
Ok(())
}
}
fn client(&self) -> &Self::Client {
&self.mock_client
}
fn client_mut(&mut self) -> &mut Self::Client {
&mut self.mock_client
}
fn config(&self) -> &ClangdConfig {
&self.config
}
fn uptime(&self) -> std::time::Duration {
self.started_at.elapsed()
}
fn working_directory(&self) -> &PathBuf {
&self.config.working_directory
}
fn build_directory(&self) -> &PathBuf {
&self.config.build_directory
}
}
#[derive(Debug)]
pub struct MockLspClient {
initialized: bool,
}
impl MockLspClient {
pub fn new() -> Self {
Self { initialized: true }
}
pub fn is_initialized(&self) -> bool {
self.initialized
}
}
pub struct MockProjectWorkspace {
project_root: PathBuf,
components: Vec<ProjectComponent>,
}
impl MockProjectWorkspace {
pub fn new(project_root: PathBuf) -> Self {
Self {
project_root,
components: Vec::new(),
}
}
pub fn add_component(
&mut self,
build_dir: PathBuf,
provider_type: &str,
generator: &str,
build_type: &str,
) -> Result<(), ProjectError> {
let compilation_database_path = build_dir.join("compile_commands.json");
let component = ProjectComponent {
build_dir_path: build_dir,
source_root_path: self.project_root.clone(),
compilation_database_path,
provider_type: provider_type.to_string(),
generator: generator.to_string(),
build_type: build_type.to_string(),
build_options: std::collections::HashMap::new(),
};
self.components.push(component);
Ok(())
}
pub fn into_project_workspace(self) -> ProjectWorkspace {
ProjectWorkspace::new(self.project_root, self.components, 1)
}
pub fn get_components_for_provider(&self, provider_type: &str) -> Vec<&ProjectComponent> {
self.components
.iter()
.filter(|c| c.provider_type == provider_type)
.collect()
}
}
pub mod test_helpers {
use super::*;
use crate::clangd::config::ClangdConfigBuilder;
pub struct TestClangdPaths;
impl TestClangdPaths {
pub const MOCK: &'static str = "mock-clangd";
pub const INVALID: &'static str = "nonexistent-clangd-binary";
}
pub enum TestConfigType<'a> {
Mock,
#[cfg(feature = "clangd-integration-tests")]
Integration,
Custom(&'a str),
Failing,
}
pub fn create_test_config(
project_root: &PathBuf,
build_dir: &PathBuf,
config_type: TestConfigType,
) -> Result<ClangdConfig, crate::clangd::error::ClangdConfigError> {
let clangd_path = match config_type {
TestConfigType::Mock => TestClangdPaths::MOCK,
#[cfg(feature = "clangd-integration-tests")]
TestConfigType::Integration => {
#[cfg(test)]
{
&crate::test_utils::get_test_clangd_path()
}
#[cfg(not(test))]
{
"/usr/bin/clangd" }
}
TestConfigType::Custom(path) => path,
TestConfigType::Failing => TestClangdPaths::INVALID,
};
ClangdConfigBuilder::new()
.working_directory(project_root)
.build_directory(build_dir)
.clangd_path(clangd_path)
.build()
}
pub fn create_mock_session(
project_root: &PathBuf,
build_dir: &PathBuf,
) -> Result<MockClangdSession, crate::clangd::error::ClangdConfigError> {
let config = create_test_config(project_root, build_dir, TestConfigType::Mock)?;
Ok(MockClangdSession::new(config))
}
#[cfg(test)]
pub fn create_session_with_mock_dependencies(
config: ClangdConfig,
) -> super::super::session::ClangdSession<
crate::io::process::MockProcessManager,
crate::lsp::testing::MockLspClientTrait,
> {
use crate::clangd::file_manager::ClangdFileManager;
use crate::clangd::index::IndexProgressMonitor;
use crate::io::process::MockProcessManager;
use crate::lsp::testing::MockLspClientTrait;
let mock_process = MockProcessManager::new();
let mut mock_lsp = MockLspClientTrait::new();
mock_lsp.expect_is_initialized().returning(|| true);
mock_lsp
.expect_shutdown()
.returning(|| Box::pin(async { Ok(()) }));
mock_lsp
.expect_close()
.returning(|| Box::pin(async { Ok(()) }));
mock_lsp
.expect_open_text_document()
.returning(|_, _, _, _| Box::pin(async { Ok(()) }));
let _file_manager = ClangdFileManager::new();
let index_progress_monitor = IndexProgressMonitor::new();
let log_monitor = crate::clangd::log_monitor::LogMonitor::new();
super::super::session::ClangdSession::with_dependencies(
config,
mock_process,
mock_lsp,
index_progress_monitor,
log_monitor,
)
}
#[cfg(all(test, feature = "clangd-integration-tests"))]
pub async fn create_integration_test_session() -> Result<
(
crate::test_utils::integration::TestProject,
crate::clangd::session::ClangdSession,
),
Box<dyn std::error::Error>,
> {
let test_project = crate::test_utils::integration::TestProject::new().await?;
test_project.cmake_configure().await?;
let config = create_test_config(
&test_project.project_root,
&test_project.build_dir,
TestConfigType::Integration,
)?;
let session = crate::clangd::session::ClangdSession::new(config).await?;
Ok((test_project, session))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use test_helpers::*;
#[tokio::test]
async fn test_mock_session_lifecycle() {
let (_temp_dir, project_root, build_dir) =
crate::test_utils::project::create_mock_build_folder();
let session = create_mock_session(&project_root, &build_dir).unwrap();
assert_eq!(session.working_directory(), &project_root);
assert_eq!(session.build_directory(), &build_dir);
assert!(session.uptime().as_nanos() > 0);
let result = session.close().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_mock_session_construction_failure() {
let (_temp_dir, project_root, build_dir) =
crate::test_utils::project::create_mock_build_folder();
let config = create_test_config(&project_root, &build_dir, TestConfigType::Mock).unwrap();
let result = MockClangdSession::new_with_failure(config, true).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_mock_session_close_failure() {
let (_temp_dir, project_root, build_dir) =
crate::test_utils::project::create_mock_build_folder();
let mut session = create_mock_session(&project_root, &build_dir).unwrap();
session.set_close_failure(true);
let result = session.close().await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_mock_session_stderr_handling() {
let (_temp_dir, project_root, build_dir) =
crate::test_utils::project::create_mock_build_folder();
let mut session = create_mock_session(&project_root, &build_dir).unwrap();
let stderr_lines = Arc::new(Mutex::new(Vec::<String>::new()));
let stderr_lines_clone = Arc::clone(&stderr_lines);
session.set_stderr_handler(move |line| {
stderr_lines_clone.lock().unwrap().push(line);
});
session.simulate_stderr("test error line");
{
let lines = stderr_lines.lock().unwrap();
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], "test error line");
}
session.close().await.unwrap();
}
#[tokio::test]
async fn test_resource_cleanup_on_construction_failure() {
let (_temp_dir, project_root, build_dir) =
crate::test_utils::project::create_mock_build_folder();
let config = create_test_config(&project_root, &build_dir, TestConfigType::Mock).unwrap();
let result = MockClangdSession::new_with_failure(config, true).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_session_drop_behavior() {
let (_temp_dir, project_root, build_dir) =
crate::test_utils::project::create_mock_build_folder();
let session = create_mock_session(&project_root, &build_dir).unwrap();
assert_eq!(session.working_directory(), &project_root);
assert_eq!(session.build_directory(), &build_dir);
assert!(session.uptime().as_nanos() > 0);
}
#[test]
fn test_mock_meta_project() {
let project_root = PathBuf::from("/test/project");
let mock_meta = MockProjectWorkspace::new(project_root.clone());
assert_eq!(mock_meta.project_root, project_root);
assert_eq!(mock_meta.components.len(), 0);
}
#[tokio::test]
async fn test_trait_design_violation_fix() {
let (_temp_dir, project_root, build_dir) =
crate::test_utils::project::create_mock_build_folder();
let session = create_mock_session(&project_root, &build_dir).unwrap();
let client = session.client();
assert!(client.is_initialized());
let mut session = create_mock_session(&project_root, &build_dir).unwrap();
let _client_mut = session.client_mut();
session.close().await.unwrap();
}
#[tokio::test]
async fn test_polymorphic_session_usage() {
let (_temp_dir, project_root, build_dir) =
crate::test_utils::project::create_mock_build_folder();
let session = create_mock_session(&project_root, &build_dir).unwrap();
async fn use_session_polymorphically<S>(session: S) -> Result<(), S::Error>
where
S: ClangdSessionTrait,
{
let _client = session.client();
session.close().await
}
use_session_polymorphically(session).await.unwrap();
}
}