-
Notifications
You must be signed in to change notification settings - Fork 59
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
Best way to test servers that implement LanguageServer? #355
Comments
Just FYI, I think this project is dead. |
That's disappointing. Would that mean |
Probably not. To be honest, I don't think anyone uses it anyway. |
I suspect the non- Currently, it should be possible to drive an use futures::{SinkExt, StreamExt};
use tower_lsp::jsonrpc::{Request, Response};
use tower_lsp::LspService;
let (mut server, mut client) = LspService::new(|client| FooServer { client });
// Use `server.call(...).await` to send a `Request` to the server and receive a `Response`.
// Use `client.next().await` to receive any pending client `Request`s from the server.
// Use `client.send(...).await` to reply to those requests with mock client `Response`s. This makes use of these trait implementations included with impl<S: LanguageServer> Service<Request> for LspService<S> { ... }
impl Stream for ClientSocket { type Item = Request; ... }
impl Sink<Response> for ClientSocket { ... } It would be nice if we could ship some convenient testing tools to make testing LSP flows easier. I'm looking to make a maintenance release sometime this week which updates dependencies and makes a few quality-of-life improvements, but I suspect this may require a somewhat larger effort and might be addressed in the medium-term down the line. Let's keep this ticket open to track this. |
Related to #229, though that ticket is more about documenting how to test the |
Just noticed some relevant information in a prior PR comment I'd like to link here: #344 (comment)
|
I like the test harness you've proposed above 😍 I too need something that allows both client -> server and server -> client requests to be testable. At the moment I'm using the following approach to test requests going both ways. It feels a little low-level, and can probably be much improved on (I'm new to Rust!): async fn test_did_open_e2e() {
let initialize = r#"{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{"textDocumentSync":1}},"id":1}"#;
let did_open = r#"{
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///foo.rs",
"languageId": "rust",
"version": 1,
"text": "this is a\ntest fo typos\n"
}
}
}
"#;
let (mut req_client, mut resp_client) = start_server();
let mut buf = vec![0; 1024];
req_client.write_all(req(initialize).as_bytes()).await.unwrap();
let _ = resp_client.read(&mut buf).await.unwrap();
tracing::info!("{}", did_open);
req_client.write_all(req(did_open).as_bytes()).await.unwrap();
let n = resp_client.read(&mut buf).await.unwrap();
assert_eq!(
body(&buf[..n]).unwrap(),
r#"{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"diagnostics":[{"message":"`fo` should be `of`, `for`","range":{"end":{"character":7,"line":1},"start":{"character":5,"line":1}},"severity":2,"source":"typos-lsp"}],"uri":"file:///foo.rs","version":1}}"#,
)
}
fn start_server() -> (tokio::io::DuplexStream, tokio::io::DuplexStream) {
let (req_client, req_server) = tokio::io::duplex(1024);
let (resp_server, resp_client) = tokio::io::duplex(1024);
let (service, socket) = LspService::new(|client| Backend { client });
// start server as concurrent task
tokio::spawn(Server::new(req_server, resp_server, socket).serve(service));
(req_client, resp_client)
}
fn req(msg: &str) -> String {
format!("Content-Length: {}\r\n\r\n{}", msg.len(), msg)
}
fn body(src: &[u8]) -> Result<&str, anyhow::Error> {
// parse headers to get headers length
let mut dst = [httparse::EMPTY_HEADER; 2];
let (headers_len, _) = match httparse::parse_headers(src, &mut dst)? {
httparse::Status::Complete(output) => output,
httparse::Status::Partial => return Err(anyhow::anyhow!("partial headers")),
};
// skip headers
let skipped = &src[headers_len..];
// return the rest (ie: the body) as &str
std::str::from_utf8(skipped).map_err(anyhow::Error::from)
} |
Thanks for this, I was trying to |
I tried lsp testing based on #355 (comment). https://github.com/veryl-lang/veryl/blob/master/crates/languageserver/src/tests.rs In the above test, #[tokio::test]
async fn initialize() {
let mut server = TestServer::new(Backend::new);
let params = InitializeParams::default();
let req = Request::build("initialize")
.params(json!(params))
.id(1)
.finish();
server.send_request(req).await;
let res = server.recv_response().await;
assert!(res.is_ok());
} |
(This is likely just a documentation issue more than anything, but...)
The way I currently test
LanguageServer
implementations is to construct the server and then call the direct methods on the server and checking the response. This works fine, until you need to test interactions with the client, e.g. fortextDocument/publishDiagnostics
. The other oddity that happens as a result of this, is that our language server wraps the client access in an option, because the tests don't have a client when they start up. Now we have production code that has test-specific code in it, which is a huge no-no.I assume there's better machinery than creating the server raw, but it's not clear exactly what the "happy path" is here. Any help would be appreciated.
The text was updated successfully, but these errors were encountered: