Skip to content

Commit

Permalink
Add language server test
Browse files Browse the repository at this point in the history
  • Loading branch information
dalance committed Aug 6, 2024
1 parent 3a81268 commit bafc107
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 2 deletions.
6 changes: 4 additions & 2 deletions crates/languageserver/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#![recursion_limit = "256"]

use tower_lsp::{LspService, Server};

mod backend;
mod keyword;
mod server;
#[cfg(test)]
mod tests;

use backend::Backend;
use tower_lsp::{LspService, Server};

#[tokio::main]
async fn main() {
Expand Down
2 changes: 2 additions & 0 deletions crates/languageserver/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,9 +593,11 @@ impl Server {

fn get_metadata(&mut self, url: &Url) -> Option<Metadata> {
let path = url.path();
dbg!(&path);
if let Some(metadata) = self.metadata_map.get(path) {
return Some(metadata.to_owned());
} else if let Ok(metadata_path) = Metadata::search_from(path) {
dbg!(&metadata_path);
if let Ok(metadata) = Metadata::load(metadata_path) {
self.metadata_map.insert(path.to_string(), metadata.clone());
return Some(metadata);
Expand Down
246 changes: 246 additions & 0 deletions crates/languageserver/src/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
use crate::Backend;
use serde_json::{json, Value};
use std::collections::VecDeque;
use std::env;
use std::path::PathBuf;
use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream};
use tower_lsp::jsonrpc::{Id, Request, Response};
use tower_lsp::lsp_types::*;
use tower_lsp::{Client, LanguageServer, LspService, Server};

struct TestServer {
req_stream: DuplexStream,
res_stream: DuplexStream,
responses: VecDeque<String>,
}

impl TestServer {
fn new<F, S>(init: F) -> Self
where
F: FnOnce(Client) -> S,
S: LanguageServer,
{
let (req_client, req_server) = tokio::io::duplex(1024);
let (res_server, res_client) = tokio::io::duplex(1024);

let (service, socket) = LspService::new(init);

tokio::spawn(Server::new(req_server, res_server, socket).serve(service));

Self {
req_stream: req_client,
res_stream: res_client,
responses: VecDeque::new(),
}
}

fn encode(payload: &str) -> String {
format!("Content-Length: {}\r\n\r\n{}", payload.len(), payload)
}

fn decode(text: &str) -> Vec<String> {
let mut ret = Vec::new();
let mut temp = text;

while !temp.is_empty() {
let p = temp.find("\r\n\r\n").unwrap();
let (header, body) = temp.split_at(p + 4);
let len = header
.strip_prefix("Content-Length: ")
.unwrap()
.strip_suffix("\r\n\r\n")
.unwrap();
let len: usize = len.parse().unwrap();
let (body, rest) = body.split_at(len);
ret.push(body.to_string());
temp = rest;
}

ret
}

async fn send_request(&mut self, req: Request) {
let req = serde_json::to_string(&req).unwrap();
let req = Self::encode(&req);
self.req_stream.write_all(req.as_bytes()).await.unwrap();
}

async fn send_ack(&mut self, id: &Id) {
let req = Response::from_ok(id.clone(), None::<serde_json::Value>.into());
let req = serde_json::to_string(&req).unwrap();
let req = Self::encode(&req);
self.req_stream.write_all(req.as_bytes()).await.unwrap();
}

async fn recv_response(&mut self) -> Response {
if self.responses.is_empty() {
let mut buf = vec![0; 1024];
let n = self.res_stream.read(&mut buf).await.unwrap();
let ret = String::from_utf8(buf[..n].to_vec()).unwrap();
for x in Self::decode(&ret) {
self.responses.push_front(x);
}
}
let res = self.responses.pop_back().unwrap();
serde_json::from_str(&res).unwrap()
}

async fn recv_notification(&mut self) -> Request {
if self.responses.is_empty() {
let mut buf = vec![0; 1024];
let n = self.res_stream.read(&mut buf).await.unwrap();
let ret = String::from_utf8(buf[..n].to_vec()).unwrap();
for x in Self::decode(&ret) {
self.responses.push_front(x);
}
}
let res = self.responses.pop_back().unwrap();
serde_json::from_str(&res).unwrap()
}
}

fn build_initialize(id: i64) -> Request {
let params = InitializeParams::default();
Request::build("initialize")
.params(json!(params))
.id(id)
.finish()
}

fn build_initialized() -> Request {
let params = InitializedParams {};
Request::build("initialized").params(json!(params)).finish()
}

fn build_did_open(text: &str) -> Request {
let mut path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
path.pop();
path.pop();
path.push("test.veryl");
let uri = Url::from_file_path(path).unwrap();
let text_document = TextDocumentItem {
uri,
language_id: "veryl".to_string(),
version: 0,
text: text.to_string(),
};

let params = DidOpenTextDocumentParams { text_document };

Request::build("textDocument/didOpen")
.params(json!(params))
.finish()
}

#[tokio::test]
async fn did_open() {
let mut server = TestServer::new(Backend::new);

let req = build_initialize(1);
server.send_request(req).await;
let res = server.recv_response().await;
assert!(res.is_ok());

let req = build_initialized();
server.send_request(req).await;
let res = server.recv_notification().await;
assert_eq!(res.method(), "window/logMessage");
assert_eq!(res.params().unwrap()["message"], "server initialized!");

let req = build_did_open("module A {}");
server.send_request(req).await;

let res = server.recv_notification().await;
assert_eq!(res.method(), "window/logMessage");
assert_eq!(res.params().unwrap()["message"], "did_open");

let res = server.recv_notification().await;
dbg!(&res);
assert_eq!(res.method(), "textDocument/publishDiagnostics");
assert!(res.params().unwrap()["diagnostics"]
.as_array()
.unwrap()
.is_empty());
}

#[tokio::test]
async fn diagnostics() {
let mut server = TestServer::new(Backend::new);

let req = build_initialize(1);
server.send_request(req).await;
let res = server.recv_response().await;
assert!(res.is_ok());

let req = build_initialized();
server.send_request(req).await;
let res = server.recv_notification().await;
assert_eq!(res.method(), "window/logMessage");
assert_eq!(res.params().unwrap()["message"], "server initialized!");

let req = build_did_open("module A { var a: logic; }");
server.send_request(req).await;

let res = server.recv_notification().await;
assert_eq!(res.method(), "window/logMessage");
assert_eq!(res.params().unwrap()["message"], "did_open");

let res = server.recv_notification().await;
dbg!(&res);
assert_eq!(res.method(), "textDocument/publishDiagnostics");
let diags = res.params().unwrap()["diagnostics"].as_array().unwrap();
assert_eq!(diags[0]["code"], Value::from("unused_variable"));
assert_eq!(diags[0]["range"]["start"]["character"], Value::from(15));
assert_eq!(diags[0]["range"]["start"]["line"], Value::from(0));
assert_eq!(diags[0]["range"]["end"]["character"], Value::from(16));
assert_eq!(diags[0]["range"]["end"]["line"], Value::from(0));
}

#[tokio::test]
async fn progress() {
let mut server = TestServer::new(Backend::new);

let req = build_initialize(1);
server.send_request(req).await;
let res = server.recv_response().await;
assert!(res.is_ok());

let req = build_initialized();
server.send_request(req).await;
let res = server.recv_notification().await;
assert_eq!(res.method(), "window/logMessage");
assert_eq!(res.params().unwrap()["message"], "server initialized!");

let req = build_did_open("module A {}");
server.send_request(req).await;

let res = server.recv_notification().await;
assert_eq!(res.method(), "window/logMessage");
assert_eq!(res.params().unwrap()["message"], "did_open");

let res = server.recv_notification().await;
dbg!(&res);
assert_eq!(res.method(), "textDocument/publishDiagnostics");

let res = server.recv_notification().await;
assert_eq!(res.method(), "window/workDoneProgress/create");

server.send_ack(res.id().unwrap()).await;

let mut progress_finish = false;
let mut percentage = 0;
while !progress_finish {
let res = server.recv_notification().await;
dbg!(&res);
if res.method() == "$/progress" {
if res.params().unwrap()["value"]["kind"] == Value::from("end") {
progress_finish = true;
} else if res.params().unwrap()["value"]["kind"] == Value::from("report") {
percentage = res.params().unwrap()["value"]["percentage"]
.as_i64()
.unwrap();
}
}
}
assert_eq!(percentage, 100);
}

0 comments on commit bafc107

Please sign in to comment.