Skip to content

Conversation

@dhruvmanila
Copy link
Member

@dhruvmanila dhruvmanila commented Jul 17, 2025

Summary

Closes: astral-sh/ty#88

This PR implements an initial version of a mock language server that can be used to write e2e tests using the real server running in the background.

The way it works is that you'd use the TestServerBuilder to help construct the TestServer with the setup data. This could be the workspace folders, populating the file and it's content in the memory file system, setting the right client capabilities to make the server respond correctly, etc. This can be expanded as we write more test cases.

There are still a few things to follow-up on:

  • In the Drop implementation, we should assert that there are no pending notification, request and responses from the server that the test code hasn't handled yet Implemented in afd1f82 (#19391)
  • Reduce the setup boilerplate in any way we can
  • Improve the final assertion, currently I'm just snapshotting the final output

Test Plan

Written a few test cases.

@dhruvmanila dhruvmanila added server Related to the LSP server testing Related to testing Ruff itself ty Multi-file analysis & type inference labels Jul 17, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Jul 17, 2025

mypy_primer results

No ecosystem changes detected ✅
No memory usage changes detected ✅

@github-actions
Copy link
Contributor

github-actions bot commented Jul 17, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@dhruvmanila dhruvmanila force-pushed the dhruv/mock-server branch 2 times, most recently from c229255 to 332c1cc Compare July 17, 2025 14:40
@dhruvmanila dhruvmanila marked this pull request as ready for review July 17, 2025 14:50
@dhruvmanila dhruvmanila force-pushed the dhruv/mock-server branch 2 times, most recently from afd1f82 to 964b413 Compare July 18, 2025 05:54
Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

Nice, this is awesome!

I've a few suggestions on how the error handling and asserting some basic constraints of the LSP protocol can be improved. But I don't mind if you decide to defer those to a follow up PR.


