Skip to content
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

Use a global test client for all unit tests #130

Merged
merged 7 commits into from
Jan 8, 2025

Conversation

DavisVaughan
Copy link
Collaborator

@DavisVaughan DavisVaughan commented Jan 7, 2025

A feature plucked from #126, and a step towards allowing dynamic setting of logLevel and dependencyLogLevels in the logging code, which are going to require some global state.

Here are notes from that other PR about this:

We now have a global test client that is used across unit tests. It is stored behind a Mutex, which forces unit tests that require the client to run sequentially, while still allowing all other tests to run in parallel without issue. You use with_client(|client| { }) in a unit test to access it. You just have to be thoughtful to not do something in your unit test that puts the client in a state that affects other unit tests. If you need to destructively modify the client for a test, you create an integration test instead (see tests/initialization.rs which tests start up and shut down). Additionally, because we only have 1 client per process and the client tests are forced to run sequentially, using -- --nocapture actually makes sense now even without forcing 1 thread!

The async code makes this quite tricky. [#tokio::test] creates 1 runtime per test, which completely screws you up if you have a global object that needs to be safely synchronized across tests, because you can't really synchronize across runtimes. The temporary solution used here is to use a global test runtime as well, and just call block_on() for each test. See tokio-rs/tokio#2374 for more.

The better solution in the future will be to remove the async code in favor of lsp-server, which will simplify everything down to this: https://github.com/posit-dev/air/blob/feature/ruff/crates/server/src/test/client.rs


I've also added in the ability for our test client to send the initialized() notification. This will end up being required when we switch to lsp-server, as lsp-server waits for initialized() before letting us proceed on the server side.

Comment on lines 49 to 82
client
.register_capability(regs)
.instrument(span.exit())
.await?;
if !regs.is_empty() {
client
.register_capability(regs)
.instrument(span.exit())
.await?;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we send initialized(), this handle_initialized() is fired and the server was sending this request to the client that the client test code was not expecting. Since the test client doesn't actually have the capability required here, I guarded it like this to avoid sending the request entirely.

(Note this was quite hard to debug, as it seems like the codec code can't figure out how to deserialize this message from the server at all)

Comment on lines -207 to +208
if is_test_client(client_info) {
// During parallel testing, `set_global_default()` gets called multiple times
// per process. That causes it to error, but we ignore this.
tracing::subscriber::set_global_default(subscriber).ok();
} else {
tracing::subscriber::set_global_default(subscriber)
.expect("Should be able to set the global subscriber exactly once.");
}
tracing::subscriber::set_global_default(subscriber)
.expect("Should be able to set the global subscriber exactly once.");
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay

Comment on lines -128 to +136
self.recv_response().await;

let response = self.recv_response().await;
assert_eq!(&id, response.id());
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched id() to return jsonrpc::Id because I found it was quite useful to add in some extra asserts to ensure that a call to recv_response() actually returns the response you were hoping for

@DavisVaughan DavisVaughan requested a review from lionel- January 7, 2025 18:32
Copy link
Collaborator

@lionel- lionel- left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LG!

@@ -54,5 +54,4 @@ jobs:
env:
AIR_LOG_LEVEL: trace
# `--nocapture` to see our own `tracing` logs
# `--test-threads 1` to ensure `tracing` logs aren't interleaved
run: cargo test -- --nocapture --test-threads 1
run: cargo test -- --nocapture
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's nice to make CI and local testing more consistent.

let mut client = init_test_client().await;
#[test]
fn test_format_minimal_diff() {
with_client(|client| async {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit unfortunate to increase nesting but the explicitness is nice.

/// a `&mut TestClient` to the unit test.
static TEST_CLIENT: OnceCell<Mutex<TestClient>> = OnceCell::const_new();

/// `#[tokio::test]` creates a new runtime for each individual test. That doesn't work
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be nice to remove all these tokio complications.

And rename a few files to reflect new location
Otherwise we end up sending our test client a request here that it isn't expecting at this time, since it didn't say it supported these capabilities
This is definitely required for lsp-server, otherwise it can't proceed, but is also the right thing to do as a client in general
With new support for sending `initialized()` too
@DavisVaughan DavisVaughan force-pushed the feature/test-global-client branch from 30d6178 to 64feef0 Compare January 8, 2025 18:37
@DavisVaughan DavisVaughan merged commit 660975b into main Jan 8, 2025
4 checks passed
@DavisVaughan DavisVaughan deleted the feature/test-global-client branch January 8, 2025 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants