Skip to content

Commit

Permalink
feat(tests): Add e2e tests for language server (#11)
Browse files Browse the repository at this point in the history
* feat(tests): Add e2e tests for language server

* fix: aaa

* fix: ciiz

* fix:

* fix: benchmark

* feat: handle shutdown and exit correctly

* nit: run tests in parallel

* nit: comments
  • Loading branch information
Princesseuh authored Dec 12, 2024
1 parent 2a95cc5 commit 0727f3f
Show file tree
Hide file tree
Showing 27 changed files with 905 additions and 908 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/benchmark.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
targets: wasm32-unknown-unknown
bins: cargo-codspeed, wasm-bindgen-cli, just, wasm-opt

- name: Install dependencies
run: just install

- name: Build (WASM)
run: just build-wasm benchmark

Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ jobs:
targets: wasm32-unknown-unknown
bins: wasm-bindgen-cli, just, wasm-opt

- name: Install dependencies
run: just install

- name: Build
run: just build

- name: Build (WASM)
run: just build-wasm

Expand Down
15 changes: 10 additions & 5 deletions crates/weblsp/src/css.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use crate::cast;
use csslsrs::service::LanguageService;
use lsp_server::{Connection, Message, Request, Response};
use lsp_types::request::{
ColorPresentationRequest, DocumentColor, FoldingRangeRequest, HoverRequest,
};
use std::error::Error;

use crate::requests::cast;

/// Initialize our CSS Language Service (CSSlsrs).
/// Used once at the start of the main loop, so the document store stays alive throughout the server's lifetime.
pub fn init_language_service() -> LanguageService {
Expand All @@ -18,13 +22,13 @@ pub fn handle_request(
) -> Result<(), Box<dyn Error + Sync + Send>> {
match req.method.as_str() {
"textDocument/documentColor" => {
let (id, params) = cast::<lsp_types::request::DocumentColor>(req)?;
let (id, params) = cast::<DocumentColor>(req)?;
let colors = language_service
.get_document_colors(get_text_document(params.text_document, language_service)?);
send_result(connection, id, serde_json::to_value(&colors).unwrap())?;
}
"textDocument/colorPresentation" => {
let (id, params) = cast::<lsp_types::request::ColorPresentationRequest>(req)?;
let (id, params) = cast::<ColorPresentationRequest>(req)?;
let presentations =
language_service.get_color_presentations(lsp_types::ColorInformation {
color: params.color,
Expand All @@ -37,13 +41,13 @@ pub fn handle_request(
)?;
}
"textDocument/foldingRange" => {
let (id, params) = cast::<lsp_types::request::FoldingRangeRequest>(req)?;
let (id, params) = cast::<FoldingRangeRequest>(req)?;
let ranges = language_service
.get_folding_ranges(get_text_document(params.text_document, language_service)?);
send_result(connection, id, serde_json::to_value(&ranges).unwrap())?;
}
"textDocument/hover" => {
let (id, params) = cast::<lsp_types::request::HoverRequest>(req)?;
let (id, params) = cast::<HoverRequest>(req)?;
let hover = language_service.get_hover(
get_text_document(
params.text_document_position_params.text_document,
Expand All @@ -69,6 +73,7 @@ fn get_text_document(
None => return Err(Box::from("Document not found")),
};

// TODO: It'd be great to avoid cloning the document here, might need to refactor methods to take a reference instead.
Ok(text_document.document.clone())
}

Expand Down
97 changes: 49 additions & 48 deletions crates/weblsp/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,90 +1,91 @@
pub mod css;
pub mod notifications;
pub mod requests;
use lsp_server::{Connection, ExtractError, Message, Request, RequestId};
use lsp_types::{InitializeParams, ServerCapabilities, TextDocumentSyncCapability};
use std::error::Error;
mod css;
mod notifications;
mod requests;
mod response;
mod server;
use lsp_server::{Connection, Message};
use lsp_types::{notification::Notification, InitializeParams};
use server::get_server_capabilities;
use std::{error::Error, process::ExitCode};

/// Entry point for our WEBlsp server.
/// Heavily inspired by -> https://github.com/rust-lang/rust-analyzer/blob/master/lib/lsp-server/examples/goto_def.rs
fn main() -> Result<(), Box<dyn Error + Sync + Send>> {
fn main() -> Result<ExitCode, Box<dyn Error + Sync + Send>> {
// Note that we must have our logging only write out to stderr.
eprintln!("starting server");

// Create the transport. Includes the stdio (stdin and stdout) versions but this could
// also be implemented to use sockets or HTTP.
let (connection, io_threads) = Connection::stdio();

// Run the server and wait for the two threads to end (typically by trigger LSP Exit event).
let server_capabilities = serde_json::to_value(&ServerCapabilities {
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
color_provider: Some(lsp_types::ColorProviderCapability::Simple(true)),
folding_range_provider: Some(lsp_types::FoldingRangeProviderCapability::Simple(true)),
text_document_sync: Some(TextDocumentSyncCapability::Kind(
lsp_types::TextDocumentSyncKind::FULL,
)),
..Default::default()
})
.unwrap();
let initialization_params = match connection.initialize(server_capabilities) {
Ok(it) => it,
let initialization_params = match connection.initialize(get_server_capabilities()) {
Ok(params) => params,
Err(e) => {
if e.channel_is_disconnected() {
io_threads.join()?;
}
return Err(e.into());
}
};

// Init language services and start the main loop.
let css_language_service = css::init_language_service();
main_loop(connection, initialization_params, css_language_service)?;

// Run the server and wait for the two threads to end (typically by shutdown then exit messages).
let exit_code = main_loop(connection, initialization_params, css_language_service)?;

// Joins the IO threads to ensure all communication is properly finished.
io_threads.join()?;

// Shut down gracefully.
eprintln!("shutting down server");
Ok(())

Ok(exit_code)
}

/// Main loop of our WEBlsp server. Handles all incoming messages, and dispatches them to the appropriate language handler.
fn main_loop(
connection: Connection,
params: serde_json::Value,
init_params: serde_json::Value,
mut css_language_service: csslsrs::service::LanguageService,
) -> Result<(), Box<dyn Error + Sync + Send>> {
let _params: InitializeParams = serde_json::from_value(params).unwrap();
) -> Result<ExitCode, Box<dyn Error + Sync + Send>> {
let mut awaiting_exit = false;
let _init_params: InitializeParams = serde_json::from_value(init_params).unwrap();

for msg in &connection.receiver {
eprintln!("new msg: {msg:?}");
// TODO: Handle trace levels and notifications instead of just printing them to stderr.
eprintln!("new msg: {:?}", msg);

// If we're waiting for an exit notification, any message other than it is an error, and will cause the server to exit with a failure exit code.
// As such, we can handle this outside of the match statement.
if awaiting_exit {
if let Message::Notification(not) = &msg {
if not.method == lsp_types::notification::Exit::METHOD {
return Ok(ExitCode::SUCCESS);
}
}
eprintln!("Shutting down without receiving `Exit` notification.");
return Ok(ExitCode::FAILURE);
}

// Handle the rest of the messages.
match msg {
Message::Request(req) => {
requests::handle_request(req, &mut css_language_service, &connection)?;
continue;
let request =
requests::handle_request(req, &mut css_language_service, &connection)?;

if request.is_shutdown {
awaiting_exit = true;
}
}
Message::Response(resp) => {
handle_response(resp)?;
continue;
response::handle_response(resp)?;
}
Message::Notification(not) => {
notifications::handle_notification(not, &mut css_language_service, &connection)?;
continue;
}
}
}
Ok(())
}

/// TMP: log the response.
fn handle_response(resp: lsp_server::Response) -> Result<(), Box<dyn Error + Sync + Send>> {
eprintln!("handle_response: got {resp:?}");
Ok(())
}

/// Attempts to cast a request to a specific LSP request type.
/// If the request is not of the specified type, an error will be returned.
/// If the request is of the specified type, the request ID and parameters will be returned.
pub fn cast<R>(req: Request) -> Result<(RequestId, R::Params), ExtractError<Request>>
where
R: lsp_types::request::Request,
R::Params: serde::de::DeserializeOwned,
{
req.extract(R::METHOD)
Ok(ExitCode::SUCCESS)
}
7 changes: 4 additions & 3 deletions crates/weblsp/src/notifications.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use lsp_server::Connection;
use lsp_types::{DidChangeTextDocumentParams, DidOpenTextDocumentParams, TextDocumentItem};
use std::error::Error;
use std::{error::Error, process::exit};

/// Used by the main loop to handle notifications. Notifications are messages that the client sends to the server.
/// Notable notifications include `exit`, `textDocument/didOpen`, and `textDocument/didChange`.
Expand All @@ -10,9 +10,10 @@ pub fn handle_notification(
_connection: &Connection,
) -> Result<(), Box<dyn Error + Sync + Send>> {
match notification.method.as_str() {
// Proper shutdown is handled directly by the main loop, so if we get an `exit` notification here, it's an invalid one and we should exit with an error.
"exit" => {
eprintln!("exit: shutting down server");
std::process::exit(0);
eprintln!("Shutting down without receiving `Shutdown` request.");
exit(1);
}
"textDocument/didOpen" => {
// didOpen notification carry a textDocument item, which contains the document's URI and languageId.
Expand Down
41 changes: 34 additions & 7 deletions crates/weblsp/src/requests.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
use lsp_server::Connection;
use lsp_server::{Connection, ExtractError, Request, RequestId};
use lsp_types::request::Request as _;
use std::error::Error;
use std::str::FromStr;

use crate::css;

#[derive(Default)]
pub struct RequestOutcome {
pub(crate) is_shutdown: bool,
}

/// Used by the main loop. Based on the document's language, this function will dispatch the request to the appropriate language handler.
/// Requests are LSP features that the client wants to use, and the server must respond to each request.
pub fn handle_request(
req: lsp_server::Request,
css_language_service: &mut csslsrs::service::LanguageService,
connection: &Connection,
) -> Result<(), Box<dyn Error + Sync + Send>> {
) -> Result<RequestOutcome, Box<dyn Error + Sync + Send>> {
if req.method == lsp_types::request::Shutdown::METHOD {
connection
.sender
.send(lsp_server::Message::Response(lsp_server::Response::new_ok(
req.id,
(),
)))?;

return Ok(RequestOutcome { is_shutdown: true });
}

let language_id = get_language_id(&req, css_language_service)?;
match language_id.as_str() {
"css" => {
Expand All @@ -20,7 +37,8 @@ pub fn handle_request(
eprintln!("unsupported language: {}", language_id);
}
}
Ok(())

Ok(RequestOutcome::default())
}

// TMP: TODO: For now, we use CSSlsrs' store, because we only support CSS. So I can just retrieve the document from this store from its URI.
Expand All @@ -36,12 +54,10 @@ fn get_language_id(
.get("textDocument")
.and_then(|td| td.get("uri"))
.and_then(|uri| uri.as_str())
.and_then(|uri| lsp_types::Uri::from_str(uri).ok())
.ok_or("Missing or invalid 'textDocument.uri' in request parameters")?;

let text_document_uri = lsp_types::Uri::from_str(text_document_identifier)
.map_err(|_| "Invalid 'textDocument.uri' in request parameters")?;

let store_entry = match css_language_service.get_document(&text_document_uri) {
let store_entry = match css_language_service.get_document(&text_document_identifier) {
Some(doc) => doc,
None => return Err(Box::from("Document not found")),
};
Expand All @@ -52,3 +68,14 @@ fn get_language_id(
// The immutable borrow ends here
Ok(language_id)
}

/// Attempts to cast a request to a specific LSP request type.
/// If the request is not of the specified type, an error will be returned.
/// If the request is of the specified type, the request ID and parameters will be returned.
pub fn cast<R>(req: Request) -> Result<(RequestId, R::Params), ExtractError<Request>>
where
R: lsp_types::request::Request,
R::Params: serde::de::DeserializeOwned,
{
req.extract(R::METHOD)
}
7 changes: 7 additions & 0 deletions crates/weblsp/src/response.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use std::error::Error;

/// TMP: log the response.
pub fn handle_response(resp: lsp_server::Response) -> Result<(), Box<dyn Error + Sync + Send>> {
eprintln!("handle_response: got {resp:?}");
Ok(())
}
15 changes: 15 additions & 0 deletions crates/weblsp/src/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use lsp_types::{ServerCapabilities, TextDocumentSyncCapability};

pub(crate) fn get_server_capabilities() -> serde_json::Value {
let capabilities = ServerCapabilities {
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
color_provider: Some(lsp_types::ColorProviderCapability::Simple(true)),
folding_range_provider: Some(lsp_types::FoldingRangeProviderCapability::Simple(true)),
text_document_sync: Some(TextDocumentSyncCapability::Kind(
lsp_types::TextDocumentSyncKind::FULL,
)),
..Default::default()
};

serde_json::to_value(capabilities).unwrap()
}
13 changes: 9 additions & 4 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
alias b := build
alias bw := build-wasm
alias t := test
alias bm := benchmark
alias i := install

default: build

default_mode := "debug"

install:
pnpm install

build mode=default_mode:
just fetch-data
echo "Building to native target..."
Expand All @@ -15,7 +22,6 @@ build-wasm mode=default_mode:
cargo build --package csslsrs --target wasm32-unknown-unknown {{ if mode == "release" {"--release"} else if mode == "benchmark" {"--profile benchmark"} else {""} }} --features wasm
wasm-bindgen ./target/wasm32-unknown-unknown/{{mode}}/csslsrs.wasm --out-dir ./packages/csslsrs/src/generated --target=experimental-nodejs-module {{ if mode == "release" { "" } else { "--keep-debug" } }}
{{ if mode == "release" { "wasm-opt -O4 ./packages/csslsrs/src/generated/csslsrs_bg.wasm -o ./packages/csslsrs/src/generated/csslsrs_bg.wasm" } else { "" } }}
pnpm -C ./packages/csslsrs install
pnpm -C ./packages/csslsrs run build

fetch-data:
Expand All @@ -27,11 +33,10 @@ test:
echo "Running Rust tests..."
cargo test
echo "Running JS tests..."
pnpm -C ./packages/csslsrs run test
pnpm -r run test --run

benchmark:
echo "Running Native benchmarks..."
cargo bench
echo "Running WASM benchmarks..."
just build-wasm release
pnpm -C ./packages/benchmark-wasm run benchmark
pnpm -C ./packages/benchmark-wasm run benchmark --run
Loading

0 comments on commit 0727f3f

Please sign in to comment.