use async_trait::async_trait;
use std::path::PathBuf;
use std::time::Instant;
use tracing::{debug, info, warn};
use crate::clangd::config::ClangdConfig;
use crate::clangd::error::ClangdSessionError;
use crate::clangd::index::IndexProgressMonitor;
use crate::clangd::log_monitor::LogMonitor;
use crate::clangd::session_builder::ClangdSessionBuilder;
use crate::io::{ChildProcessManager, ProcessManager, StderrMonitor, StdioTransport, StopMode};
use crate::lsp::{LspClient, traits::LspClientTrait};
#[cfg(test)]
type TestSession =
ClangdSession<crate::io::process::MockProcessManager, crate::lsp::testing::MockLspClientTrait>;
#[async_trait]
pub trait ClangdSessionTrait: Send + Sync {
type Error: std::error::Error + Send + Sync + 'static;
type Client: Send + Sync;
async fn close(self) -> Result<(), Self::Error>;
fn client(&self) -> &Self::Client;
fn client_mut(&mut self) -> &mut Self::Client;
fn config(&self) -> &ClangdConfig;
fn uptime(&self) -> std::time::Duration;
fn working_directory(&self) -> &PathBuf;
fn build_directory(&self) -> &PathBuf;
}
pub struct ClangdSession<P = ChildProcessManager, C = LspClient<StdioTransport>>
where
P: ProcessManager + 'static,
C: LspClientTrait + 'static,
{
config: ClangdConfig,
process_manager: Box<P>,
lsp_client: Box<C>,
index_progress_monitor: IndexProgressMonitor,
log_monitor: LogMonitor,
started_at: Instant,
}
impl<P, C> ClangdSession<P, C>
where
P: ProcessManager + 'static,
C: LspClientTrait + 'static,
{
pub fn with_dependencies(
config: ClangdConfig,
process_manager: P,
lsp_client: C,
index_progress_monitor: IndexProgressMonitor,
log_monitor: LogMonitor,
) -> Self {
let started_at = Instant::now();
Self {
config,
process_manager: Box::new(process_manager),
lsp_client: Box::new(lsp_client),
index_progress_monitor,
log_monitor,
started_at,
}
}
}
impl ClangdSession {
pub async fn new(config: ClangdConfig) -> Result<Self, ClangdSessionError> {
ClangdSessionBuilder::new()
.with_config(config)
.build()
.await
}
}
impl<P, C> ClangdSession<P, C>
where
P: ProcessManager + 'static,
C: LspClientTrait + 'static,
{
pub async fn close(mut self) -> Result<(), ClangdSessionError> {
info!("Gracefully shutting down clangd session");
debug!("Shutting down LSP client");
let shutdown_result = tokio::time::timeout(
self.config.lsp_config.request_timeout,
self.lsp_client.shutdown(),
)
.await;
match shutdown_result {
Ok(Ok(())) => debug!("LSP client shutdown completed"),
Ok(Err(e)) => warn!("LSP client shutdown error: {}", e),
Err(_) => warn!("LSP client shutdown timed out"),
}
let _ = self.lsp_client.close().await;
debug!("Stopping clangd process");
self.process_manager
.stop(StopMode::Graceful)
.await
.map_err(|e| {
ClangdSessionError::unexpected_failure(format!("Process stop failed: {}", e))
})?;
info!("Clangd session shutdown completed");
Ok(())
}
pub fn uptime(&self) -> std::time::Duration {
self.started_at.elapsed()
}
pub fn index_progress_monitor(&self) -> &IndexProgressMonitor {
&self.index_progress_monitor
}
pub fn log_monitor(&self) -> &LogMonitor {
&self.log_monitor
}
pub fn setup_stderr_monitoring(&mut self)
where
P: StderrMonitor,
{
let processor = self.log_monitor.create_stderr_processor();
self.process_manager.on_stderr_line(move |line: String| {
processor(line);
});
debug!("LogMonitor stderr processing wired to process manager");
}
}
impl<P, C> Drop for ClangdSession<P, C>
where
P: ProcessManager + 'static,
C: LspClientTrait + 'static,
{
fn drop(&mut self) {
if self.process_manager.is_running() {
warn!("ClangdSession dropped without calling close() - force killing process");
self.process_manager.kill_sync();
}
}
}
#[async_trait]
impl<P, C> ClangdSessionTrait for ClangdSession<P, C>
where
P: ProcessManager + 'static,
C: LspClientTrait + 'static,
{
type Error = ClangdSessionError;
type Client = C;
async fn close(self) -> Result<(), Self::Error> {
ClangdSession::close(self).await
}
fn client(&self) -> &Self::Client {
&self.lsp_client
}
fn client_mut(&mut self) -> &mut Self::Client {
&mut self.lsp_client
}
fn config(&self) -> &ClangdConfig {
&self.config
}
fn uptime(&self) -> std::time::Duration {
self.uptime()
}
fn working_directory(&self) -> &PathBuf {
&self.config.working_directory
}
fn build_directory(&self) -> &PathBuf {
&self.config.build_directory
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "test-logging")]
#[ctor::ctor]
fn init_test_logging() {
crate::test_utils::logging::init();
}
#[tokio::test]
async fn test_session_construction_failure() {
use crate::clangd::testing::test_helpers::*;
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::Failing).unwrap();
let result = ClangdSession::new(config).await;
assert!(result.is_err());
}
#[cfg(feature = "clangd-integration-tests")]
#[tokio::test]
async fn test_session_ready_when_constructed() {
use crate::clangd::testing::test_helpers::*;
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::Integration).unwrap();
let session = ClangdSession::new(config).await.unwrap();
assert_eq!(session.working_directory(), &project_root);
assert_eq!(session.build_directory(), &build_dir);
assert!(session.uptime().as_nanos() > 0);
session.close().await.unwrap();
}
#[cfg(feature = "clangd-integration-tests")]
#[tokio::test]
async fn test_session_close() {
use crate::clangd::testing::test_helpers::*;
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::Integration).unwrap();
let session = ClangdSession::new(config).await.unwrap();
let result = session.close().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_trait_polymorphism_with_mocks() {
use crate::clangd::testing::test_helpers::*;
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());
session.close().await.unwrap();
}
#[tokio::test]
async fn test_polymorphic_session_usage() {
use crate::clangd::testing::test_helpers::*;
async fn use_session_polymorphically<S>(session: S) -> Result<String, S::Error>
where
S: ClangdSessionTrait,
{
let uptime = session.uptime();
session.close().await?;
Ok(format!("Session ran for {uptime:?}"))
}
let (_temp_dir, project_root, build_dir) =
crate::test_utils::project::create_mock_build_folder();
let mock_session = create_mock_session(&project_root, &build_dir).unwrap();
let result = use_session_polymorphically(mock_session).await;
assert!(result.is_ok());
assert!(result.unwrap().contains("Session ran for"));
}
#[tokio::test]
async fn test_dependency_injection_with_mocks() {
use crate::clangd::testing::test_helpers::*;
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 session = create_session_with_mock_dependencies(config);
assert_eq!(session.working_directory(), &project_root);
assert_eq!(session.build_directory(), &build_dir);
assert!(session.uptime().as_nanos() > 0);
let client = session.client();
assert!(client.is_initialized());
session.close().await.unwrap();
}
#[tokio::test]
async fn test_session_factory_for_testing() {
use crate::clangd::testing::test_helpers::*;
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();
use crate::io::process::MockProcessManager;
use crate::lsp::testing::MockLspClientTrait;
use crate::lsp::traits::LspClientTrait;
let process_manager = MockProcessManager::new();
let mut lsp_client = MockLspClientTrait::new();
lsp_client.expect_is_initialized().returning(|| true);
lsp_client
.expect_shutdown()
.returning(|| Box::pin(async { Ok(()) }));
lsp_client
.expect_close()
.returning(|| Box::pin(async { Ok(()) }));
lsp_client
.expect_open_text_document()
.returning(|_, _, _, _| Box::pin(async { Ok(()) }));
let session = ClangdSessionBuilder::new()
.with_config(config)
.with_process_manager(process_manager)
.with_lsp_client(lsp_client)
.build()
.await
.unwrap();
assert_eq!(session.working_directory(), &project_root);
assert_eq!(session.build_directory(), &build_dir);
assert!(session.uptime().as_nanos() > 0);
let client = session.client();
assert!(client.is_initialized());
session.close().await.unwrap();
}
#[tokio::test]
async fn test_unit_testing_without_external_processes() {
use crate::clangd::testing::test_helpers::*;
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();
use crate::io::process::MockProcessManager;
use crate::lsp::testing::MockLspClientTrait;
use crate::lsp::traits::LspClientTrait;
let process_manager = MockProcessManager::new();
let mut lsp_client = MockLspClientTrait::new();
lsp_client.expect_is_initialized().returning(|| true);
lsp_client
.expect_shutdown()
.returning(|| Box::pin(async { Ok(()) }));
lsp_client
.expect_close()
.returning(|| Box::pin(async { Ok(()) }));
lsp_client
.expect_open_text_document()
.returning(|_, _, _, _| Box::pin(async { Ok(()) }));
let session = ClangdSessionBuilder::new()
.with_config(config)
.with_process_manager(process_manager)
.with_lsp_client(lsp_client)
.build()
.await
.unwrap();
assert!(session.client().is_initialized());
assert!(!session.process_manager.is_running());
let fake_file_path = temp_dir.path().join("fake.cpp");
std::fs::write(&fake_file_path, "// test content").unwrap();
session.close().await.unwrap();
}
#[cfg(all(test, feature = "clangd-integration-tests"))]
#[tokio::test]
async fn test_clangd_session_with_real_project() {
use crate::clangd::testing::test_helpers::*;
let (test_project, session) = create_integration_test_session().await.unwrap();
assert!(session.uptime().as_nanos() > 0);
assert_eq!(session.working_directory(), &test_project.project_root);
assert_eq!(session.build_directory(), &test_project.build_dir);
let client = session.client();
assert!(client.is_initialized());
session.close().await.unwrap();
}
}