-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make testing more reliable and realistic #87
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
use std::collections::HashMap; | ||
use std::future; | ||
use std::pin::Pin; | ||
use std::sync::OnceLock; | ||
|
||
use anyhow::anyhow; | ||
use biome_lsp_converters::PositionEncoding; | ||
|
@@ -31,16 +32,15 @@ use crate::tower_lsp::LspMessage; | |
use crate::tower_lsp::LspNotification; | ||
use crate::tower_lsp::LspRequest; | ||
use crate::tower_lsp::LspResponse; | ||
use crate::TESTING; | ||
|
||
pub(crate) type TokioUnboundedSender<T> = tokio::sync::mpsc::UnboundedSender<T>; | ||
pub(crate) type TokioUnboundedReceiver<T> = tokio::sync::mpsc::UnboundedReceiver<T>; | ||
|
||
// The global instance of the auxiliary event channel, used for sending log | ||
// messages or spawning threads from free functions. Since this is an unbounded | ||
// channel, sending a log message is not async nor blocking. Tokio senders are | ||
// Send and Sync so this global variable can be safely shared across threads. | ||
static mut AUXILIARY_EVENT_TX: std::cell::OnceCell<TokioUnboundedSender<AuxiliaryEvent>> = | ||
std::cell::OnceCell::new(); | ||
// channel, sending a log message is not async nor blocking. | ||
static AUXILIARY_EVENT_TX: OnceLock<TokioUnboundedSender<AuxiliaryEvent>> = OnceLock::new(); | ||
|
||
// This is the syntax for trait aliases until an official one is stabilised. | ||
// This alias is for the future of a `JoinHandle<anyhow::Result<T>>` | ||
|
@@ -378,16 +378,9 @@ impl AuxiliaryState { | |
// Set global instance of this channel. This is used for interacting | ||
// with the auxiliary loop (logging messages or spawning a task) from | ||
// free functions. | ||
unsafe { | ||
#[allow(static_mut_refs)] | ||
if let Some(val) = AUXILIARY_EVENT_TX.get_mut() { | ||
// Reset channel if already set. Happens e.g. on reconnection after a refresh. | ||
*val = auxiliary_event_tx; | ||
} else { | ||
// Set channel for the first time | ||
AUXILIARY_EVENT_TX.set(auxiliary_event_tx).unwrap(); | ||
} | ||
} | ||
AUXILIARY_EVENT_TX | ||
.set(auxiliary_event_tx) | ||
.expect("Auxiliary event channel can't be set more than once."); | ||
Comment on lines
-381
to
+383
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had the very important realization that unlike in the current ark setup, we won't ever go through here twice in air. There is no "reconnect" lifecycle to manage, our whole process is just going to get dropped and reestablished, so we don't have to worry about this anymore. This was being triggered in our tests though - even if we ran the tests sequentially we'd come through here multiple times as we "start up" a new LSP within the same process. I've fixed that by running the LSP tests as integration tests with 1 client per process, truly ensuring we never run through here again.
Comment on lines
-381
to
+383
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Notably you only see the This is because if the auxiliary or main loop threads panic, then we don't propagate the panic up to the main process at all. Tokio will return a But note that with this PR |
||
|
||
// List of pending tasks for which we manage the lifecycle (mainly relay | ||
// errors and panics) | ||
|
@@ -453,12 +446,10 @@ impl AuxiliaryState { | |
} | ||
|
||
fn auxiliary_tx() -> &'static TokioUnboundedSender<AuxiliaryEvent> { | ||
// If we get here that means the LSP was initialised at least once. The | ||
// channel might be closed if the LSP was dropped, but it should exist. | ||
unsafe { | ||
#[allow(static_mut_refs)] | ||
AUXILIARY_EVENT_TX.get().unwrap() | ||
} | ||
// If we get here that means the LSP was initialised in `AuxiliaryState::new()`. | ||
// The channel might be closed if the LSP was dropped, but it should exist | ||
// (and in that case we expect the process to exit shortly afterwards anyways). | ||
AUXILIARY_EVENT_TX.get().unwrap() | ||
} | ||
|
||
fn send_auxiliary(event: AuxiliaryEvent) { | ||
|
@@ -471,8 +462,9 @@ fn send_auxiliary(event: AuxiliaryEvent) { | |
/// Send a message to the LSP client. This is non-blocking and treated on a | ||
/// latency-sensitive task. | ||
pub(crate) fn log(level: lsp_types::MessageType, message: String) { | ||
// We're not connected to an LSP client when running unit tests | ||
if cfg!(test) { | ||
// We don't want to send logs to the client when running integration tests, | ||
// as they interfere with our ability to track sent/received requests. | ||
if *TESTING.get().unwrap_or(&false) { | ||
return; | ||
} | ||
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using the "old" style |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
// For some reason this setup gives false alarms about dead code | ||
#[allow(dead_code)] | ||
pub mod test_client; | ||
#[allow(dead_code)] | ||
pub mod tower_lsp_test_client; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
use lsp::tower_lsp::start_test_server; | ||
use lsp_test::lsp_client::TestClient; | ||
|
||
pub async fn start_test_client() -> lsp_test::lsp_client::TestClient { | ||
TestClient::new(|server_rx, client_tx| async { start_test_server(server_rx, client_tx).await }) | ||
} | ||
|
||
pub async fn init_test_client() -> TestClient { | ||
let mut client = start_test_client().await; | ||
|
||
client.initialize().await; | ||
client.recv_response().await; | ||
|
||
client | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
mod fixtures; | ||
|
||
use fixtures::test_client::init_test_client; | ||
use fixtures::tower_lsp_test_client::TestClientExt; | ||
use lsp::documents::Document; | ||
|
||
// https://github.com/posit-dev/air/issues/61 | ||
#[tests_macros::lsp_test] | ||
async fn test_format_minimal_diff() { | ||
let mut client = init_test_client().await; | ||
|
||
#[rustfmt::skip] | ||
let doc = Document::doodle( | ||
"1 | ||
2+2 | ||
3 | ||
", | ||
); | ||
|
||
let edits = client.format_document_edits(&doc).await.unwrap(); | ||
assert!(edits.len() == 1); | ||
|
||
let edit = &edits[0]; | ||
assert_eq!(edit.new_text, " + "); | ||
|
||
client | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
mod fixtures; | ||
|
||
use fixtures::test_client::init_test_client; | ||
use fixtures::tower_lsp_test_client::TestClientExt; | ||
use lsp::documents::Document; | ||
|
||
#[tests_macros::lsp_test] | ||
async fn test_format() { | ||
let mut client = init_test_client().await; | ||
|
||
#[rustfmt::skip] | ||
let doc = Document::doodle( | ||
" | ||
1 | ||
2+2 | ||
3 + 3 + | ||
3", | ||
); | ||
|
||
let formatted = client.format_document(&doc).await; | ||
insta::assert_snapshot!(formatted); | ||
|
||
client | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to rename this back but i got a little confused seeing
TestClient
accept a lambda function that calledstart_lsp()
. I thoughtstart_lsp()
did something client related when I saw that.