impl Drop for TestServer {
fn drop(&mut self) {
self.drain_messages();
Copy link
Member

Choose a reason for hiding this comment

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

Should we move the drain to after the shutdown handling because the server could still be sending requests to the client during the shutdown sequence if it wnated to.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we should do this first because if we reached here then the test case didn't handle some messages.

Copy link
Member

Choose a reason for hiding this comment

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

We might then need to do it twice?

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I think I might have misunderstood what it does. It simply drops all messages. Shouldn't it be an error if a test doesn't handle all messages? We could add a method that drops all messages but I would sort of expect that all messages are handled and any unhandled message is an error (because it didn't fully test the flow?)

Copy link
Member Author

@dhruvmanila dhruvmanila Jul 18, 2025

Choose a reason for hiding this comment

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

It doesn't drop all the message, it basically keeps asking the server for the messages via the self.receive call until we stop receiving any messages from the server which records them in self.requests, self.notifications and self.responses and at the end there's a self.assert_no_pending_messages call that asserts that these data structures are empty.

Copy link
Member Author

Choose a reason for hiding this comment

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

This split is necessary because we should only panic after the server thread is joined to avoid unnecessary panic messages.

R: Request,
{
let id = self.next_request_id();
let request = lsp_server::Request::new(id.clone(), R::METHOD.to_string(), params);
Copy link
Member

Choose a reason for hiding this comment

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

This is more of an extension but it would be nice if the server tracked the pending_requests (FxHashMap<id, &'static str> where the value is the method).

  • get_response should panic if the server receives a response for a request not in pending_requests (that means the response was sent twice, e.g. after cancellation)
  • Drop should probably panic if there are any pending requests? I'm not entirely sure about that as it isn't a LSP spec violation. We might at least want to warn using tracing

@dhruvmanila dhruvmanila force-pushed the dhruv/mock-server branch 2 times, most recently from 79a42e2 to 98f4b46 Compare July 21, 2025 09:06
@AlexWaygood AlexWaygood removed their request for review July 21, 2025 13:42
@MichaReiser
Copy link
Member

I'm taking a look at the failing windows test

@MichaReiser
Copy link
Member

This works for me:

Subject: [PATCH] Add Micro Benchmark

This PR adds a microbenchmark for the Ruff linter. Adding microbenchmark for other tools
should be straightforward.
---
Index: crates/ty_server/src/test.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/crates/ty_server/src/test.rs b/crates/ty_server/src/test.rs
--- a/crates/ty_server/src/test.rs	(revision 07e2747a21e17da81ff3c9433e27721bb8a9d61e)
+++ b/crates/ty_server/src/test.rs	(date 1753194432043)
@@ -13,7 +13,7 @@
 use std::time::Duration;
 use std::{fmt, fs};
 
-use anyhow::{Context, Result};
+use anyhow::{Context, Result, anyhow};
 use crossbeam::channel::RecvTimeoutError;
 use insta::internals::SettingsBindDropGuard;
 use lsp_server::{Connection, Message, RequestId, Response, ResponseError};
@@ -836,15 +836,16 @@
         })?;
 
         let mut settings = insta::Settings::clone_current();
-        settings.add_filter(&tempdir_filter(&project_dir), "<temp_dir>/");
+        settings.add_filter(&tempdir_filter(project_dir.as_str()), "<temp_dir>/");
         // TODO: Is this required?
-        // settings.add_filter(
-        //     &tempdir_filter(
-        //         Url::from_file_path(project_dir.as_std_path())
-        //             .context("Failed to convert project path to URL")?,
-        //     ),
-        //     "file://<temp_dir>/",
-        // );
+        settings.add_filter(
+            &tempdir_filter(
+                Url::from_file_path(project_dir.as_std_path())
+                    .map_err(|()| anyhow!("Failed to convert root directory to url"))?
+                    .path(),
+            ),
+            "/<temp_dir>/",
+        );
         settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
         settings.add_filter(
             r#"The system cannot find the file specified."#,
@@ -865,6 +866,6 @@
     }
 }
 
-fn tempdir_filter(path: &SystemPath) -> String {
-    format!(r"{}\\?/?", regex::escape(path.as_str()))
+fn tempdir_filter(path: impl AsRef<str>) -> String {
+    format!(r"{}\\?/?", regex::escape(path.as_ref()))
 }

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

This is great. I've a few more inline suggestions

/// Perform LSP initialization handshake
fn initialize(
mut self,
workspace_folders: Vec<WorkspaceFolder>,
Copy link
Member

Choose a reason for hiding this comment

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

Nit: The initialize function can compute workspace_folders from iterating over workspace_configurations (move line 133 into `initialize)

Comment on lines +305 to +317
#[cfg(test)]
mod tests {
use anyhow::Result;
use lsp_types::notification::PublishDiagnostics;
use ruff_db::system::SystemPath;

use crate::session::ClientOptions;
use crate::test::TestServerBuilder;

#[test]
fn initialization() -> Result<()> {
let server = TestServerBuilder::new()?
.build()?
Copy link
Member

Choose a reason for hiding this comment

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

I think I'd prefer if the tests were written as integration tests in the tests folder. See the ty/cli/main.rs for how to share code between integration tests. Unless this forces us to make many more types public that otherwise wouldn't have to be.

My main thinking here is that these are integration tests and the snapshots folder is a bit distracting in the middle of rust modules (this could also be solved by specifying another path in the insta settings)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I wanted this to be integration tests but it will require making ClientOptions and everything inside it as publicly available so that it can be used to specify workspace options in the test server.

Copy link
Member

Choose a reason for hiding this comment

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

I see. I don't think I would mind this overly much but maybe worth a separate PR so that it's clear what we need to make public

@dhruvmanila dhruvmanila merged commit 53d795d into main Jul 23, 2025
36 checks passed
@dhruvmanila dhruvmanila deleted the dhruv/mock-server branch July 23, 2025 06:56
UnboundVariable pushed a commit to UnboundVariable/ruff that referenced this pull request Jul 23, 2025
* main: (28 commits)
  [ty] highlight the argument in `static_assert` error messages (astral-sh#19426)
  [ty] Infer single-valuedness for enums based on `int`/`str` (astral-sh#19510)
  [ty] Restructure submodule query around `File` dependency
  [ty] Make `Module` a Salsa ingredient
  [ty] Reachability analysis for `isinstance(…)` branches (astral-sh#19503)
  [ty] Normalize single-member enums to their instance type (astral-sh#19502)
  [ty] Invert `ty_ide` and `ty_project` dependency (astral-sh#19501)
  [ty] Implement mock language server for testing (astral-sh#19391)
  [ty] Detect enums if metaclass is a subtype of EnumType/EnumMeta (astral-sh#19481)
  [ty] perform type narrowing for places marked `global` too (astral-sh#19381)
  [ty] Use `ThinVec` for sub segments in `PlaceExpr` (astral-sh#19470)
  [ty] Splat variadic arguments into parameter list (astral-sh#18996)
  [`flake8-pyi`] Skip fix if all `Union` members are `None` (`PYI016`)  (astral-sh#19416)
  Skip notebook with errors in ecosystem check (astral-sh#19491)
  [ty] Consistent use of American english (in rules) (astral-sh#19488)
  [ty] Support iterating over enums (astral-sh#19486)
  Fix panic for illegal `Literal[…]` annotations with inner subscript expressions (astral-sh#19489)
  Move fix suggestion to subdiagnostic (astral-sh#19464)
  [ty] Implement non-stdlib stub mapping for classes and functions (astral-sh#19471)
  [ty] Disallow illegal uses of `ClassVar` (astral-sh#19483)
  ...

# Conflicts:
#	crates/ty_ide/src/goto.rs
dcreager added a commit that referenced this pull request Jul 23, 2025
* main:
  [ty] Fix narrowing and reachability of class patterns with arguments (#19512)
  [ty] Implemented partial support for "find references" language server feature. (#19475)
  [`flake8-use-pathlib`] Add autofix for `PTH101`, `PTH104`, `PTH105`, `PTH121` (#19404)
  [`perflint`] Parenthesize generator expressions (`PERF401`) (#19325)
  [`pep8-naming`] Fix `N802` false positives for `CGIHTTPRequestHandler` and `SimpleHTTPRequestHandler` (#19432)
  [`pylint`] Handle empty comments after line continuation (`PLR2044`) (#19405)
  Move concise diagnostic rendering to `ruff_db` (#19398)
  [ty] highlight the argument in `static_assert` error messages (#19426)
  [ty] Infer single-valuedness for enums based on `int`/`str` (#19510)
  [ty] Restructure submodule query around `File` dependency
  [ty] Make `Module` a Salsa ingredient
  [ty] Reachability analysis for `isinstance(…)` branches (#19503)
  [ty] Normalize single-member enums to their instance type (#19502)
  [ty] Invert `ty_ide` and `ty_project` dependency (#19501)
  [ty] Implement mock language server for testing (#19391)
  [ty] Detect enums if metaclass is a subtype of EnumType/EnumMeta (#19481)
  [ty] perform type narrowing for places marked `global` too (#19381)
dhruvmanila added a commit that referenced this pull request Jul 24, 2025
AlexWaygood pushed a commit that referenced this pull request Jul 25, 2025
## Summary

Closes: astral-sh/ty#88

This PR implements an initial version of a mock language server that can
be used to write e2e tests using the real server running in the
background.

The way it works is that you'd use the `TestServerBuilder` to help
construct the `TestServer` with the setup data. This could be the
workspace folders, populating the file and it's content in the memory
file system, setting the right client capabilities to make the server
respond correctly, etc. This can be expanded as we write more test
cases.

There are still a few things to follow-up on:
- ~In the `Drop` implementation, we should assert that there are no
pending notification, request and responses from the server that the
test code hasn't handled yet~ Implemented in [`afd1f82`
(#19391)](afd1f82)
- Reduce the setup boilerplate in any way we can
- Improve the final assertion, currently I'm just snapshotting the final
output

## Test Plan

Written a few test cases.
AlexWaygood pushed a commit that referenced this pull request Jul 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

server Related to the LSP server testing Related to testing Ruff itself ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add test infrastructure for the language server

3 participants