mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-26 13:03:31 +00:00
Add an integration test
This commit is contained in:
parent
e8515fecd7
commit
541170420b
9 changed files with 266 additions and 16 deletions
|
@ -15,7 +15,7 @@ mod stdio;
|
||||||
use crossbeam_channel::{Sender, Receiver};
|
use crossbeam_channel::{Sender, Receiver};
|
||||||
use languageserver_types::{
|
use languageserver_types::{
|
||||||
ServerCapabilities, InitializeResult,
|
ServerCapabilities, InitializeResult,
|
||||||
request::{Initialize},
|
request::{Initialize, Shutdown},
|
||||||
notification::{Initialized, Exit},
|
notification::{Initialized, Exit},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,6 +48,17 @@ pub fn run_server(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle_shutdown(req: RawRequest, sender: &Sender<RawMessage>) -> Option<RawRequest> {
|
||||||
|
match req.cast::<Shutdown>() {
|
||||||
|
Ok((id, ())) => {
|
||||||
|
let resp = RawResponse::ok::<Shutdown>(id, ());
|
||||||
|
sender.send(RawMessage::Response(resp));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Err(req) => Some(req),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn initialize(
|
||||||
receiver: &mut Receiver<RawMessage>,
|
receiver: &mut Receiver<RawMessage>,
|
||||||
sender: &mut Sender<RawMessage>,
|
sender: &mut Sender<RawMessage>,
|
||||||
|
@ -61,7 +72,7 @@ fn initialize(
|
||||||
msg =>
|
msg =>
|
||||||
bail!("expected initialize request, got {:?}", msg),
|
bail!("expected initialize request, got {:?}", msg),
|
||||||
};
|
};
|
||||||
let resp = RawResponse::ok(id, InitializeResult { capabilities: caps });
|
let resp = RawResponse::ok::<Initialize>(id, InitializeResult { capabilities: caps });
|
||||||
sender.send(RawMessage::Response(resp));
|
sender.send(RawMessage::Response(resp));
|
||||||
match receiver.recv() {
|
match receiver.recv() {
|
||||||
Some(RawMessage::Notification(n)) => {
|
Some(RawMessage::Notification(n)) => {
|
||||||
|
|
|
@ -87,6 +87,17 @@ impl RawMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RawRequest {
|
impl RawRequest {
|
||||||
|
pub fn new<R>(id: u64, params: R::Params) -> RawRequest
|
||||||
|
where
|
||||||
|
R: Request,
|
||||||
|
R::Params: Serialize,
|
||||||
|
{
|
||||||
|
RawRequest {
|
||||||
|
id: id,
|
||||||
|
method: R::METHOD.to_string(),
|
||||||
|
params: to_value(¶ms).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn cast<R>(self) -> ::std::result::Result<(u64, R::Params), RawRequest>
|
pub fn cast<R>(self) -> ::std::result::Result<(u64, R::Params), RawRequest>
|
||||||
where
|
where
|
||||||
R: Request,
|
R: Request,
|
||||||
|
@ -102,7 +113,10 @@ impl RawRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RawResponse {
|
impl RawResponse {
|
||||||
pub fn ok(id: u64, result: impl Serialize) -> RawResponse {
|
pub fn ok<R>(id: u64, result: R::Result) -> RawResponse
|
||||||
|
where R: Request,
|
||||||
|
R::Result: Serialize,
|
||||||
|
{
|
||||||
RawResponse {
|
RawResponse {
|
||||||
id,
|
id,
|
||||||
result: Some(to_value(&result).unwrap()),
|
result: Some(to_value(&result).unwrap()),
|
||||||
|
|
|
@ -24,3 +24,6 @@ libsyntax2 = { path = "../libsyntax2" }
|
||||||
libeditor = { path = "../libeditor" }
|
libeditor = { path = "../libeditor" }
|
||||||
libanalysis = { path = "../libanalysis" }
|
libanalysis = { path = "../libanalysis" }
|
||||||
gen_lsp_server = { path = "../gen_lsp_server" }
|
gen_lsp_server = { path = "../gen_lsp_server" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempdir = "0.3.7"
|
||||||
|
|
|
@ -21,7 +21,7 @@ extern crate im;
|
||||||
extern crate relative_path;
|
extern crate relative_path;
|
||||||
|
|
||||||
mod caps;
|
mod caps;
|
||||||
mod req;
|
pub mod req;
|
||||||
mod conv;
|
mod conv;
|
||||||
mod main_loop;
|
mod main_loop;
|
||||||
mod vfs;
|
mod vfs;
|
||||||
|
|
|
@ -11,7 +11,10 @@ use serde::{Serialize, de::DeserializeOwned};
|
||||||
use crossbeam_channel::{bounded, Sender, Receiver};
|
use crossbeam_channel::{bounded, Sender, Receiver};
|
||||||
use languageserver_types::{NumberOrString};
|
use languageserver_types::{NumberOrString};
|
||||||
use libanalysis::{FileId, JobHandle, JobToken};
|
use libanalysis::{FileId, JobHandle, JobToken};
|
||||||
use gen_lsp_server::{RawRequest, RawNotification, RawMessage, RawResponse, ErrorCode};
|
use gen_lsp_server::{
|
||||||
|
RawRequest, RawNotification, RawMessage, RawResponse, ErrorCode,
|
||||||
|
handle_shutdown,
|
||||||
|
};
|
||||||
|
|
||||||
use {
|
use {
|
||||||
req,
|
req,
|
||||||
|
@ -21,6 +24,7 @@ use {
|
||||||
main_loop::subscriptions::{Subscriptions},
|
main_loop::subscriptions::{Subscriptions},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
enum Task {
|
enum Task {
|
||||||
Respond(RawResponse),
|
Respond(RawResponse),
|
||||||
Notify(RawNotification),
|
Notify(RawNotification),
|
||||||
|
@ -40,7 +44,7 @@ pub fn main_loop(
|
||||||
|
|
||||||
let mut pending_requests = HashMap::new();
|
let mut pending_requests = HashMap::new();
|
||||||
let mut subs = Subscriptions::new();
|
let mut subs = Subscriptions::new();
|
||||||
main_loop_inner(
|
let res = main_loop_inner(
|
||||||
&pool,
|
&pool,
|
||||||
msg_receriver,
|
msg_receriver,
|
||||||
msg_sender,
|
msg_sender,
|
||||||
|
@ -50,17 +54,19 @@ pub fn main_loop(
|
||||||
&mut state,
|
&mut state,
|
||||||
&mut pending_requests,
|
&mut pending_requests,
|
||||||
&mut subs,
|
&mut subs,
|
||||||
)?;
|
);
|
||||||
|
|
||||||
info!("waiting for background jobs to finish...");
|
info!("waiting for tasks to finish...");
|
||||||
task_receiver.for_each(|task| on_task(task, msg_sender, &mut pending_requests));
|
task_receiver.for_each(|task| on_task(task, msg_sender, &mut pending_requests));
|
||||||
|
info!("...tasks have finished");
|
||||||
|
info!("joining threadpool...");
|
||||||
pool.join();
|
pool.join();
|
||||||
info!("...background jobs have finished");
|
info!("...threadpool has finished");
|
||||||
|
|
||||||
info!("waiting for file watcher to finish...");
|
info!("waiting for file watcher to finish...");
|
||||||
watcher.stop()?;
|
watcher.stop()?;
|
||||||
info!("...file watcher has finished");
|
info!("...file watcher has finished");
|
||||||
Ok(())
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main_loop_inner(
|
fn main_loop_inner(
|
||||||
|
@ -73,15 +79,17 @@ fn main_loop_inner(
|
||||||
state: &mut ServerWorldState,
|
state: &mut ServerWorldState,
|
||||||
pending_requests: &mut HashMap<u64, JobHandle>,
|
pending_requests: &mut HashMap<u64, JobHandle>,
|
||||||
subs: &mut Subscriptions,
|
subs: &mut Subscriptions,
|
||||||
) -> Result<u64> {
|
) -> Result<()> {
|
||||||
let mut fs_receiver = Some(fs_receiver);
|
let mut fs_receiver = Some(fs_receiver);
|
||||||
loop {
|
loop {
|
||||||
|
#[derive(Debug)]
|
||||||
enum Event {
|
enum Event {
|
||||||
Msg(RawMessage),
|
Msg(RawMessage),
|
||||||
Task(Task),
|
Task(Task),
|
||||||
Fs(Vec<FileEvent>),
|
Fs(Vec<FileEvent>),
|
||||||
FsWatcherDead,
|
FsWatcherDead,
|
||||||
}
|
}
|
||||||
|
trace!("selecting");
|
||||||
let event = select! {
|
let event = select! {
|
||||||
recv(msg_receiver, msg) => match msg {
|
recv(msg_receiver, msg) => match msg {
|
||||||
Some(msg) => Event::Msg(msg),
|
Some(msg) => Event::Msg(msg),
|
||||||
|
@ -93,6 +101,7 @@ fn main_loop_inner(
|
||||||
None => Event::FsWatcherDead,
|
None => Event::FsWatcherDead,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
trace!("selected {:?}", event);
|
||||||
let mut state_changed = false;
|
let mut state_changed = false;
|
||||||
match event {
|
match event {
|
||||||
Event::FsWatcherDead => fs_receiver = None,
|
Event::FsWatcherDead => fs_receiver = None,
|
||||||
|
@ -105,9 +114,9 @@ fn main_loop_inner(
|
||||||
Event::Msg(msg) => {
|
Event::Msg(msg) => {
|
||||||
match msg {
|
match msg {
|
||||||
RawMessage::Request(req) => {
|
RawMessage::Request(req) => {
|
||||||
let req = match req.cast::<req::Shutdown>() {
|
let req = match handle_shutdown(req, msg_sender) {
|
||||||
Ok((id, _params)) => return Ok(id),
|
Some(req) => req,
|
||||||
Err(req) => req,
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
match on_request(state, pending_requests, pool, &task_sender, req)? {
|
match on_request(state, pending_requests, pool, &task_sender, req)? {
|
||||||
None => (),
|
None => (),
|
||||||
|
@ -290,7 +299,7 @@ impl<'a> PoolDispatcher<'a> {
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
self.pool.execute(move || {
|
self.pool.execute(move || {
|
||||||
let resp = match f(world, params, token) {
|
let resp = match f(world, params, token) {
|
||||||
Ok(resp) => RawResponse::ok(id, resp),
|
Ok(resp) => RawResponse::ok::<R>(id, resp),
|
||||||
Err(e) => RawResponse::err(id, ErrorCode::InternalError as i32, e.to_string()),
|
Err(e) => RawResponse::err(id, ErrorCode::InternalError as i32, e.to_string()),
|
||||||
};
|
};
|
||||||
let task = Task::Respond(resp);
|
let task = Task::Respond(resp);
|
||||||
|
|
|
@ -127,7 +127,7 @@ impl Request for Runnables {
|
||||||
const METHOD: &'static str = "m/runnables";
|
const METHOD: &'static str = "m/runnables";
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct RunnablesParams {
|
pub struct RunnablesParams {
|
||||||
pub text_document: TextDocumentIdentifier,
|
pub text_document: TextDocumentIdentifier,
|
||||||
|
|
|
@ -11,11 +11,13 @@ use walkdir::WalkDir;
|
||||||
use Result;
|
use Result;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct FileEvent {
|
pub struct FileEvent {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub kind: FileEventKind,
|
pub kind: FileEventKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum FileEventKind {
|
pub enum FileEventKind {
|
||||||
Add(String),
|
Add(String),
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
|
42
crates/server/tests/heavy_tests/main.rs
Normal file
42
crates/server/tests/heavy_tests/main.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
extern crate tempdir;
|
||||||
|
extern crate crossbeam_channel;
|
||||||
|
extern crate languageserver_types;
|
||||||
|
extern crate serde;
|
||||||
|
extern crate serde_json;
|
||||||
|
extern crate gen_lsp_server;
|
||||||
|
extern crate flexi_logger;
|
||||||
|
extern crate m;
|
||||||
|
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
use m::req::{Runnables, RunnablesParams};
|
||||||
|
|
||||||
|
use support::project;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_runnables() {
|
||||||
|
let server = project(r"
|
||||||
|
//- lib.rs
|
||||||
|
#[test]
|
||||||
|
fn foo() {
|
||||||
|
}
|
||||||
|
");
|
||||||
|
server.request::<Runnables>(
|
||||||
|
RunnablesParams {
|
||||||
|
text_document: server.doc_id("lib.rs"),
|
||||||
|
position: None,
|
||||||
|
},
|
||||||
|
r#"[
|
||||||
|
{
|
||||||
|
"args": [ "test", "--", "foo", "--nocapture" ],
|
||||||
|
"bin": "cargo",
|
||||||
|
"env": { "RUST_BACKTRACE": "short" },
|
||||||
|
"label": "test foo",
|
||||||
|
"range": {
|
||||||
|
"end": { "character": 1, "line": 2 },
|
||||||
|
"start": { "character": 0, "line": 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]"#
|
||||||
|
);
|
||||||
|
}
|
169
crates/server/tests/heavy_tests/support.rs
Normal file
169
crates/server/tests/heavy_tests/support.rs
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
thread,
|
||||||
|
cell::Cell,
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
use tempdir::TempDir;
|
||||||
|
use crossbeam_channel::{bounded, Sender, Receiver};
|
||||||
|
use flexi_logger::Logger;
|
||||||
|
use languageserver_types::{
|
||||||
|
Url,
|
||||||
|
TextDocumentIdentifier,
|
||||||
|
request::{Request, Shutdown},
|
||||||
|
notification::DidOpenTextDocument,
|
||||||
|
DidOpenTextDocumentParams,
|
||||||
|
TextDocumentItem,
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::{Value, from_str, to_string_pretty};
|
||||||
|
use gen_lsp_server::{RawMessage, RawRequest, RawNotification};
|
||||||
|
|
||||||
|
use m::{Result, main_loop};
|
||||||
|
|
||||||
|
pub fn project(fixture: &str) -> Server {
|
||||||
|
Logger::with_env_or_str("").start().unwrap();
|
||||||
|
|
||||||
|
let tmp_dir = TempDir::new("test-project")
|
||||||
|
.unwrap();
|
||||||
|
let mut buf = String::new();
|
||||||
|
let mut file_name = None;
|
||||||
|
let mut paths = vec![];
|
||||||
|
macro_rules! flush {
|
||||||
|
() => {
|
||||||
|
if let Some(file_name) = file_name {
|
||||||
|
let path = tmp_dir.path().join(file_name);
|
||||||
|
fs::write(path.as_path(), buf.as_bytes()).unwrap();
|
||||||
|
paths.push((path, buf.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for line in fixture.lines() {
|
||||||
|
if line.starts_with("//-") {
|
||||||
|
flush!();
|
||||||
|
buf.clear();
|
||||||
|
file_name = Some(line["//-".len()..].trim());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
buf.push_str(line);
|
||||||
|
buf.push('\n');
|
||||||
|
}
|
||||||
|
flush!();
|
||||||
|
|
||||||
|
Server::new(tmp_dir, paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Server {
|
||||||
|
req_id: Cell<u64>,
|
||||||
|
dir: TempDir,
|
||||||
|
sender: Option<Sender<RawMessage>>,
|
||||||
|
receiver: Receiver<RawMessage>,
|
||||||
|
server: Option<thread::JoinHandle<Result<()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
fn new(dir: TempDir, files: Vec<(PathBuf, String)>) -> Server {
|
||||||
|
let path = dir.path().to_path_buf();
|
||||||
|
let (client_sender, mut server_receiver) = bounded(1);
|
||||||
|
let (mut server_sender, client_receiver) = bounded(1);
|
||||||
|
let server = thread::spawn(move || main_loop(path, &mut server_receiver, &mut server_sender));
|
||||||
|
let res = Server {
|
||||||
|
req_id: Cell::new(1),
|
||||||
|
dir,
|
||||||
|
sender: Some(client_sender),
|
||||||
|
receiver: client_receiver,
|
||||||
|
server: Some(server),
|
||||||
|
};
|
||||||
|
for (path, text) in files {
|
||||||
|
res.send_notification(RawNotification::new::<DidOpenTextDocument>(
|
||||||
|
DidOpenTextDocumentParams {
|
||||||
|
text_document: TextDocumentItem {
|
||||||
|
uri: Url::from_file_path(path).unwrap(),
|
||||||
|
language_id: "rust".to_string(),
|
||||||
|
version: 0,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn doc_id(&self, rel_path: &str) -> TextDocumentIdentifier {
|
||||||
|
let path = self.dir.path().join(rel_path);
|
||||||
|
TextDocumentIdentifier {
|
||||||
|
uri: Url::from_file_path(path).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request<R>(
|
||||||
|
&self,
|
||||||
|
params: R::Params,
|
||||||
|
expected_resp: &str,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
R: Request,
|
||||||
|
R::Params: Serialize,
|
||||||
|
{
|
||||||
|
let id = self.req_id.get();
|
||||||
|
self.req_id.set(id + 1);
|
||||||
|
let expected_resp: Value = from_str(expected_resp).unwrap();
|
||||||
|
let actual = self.send_request::<R>(id, params);
|
||||||
|
assert_eq!(
|
||||||
|
expected_resp, actual,
|
||||||
|
"Expected:\n{}\n\
|
||||||
|
Actual:\n{}\n",
|
||||||
|
to_string_pretty(&expected_resp).unwrap(),
|
||||||
|
to_string_pretty(&actual).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_request<R>(&self, id: u64, params: R::Params) -> Value
|
||||||
|
where
|
||||||
|
R: Request,
|
||||||
|
R::Params: Serialize,
|
||||||
|
{
|
||||||
|
let r = RawRequest::new::<R>(id, params);
|
||||||
|
self.sender.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.send(RawMessage::Request(r));
|
||||||
|
|
||||||
|
while let Some(msg) = self.receiver.recv() {
|
||||||
|
match msg {
|
||||||
|
RawMessage::Request(req) => panic!("unexpected request: {:?}", req),
|
||||||
|
RawMessage::Notification(_) => (),
|
||||||
|
RawMessage::Response(res) => {
|
||||||
|
assert_eq!(res.id, id);
|
||||||
|
if let Some(err) = res.error {
|
||||||
|
panic!("error response: {:#?}", err);
|
||||||
|
}
|
||||||
|
return res.result.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("no response");
|
||||||
|
}
|
||||||
|
fn send_notification(&self, not: RawNotification) {
|
||||||
|
|
||||||
|
self.sender.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.send(RawMessage::Notification(not));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Server {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
{
|
||||||
|
self.send_request::<Shutdown>(666, ());
|
||||||
|
drop(self.sender.take().unwrap());
|
||||||
|
while let Some(msg) = self.receiver.recv() {
|
||||||
|
drop(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eprintln!("joining server");
|
||||||
|
self.server.take()
|
||||||
|
.unwrap()
|
||||||
|
.join().unwrap().unwrap();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue