use {
crate::{
connect::ipc::Connection,
database,
fs::FS,
protocol::otel::exporter::setup_telemetry,
},
opentelemetry::trace::FutureExt,
};
otel::tracer!(lsp_test);
#[cfg(feature = "test")]
pub fn traced_test<P, T>(
test_name: &str,
files: &[(&str, &str)],
server: T,
test_block: impl AsyncFn(
&crate::connect::lsp::LspClient,
&mut ferrotype::Ferrotype,
) -> std::result::Result<
(),
std::boxed::Box<dyn std::error::Error + 'static>,
>,
error_expectation: (bool, Option<&str>),
snapshot: &mut ferrotype::Ferrotype,
) -> std::result::Result<(), std::boxed::Box<dyn std::error::Error + 'static>>
where
P: database::storage::Partitions,
T: crate::protocol::lsp::LanguageServer<P>,
{
let telemetry = setup_telemetry()?;
{
smol::block_on(traced_test_code(
test_name,
files,
None,
server,
snapshot,
test_block,
error_expectation,
false,
))?;
}
if let Err(err) = telemetry.tracer_provider.force_flush() {
otel::error!(
"telemetry_error",
format!("Failed to flush telemetry: {:?}", err)
);
}
drop(telemetry);
Ok(())
}
#[cfg(feature = "test")]
pub fn traced_test_with_fs<P, T>(
test_name: &str,
fs: FS,
server: T,
test_block: impl AsyncFn(
&crate::connect::lsp::LspClient,
&mut ferrotype::Ferrotype,
) -> std::result::Result<
(),
std::boxed::Box<dyn std::error::Error + 'static>,
>,
error_expectation: (bool, Option<&str>),
snapshot: &mut ferrotype::Ferrotype,
skip_fs_snapshot: bool,
) -> std::result::Result<(), std::boxed::Box<dyn std::error::Error + 'static>>
where
P: database::storage::Partitions,
T: crate::protocol::lsp::LanguageServer<P>,
{
let telemetry = setup_telemetry()?;
{
smol::block_on(traced_test_code(
test_name,
&[],
Some(fs),
server,
snapshot,
test_block,
error_expectation,
skip_fs_snapshot,
))?;
}
if let Err(err) = telemetry.tracer_provider.force_flush() {
otel::error!(
"telemetry_error",
format!("Failed to flush telemetry: {:?}", err)
);
}
drop(telemetry);
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn traced_test_code<P, T>(
test_name: &str,
files: &[(&str, &str)],
pre_built_fs: Option<FS>,
server: T,
snapshot: &mut ferrotype::Ferrotype,
test_block: impl AsyncFn(
&crate::connect::lsp::LspClient,
&mut ferrotype::Ferrotype,
) -> std::result::Result<
(),
std::boxed::Box<dyn std::error::Error + 'static>,
>,
error_expectation: (bool, Option<&str>),
skip_fs_snapshot: bool,
) -> std::result::Result<(), std::boxed::Box<dyn std::error::Error + 'static>>
where
P: database::storage::Partitions,
T: crate::protocol::lsp::LanguageServer<P>,
{
use smol::future::FutureExt as _;
let (expects_error, expected_message): (bool, Option<&str>) =
error_expectation;
let panic_err = None;
otel::span!(
@LSP_TEST_TRACER,
format!("lsp_test.{test_name}"),
"test.name" = test_name.to_string(),
"test.file.count" = files.len() as i64,
in |_cx| {
let workspace_path =
crate::Uri::parse(&format!("file://test/{}/", test_name))
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + 'static>)?;
let fs = if let Some(fs) = pre_built_fs {
fs
} else {
let fs = crate::fs::MemoryFileSystem::new();
let setup_result: std::result::Result<(), std::boxed::Box<dyn std::error::Error + 'static>> = otel::span!(
@LSP_TEST_TRACER,
"lsp_test.setup_files",
"file.count" = files.len() as i64,
in |_cx| {
for (file, content) in files {
let file_path = workspace_path.join(file)
.ok_or_else(|| format!("invalid file path: {}", file))?;
fs.write_str(&file_path, content)
.map_err(|e| format!("failed to write file {}: {}", file, e))?;
otel::event!("Wrote test file", "file" = file.to_string(), "content_len" = content.len() as i64);
}
otel::event!("Test files written", "file_count" = files.len() as i64);
Ok(())
}
);
setup_result?;
fs
};
if !skip_fs_snapshot {
fs.add_tree_to_snapshot("Before", snapshot);
}
let fs_for_snapshot = fs.clone();
let (server_conn, client_conn) = Connection::memory();
let server = crate::Laburnum::<P, T>::new(server)
.filesystem(fs)
.build_server(server_conn);
otel::span!(
@LSP_TEST_TRACER,
"lsp_test.client_lifecycle",
in |cx|{
let workspace_folders = Some(vec![crate::protocol::lsp::WorkspaceFolder {
uri: workspace_path.clone(),
name: "Workspace".to_string(),
}]);
let client = { crate::connect::lsp::LspClient::new_test(client_conn) };
let init_result: std::result::Result<(), std::boxed::Box<dyn std::error::Error + 'static>> = otel::span!(
@LSP_TEST_TRACER,
"lsp_test.client.initialize",
in |cx| {
client
.start(crate::protocol::lsp::InitializeParams {
process_id: None,
initialization_options: None,
capabilities: Default::default(),
trace: None,
workspace_folders: workspace_folders.clone(),
client_info: Some(crate::protocol::lsp::ClientInfo {
name: "Laburnum Test Macro".to_string(),
version: Some("test".to_string()),
}),
locale: None,
work_done_progress_params:
crate::protocol::lsp::WorkDoneProgressParams {
work_done_token: None,
},
..Default::default()
})
.with_context(cx)
.await
.map_err(|e| format!("Failed to start: {}", e))?;
if !client.is_initialized() {
return Err("Client should be initialized".into());
}
otel::event!("Client initialized successfully");
Ok(())
});
init_result?;
otel::span!(
@LSP_TEST_TRACER,
"lsp_test.test_block",
in |cx|{
let test_result = std::panic::AssertUnwindSafe(test_block(&client, snapshot))
.catch_unwind().with_context(cx)
.await;
if let Err(panic_payload) = test_result {
let panic_message = if let Some(s) = panic_payload.downcast_ref::<&str>()
{
s.to_string()
} else if let Some(s) = panic_payload.downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
};
eprintln!("panic {}",panic_message);
otel::exception!(
"panic",panic_message
);
std::panic::resume_unwind(panic_payload);
} else {
otel::event!("Test block completed successfully");
}
});
if expects_error {
async {
otel::event!("Validating error expectations");
let diagnostics = client.get_received_diagnostics();
let total_diagnostics: usize =
diagnostics.iter().map(|d| d.diagnostics.len()).sum();
otel::event!(
"Received diagnostics",
"diagnostic_count" = total_diagnostics as i64,
);
if let Some(msg) = expected_message {
otel::event!("Checking for specific error message", "expected_message" = msg.to_string());
let has_matching_diagnostic = diagnostics.iter().any(|d| {
d.diagnostics
.iter()
.any(|diag| diag.message.to_string().contains(msg))
});
let all_messages: Vec<String> = diagnostics
.iter()
.flat_map(|d| {
d.diagnostics.iter().map(|diag| diag.message.to_string())
})
.collect();
if has_matching_diagnostic {
otel::event!(
"Found expected error message",
"expected_message" = msg.to_string(),
);
} else {
otel::error!(
"validation_error",
format!("Expected error message '{}' not found. Got: {:?}", msg, all_messages)
);
}
assert!(
has_matching_diagnostic,
"Expected diagnostic containing '{}', but got: {:?}",
msg, all_messages
);
} else {
let has_any_diagnostic =
diagnostics.iter().any(|d| !d.diagnostics.is_empty());
if has_any_diagnostic {
otel::event!("Found expected diagnostics");
} else {
otel::error!("validation_error", "Expected diagnostics but found none");
}
assert!(
has_any_diagnostic,
"Expected at least one diagnostic, but got none"
);
}
}
.with_context(cx)
.await;
}
{
otel::span!(
@LSP_TEST_TRACER,
"lsp_test.client.shutdown",
in |cx| {
otel::event!("Shutting down client");
client.stop_test(snapshot).with_context(cx).await.ok();
otel::event!("Client shutdown complete");
}
);
}
});
{
otel::span!(
@LSP_TEST_TRACER,
"lsp_test.server.close"
);
otel::event!("Closing server");
if let Err(err) = server.close() {
otel::error!("server_error", format!("Failed to close server: {:?}", err));
}
otel::event!("Server closed");
}
if !skip_fs_snapshot {
fs_for_snapshot.add_tree_to_snapshot("After", snapshot);
}
otel::event!(
"Test completed successfully",
"test_name" = test_name.to_string(),
);
match panic_err {
Some(err) if !expects_error => Err(err),
_ => {
Ok(())
}
}
})
}
#[macro_export]
macro_rules! lsp_test {
(
<$storage_ty:ty, $language_server_ty:tt>{
$($rest:tt)*
}
) => {
$crate::lsp_test!(@munch <$storage_ty, $language_server_ty> $($rest)*);
};
(@munch <$storage_ty:ty, $language_server_ty:tt>
#[ignore = $reason:literal]
$test_name:ident({
$($file:literal => $content:expr),* $(,)?
}) => |$client:ident $(, $snapshot:ident)?| $body:block
$(, $($rest:tt)*)?
) => {
$crate::lsp_test!(@impl_ignore <$storage_ty, $language_server_ty>
$test_name({$($file => $content),*}) => |$client $(, $snapshot)?| $body
; ignore_reason = $reason
);
$($crate::lsp_test!(@munch <$storage_ty, $language_server_ty> $($rest)*);)?
};
(@munch <$storage_ty:ty, $language_server_ty:tt>
#[error($expected_error:literal)]
$test_name:ident({
$($file:literal => $content:expr),* $(,)?
}) => |$client:ident $(, $snapshot:ident)?| $body:block
$(, $($rest:tt)*)?
) => {
$crate::lsp_test!(@impl <$storage_ty, $language_server_ty>
$test_name({$($file => $content),*}) => |$client $(, $snapshot)?| $body
; error_expectation = (true, Some($expected_error))
);
$($crate::lsp_test!(@munch <$storage_ty, $language_server_ty> $($rest)*);)?
};
(@munch <$storage_ty:ty, $language_server_ty:tt>
#[error]
$test_name:ident({
$($file:literal => $content:expr),* $(,)?
}) => |$client:ident $(, $snapshot:ident)?| $body:block
$(, $($rest:tt)*)?
) => {
$crate::lsp_test!(@impl <$storage_ty, $language_server_ty>
$test_name({$($file => $content),*}) => |$client $(, $snapshot)?| $body
; error_expectation = (true, None::<&str>)
);
$($crate::lsp_test!(@munch <$storage_ty, $language_server_ty> $($rest)*);)?
};
(@munch <$storage_ty:ty, $language_server_ty:tt>
$test_name:ident({
$($file:literal => $content:expr),* $(,)?
}) => |$client:ident $(, $snapshot:ident)?| $body:block
$(, $($rest:tt)*)?
) => {
$crate::lsp_test!(@impl <$storage_ty, $language_server_ty>
$test_name({$($file => $content),*}) => |$client $(, $snapshot)?| $body
; error_expectation = (false, None::<&str>)
);
$($crate::lsp_test!(@munch <$storage_ty, $language_server_ty> $($rest)*);)?
};
(@munch <$storage_ty:ty, $language_server_ty:tt>
#[ignore = $reason:literal]
$test_name:ident(folder($folder_path:literal)) => |$client:ident $(, $snapshot:ident)?| $body:block
$(, $($rest:tt)*)?
) => {
$crate::lsp_test!(@impl_folder_ignore <$storage_ty, $language_server_ty>
$test_name(folder($folder_path)) => |$client $(, $snapshot)?| $body
; ignore_reason = $reason
);
$($crate::lsp_test!(@munch <$storage_ty, $language_server_ty> $($rest)*);)?
};
(@munch <$storage_ty:ty, $language_server_ty:tt>
#[error($expected_error:literal)]
$test_name:ident(folder($folder_path:literal)) => |$client:ident $(, $snapshot:ident)?| $body:block
$(, $($rest:tt)*)?
) => {
$crate::lsp_test!(@impl_folder <$storage_ty, $language_server_ty>
$test_name(folder($folder_path)) => |$client $(, $snapshot)?| $body
; error_expectation = (true, Some($expected_error))
);
$($crate::lsp_test!(@munch <$storage_ty, $language_server_ty> $($rest)*);)?
};
(@munch <$storage_ty:ty, $language_server_ty:tt>
#[error]
$test_name:ident(folder($folder_path:literal)) => |$client:ident $(, $snapshot:ident)?| $body:block
$(, $($rest:tt)*)?
) => {
$crate::lsp_test!(@impl_folder <$storage_ty, $language_server_ty>
$test_name(folder($folder_path)) => |$client $(, $snapshot)?| $body
; error_expectation = (true, None::<&str>)
);
$($crate::lsp_test!(@munch <$storage_ty, $language_server_ty> $($rest)*);)?
};
(@munch <$storage_ty:ty, $language_server_ty:tt>
$test_name:ident(folder($folder_path:literal)) => |$client:ident $(, $snapshot:ident)?| $body:block
$(, $($rest:tt)*)?
) => {
$crate::lsp_test!(@impl_folder <$storage_ty, $language_server_ty>
$test_name(folder($folder_path)) => |$client $(, $snapshot)?| $body
; error_expectation = (false, None::<&str>)
);
$($crate::lsp_test!(@munch <$storage_ty, $language_server_ty> $($rest)*);)?
};
(@munch <$storage_ty:ty, $language_server_ty:tt>) => {};
(@impl_ignore <$storage_ty:ty, $language_server_ty:tt>
$test_name:ident({
$($file:literal => $content:expr),* $(,)?
}) => |$client:ident $(, $snapshot_binding:ident)?| $body:block
; ignore_reason = $reason:literal
) => {
paste::paste!{
#[test]
#[ignore = $reason]
fn [<$test_name>]() -> std::result::Result<(), std::boxed::Box<(dyn std::error::Error + 'static)>> {
let mut snapshot = ferrotype::Ferrotype::new();
$crate::test::traced_test(
stringify!($test_name),
&[$(($file, $content)),*],
$language_server_ty {},
async |client, snapshot| {
let $client = &client;
$(let $snapshot_binding = snapshot;)?
$body
Ok(())
},
(false, None::<&str>),
&mut snapshot
)?;
ferrotype::assert!(snapshot);
Ok(())
}
}
};
(@impl <$storage_ty:ty, $language_server_ty:tt>
$test_name:ident({
$($file:literal => $content:expr),* $(,)?
}) => |$client:ident $(, $snapshot_binding:ident)?| $body:block
; error_expectation = $error_expectation:expr
) => {
paste::paste!{
#[test]
fn [<$test_name>]() -> std::result::Result<(), std::boxed::Box<(dyn std::error::Error + 'static)>> {
let mut snapshot = ferrotype::Ferrotype::new();
$crate::test::traced_test(
stringify!($test_name),
&[$(($file, $content)),*],
$language_server_ty {},
async |client, snapshot| {
let $client = &client;
$(let $snapshot_binding = snapshot;)?
$body
Ok(())
},
$error_expectation,
&mut snapshot
)?;
ferrotype::assert!(snapshot);
Ok(())
}
}
};
(@impl_folder_ignore <$storage_ty:ty, $language_server_ty:tt>
$test_name:ident(folder($folder_path:literal)) => |$client:ident $(, $snapshot_binding:ident)?| $body:block
; ignore_reason = $reason:literal
) => {
paste::paste!{
#[test]
#[ignore = $reason]
fn [<$test_name>]() -> std::result::Result<(), std::boxed::Box<(dyn std::error::Error + 'static)>> {
let mut snapshot = ferrotype::Ferrotype::new();
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let folder_path = manifest_dir.join($folder_path);
let workspace_uri = $crate::Uri::parse(&format!("file://test/{}/", stringify!($test_name)))
.expect("valid workspace uri");
let fs = $crate::fs::MemoryFileSystem::from_folder(&folder_path, workspace_uri)
.expect(&format!("failed to load folder: {}", folder_path.display()));
$crate::test::traced_test_with_fs::<$storage_ty, _>(
stringify!($test_name),
fs,
$language_server_ty {},
async |client, snapshot| {
let $client = &client;
$(let $snapshot_binding = snapshot;)?
$body
Ok(())
},
(false, None::<&str>),
&mut snapshot,
true,
)?;
ferrotype::assert!(snapshot);
Ok(())
}
}
};
(@impl_folder <$storage_ty:ty, $language_server_ty:tt>
$test_name:ident(folder($folder_path:literal)) => |$client:ident $(, $snapshot_binding:ident)?| $body:block
; error_expectation = $error_expectation:expr
) => {
paste::paste!{
#[test]
fn [<$test_name>]() -> std::result::Result<(), std::boxed::Box<(dyn std::error::Error + 'static)>> {
let mut snapshot = ferrotype::Ferrotype::new();
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let folder_path = manifest_dir.join($folder_path);
let workspace_uri = $crate::Uri::parse(&format!("file://test/{}/", stringify!($test_name)))
.expect("valid workspace uri");
let fs = $crate::fs::MemoryFileSystem::from_folder(&folder_path, workspace_uri)
.expect(&format!("failed to load folder: {}", folder_path.display()));
$crate::test::traced_test_with_fs::<$storage_ty, _>(
stringify!($test_name),
fs,
$language_server_ty {},
async |client, snapshot| {
let $client = &client;
$(let $snapshot_binding = snapshot;)?
$body
Ok(())
},
$error_expectation,
&mut snapshot,
true,
)?;
ferrotype::assert!(snapshot);
Ok(())
}
}
};
}