Skip to content

Commit e90533d

Browse files
committed
chore(pglt_lsp): add lifecycle test
1 parent bc762f3 commit e90533d

File tree

3 files changed

+347
-0
lines changed

3 files changed

+347
-0
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pglt_lsp/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ tower-lsp = { version = "0.20.0" }
3333
tracing = { workspace = true, features = ["attributes"] }
3434

3535
[dev-dependencies]
36+
tokio = { workspace = true, features = ["macros"] }
37+
tower = { version = "0.4.13", features = ["timeout"] }
38+
3639

3740
[lib]
3841
doctest = false

crates/pglt_lsp/tests/server.rs

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
use anyhow::bail;
2+
use anyhow::Context;
3+
use anyhow::Error;
4+
use anyhow::Result;
5+
use futures::channel::mpsc::{channel, Sender};
6+
use futures::Sink;
7+
use futures::SinkExt;
8+
use futures::Stream;
9+
use futures::StreamExt;
10+
use pglt_lsp::LSPServer;
11+
use pglt_lsp::ServerFactory;
12+
use serde::de::DeserializeOwned;
13+
use serde::Serialize;
14+
use serde_json::{from_value, to_value};
15+
use std::any::type_name;
16+
use std::fmt::Display;
17+
use std::time::Duration;
18+
use tower::timeout::Timeout;
19+
use tower::{Service, ServiceExt};
20+
use tower_lsp::jsonrpc;
21+
use tower_lsp::jsonrpc::Response;
22+
use tower_lsp::lsp_types as lsp;
23+
use tower_lsp::lsp_types::DidCloseTextDocumentParams;
24+
use tower_lsp::lsp_types::DidOpenTextDocumentParams;
25+
use tower_lsp::lsp_types::InitializeResult;
26+
use tower_lsp::lsp_types::InitializedParams;
27+
use tower_lsp::lsp_types::PublishDiagnosticsParams;
28+
use tower_lsp::lsp_types::TextDocumentContentChangeEvent;
29+
use tower_lsp::lsp_types::TextDocumentIdentifier;
30+
use tower_lsp::lsp_types::TextDocumentItem;
31+
use tower_lsp::lsp_types::VersionedTextDocumentIdentifier;
32+
use tower_lsp::lsp_types::{ClientCapabilities, Url};
33+
use tower_lsp::lsp_types::{DidChangeConfigurationParams, DidChangeTextDocumentParams};
34+
use tower_lsp::LspService;
35+
use tower_lsp::{jsonrpc::Request, lsp_types::InitializeParams};
36+
37+
/// Statically build an [Url] instance that points to the file at `$path`
38+
/// within the workspace. The filesystem path contained in the return URI is
39+
/// guaranteed to be a valid path for the underlying operating system, but
40+
/// doesn't have to refer to an existing file on the host machine.
41+
macro_rules! url {
42+
($path:literal) => {
43+
if cfg!(windows) {
44+
lsp::Url::parse(concat!("file:///z%3A/workspace/", $path)).unwrap()
45+
} else {
46+
lsp::Url::parse(concat!("file:///workspace/", $path)).unwrap()
47+
}
48+
};
49+
}
50+
51+
struct Server {
52+
service: Timeout<LspService<LSPServer>>,
53+
}
54+
55+
impl Server {
56+
fn new(service: LspService<LSPServer>) -> Self {
57+
Self {
58+
service: Timeout::new(service, Duration::from_secs(1)),
59+
}
60+
}
61+
62+
async fn notify<P>(&mut self, method: &'static str, params: P) -> Result<()>
63+
where
64+
P: Serialize,
65+
{
66+
self.service
67+
.ready()
68+
.await
69+
.map_err(Error::msg)
70+
.context("ready() returned an error")?
71+
.call(
72+
Request::build(method)
73+
.params(to_value(&params).context("failed to serialize params")?)
74+
.finish(),
75+
)
76+
.await
77+
.map_err(Error::msg)
78+
.context("call() returned an error")
79+
.and_then(|res| {
80+
if let Some(res) = res {
81+
bail!("shutdown returned {:?}", res)
82+
} else {
83+
Ok(())
84+
}
85+
})
86+
}
87+
88+
async fn request<P, R>(
89+
&mut self,
90+
method: &'static str,
91+
id: &'static str,
92+
params: P,
93+
) -> Result<Option<R>>
94+
where
95+
P: Serialize,
96+
R: DeserializeOwned,
97+
{
98+
self.service
99+
.ready()
100+
.await
101+
.map_err(Error::msg)
102+
.context("ready() returned an error")?
103+
.call(
104+
Request::build(method)
105+
.id(id)
106+
.params(to_value(&params).context("failed to serialize params")?)
107+
.finish(),
108+
)
109+
.await
110+
.map_err(Error::msg)
111+
.context("call() returned an error")?
112+
.map(|res| {
113+
let (_, body) = res.into_parts();
114+
115+
let body =
116+
body.with_context(|| format!("response to {method:?} contained an error"))?;
117+
118+
from_value(body.clone()).with_context(|| {
119+
format!(
120+
"failed to deserialize type {} from response {body:?}",
121+
type_name::<R>()
122+
)
123+
})
124+
})
125+
.transpose()
126+
}
127+
128+
/// Basic implementation of the `initialize` request for tests
129+
// The `root_path` field is deprecated, but we still need to specify it
130+
#[allow(deprecated)]
131+
async fn initialize(&mut self) -> Result<()> {
132+
let _res: InitializeResult = self
133+
.request(
134+
"initialize",
135+
"_init",
136+
InitializeParams {
137+
process_id: None,
138+
root_path: None,
139+
root_uri: Some(url!("")),
140+
initialization_options: None,
141+
capabilities: ClientCapabilities::default(),
142+
trace: None,
143+
workspace_folders: None,
144+
client_info: None,
145+
locale: None,
146+
},
147+
)
148+
.await?
149+
.context("initialize returned None")?;
150+
151+
Ok(())
152+
}
153+
154+
/// Basic implementation of the `initialized` notification for tests
155+
async fn initialized(&mut self) -> Result<()> {
156+
self.notify("initialized", InitializedParams {}).await
157+
}
158+
159+
/// Basic implementation of the `shutdown` notification for tests
160+
async fn shutdown(&mut self) -> Result<()> {
161+
self.service
162+
.ready()
163+
.await
164+
.map_err(Error::msg)
165+
.context("ready() returned an error")?
166+
.call(Request::build("shutdown").finish())
167+
.await
168+
.map_err(Error::msg)
169+
.context("call() returned an error")
170+
.and_then(|res| {
171+
if let Some(res) = res {
172+
bail!("shutdown returned {:?}", res)
173+
} else {
174+
Ok(())
175+
}
176+
})
177+
}
178+
179+
async fn open_document(&mut self, text: impl Display) -> Result<()> {
180+
self.notify(
181+
"textDocument/didOpen",
182+
DidOpenTextDocumentParams {
183+
text_document: TextDocumentItem {
184+
uri: url!("document.js"),
185+
language_id: String::from("javascript"),
186+
version: 0,
187+
text: text.to_string(),
188+
},
189+
},
190+
)
191+
.await
192+
}
193+
194+
async fn open_untitled_document(&mut self, text: impl Display) -> Result<()> {
195+
self.notify(
196+
"textDocument/didOpen",
197+
DidOpenTextDocumentParams {
198+
text_document: TextDocumentItem {
199+
uri: url!("untitled-1"),
200+
language_id: String::from("javascript"),
201+
version: 0,
202+
text: text.to_string(),
203+
},
204+
},
205+
)
206+
.await
207+
}
208+
209+
/// Opens a document with given contents and given name. The name must contain the extension too
210+
async fn open_named_document(
211+
&mut self,
212+
text: impl Display,
213+
document_name: Url,
214+
language: impl Display,
215+
) -> Result<()> {
216+
self.notify(
217+
"textDocument/didOpen",
218+
DidOpenTextDocumentParams {
219+
text_document: TextDocumentItem {
220+
uri: document_name,
221+
language_id: language.to_string(),
222+
version: 0,
223+
text: text.to_string(),
224+
},
225+
},
226+
)
227+
.await
228+
}
229+
230+
/// When calling this function, remember to insert the file inside the memory file system
231+
async fn load_configuration(&mut self) -> Result<()> {
232+
self.notify(
233+
"workspace/didChangeConfiguration",
234+
DidChangeConfigurationParams {
235+
settings: to_value(()).unwrap(),
236+
},
237+
)
238+
.await
239+
}
240+
241+
async fn change_document(
242+
&mut self,
243+
version: i32,
244+
content_changes: Vec<TextDocumentContentChangeEvent>,
245+
) -> Result<()> {
246+
self.notify(
247+
"textDocument/didChange",
248+
DidChangeTextDocumentParams {
249+
text_document: VersionedTextDocumentIdentifier {
250+
uri: url!("document.js"),
251+
version,
252+
},
253+
content_changes,
254+
},
255+
)
256+
.await
257+
}
258+
259+
async fn close_document(&mut self) -> Result<()> {
260+
self.notify(
261+
"textDocument/didClose",
262+
DidCloseTextDocumentParams {
263+
text_document: TextDocumentIdentifier {
264+
uri: url!("document.js"),
265+
},
266+
},
267+
)
268+
.await
269+
}
270+
271+
/// Basic implementation of the `pglt/shutdown` request for tests
272+
async fn pglt_shutdown(&mut self) -> Result<()> {
273+
self.request::<_, ()>("pglt/shutdown", "_pglt_shutdown", ())
274+
.await?
275+
.context("pglt/shutdown returned None")?;
276+
Ok(())
277+
}
278+
}
279+
280+
/// Number of notifications buffered by the server-to-client channel before it starts blocking the current task
281+
const CHANNEL_BUFFER_SIZE: usize = 8;
282+
283+
#[derive(Debug, PartialEq, Eq)]
284+
enum ServerNotification {
285+
PublishDiagnostics(PublishDiagnosticsParams),
286+
}
287+
288+
/// Basic handler for requests and notifications coming from the server for tests
289+
async fn client_handler<I, O>(
290+
mut stream: I,
291+
mut sink: O,
292+
mut notify: Sender<ServerNotification>,
293+
) -> Result<()>
294+
where
295+
// This function has to be generic as `RequestStream` and `ResponseSink`
296+
// are not exported from `tower_lsp` and cannot be named in the signature
297+
I: Stream<Item = Request> + Unpin,
298+
O: Sink<Response> + Unpin,
299+
{
300+
while let Some(req) = stream.next().await {
301+
if req.method() == "textDocument/publishDiagnostics" {
302+
let params = req.params().expect("invalid request");
303+
let diagnostics = from_value(params.clone()).expect("invalid params");
304+
let notification = ServerNotification::PublishDiagnostics(diagnostics);
305+
match notify.send(notification).await {
306+
Ok(_) => continue,
307+
Err(_) => break,
308+
}
309+
}
310+
311+
let id = match req.id() {
312+
Some(id) => id,
313+
None => continue,
314+
};
315+
316+
let res = Response::from_error(id.clone(), jsonrpc::Error::method_not_found());
317+
318+
sink.send(res).await.ok();
319+
}
320+
321+
Ok(())
322+
}
323+
324+
#[tokio::test]
325+
async fn basic_lifecycle() -> Result<()> {
326+
let factory = ServerFactory::default();
327+
let (service, client) = factory.create(None).into_inner();
328+
let (stream, sink) = client.split();
329+
let mut server = Server::new(service);
330+
331+
let (sender, _) = channel(CHANNEL_BUFFER_SIZE);
332+
let reader = tokio::spawn(client_handler(stream, sink, sender));
333+
334+
server.initialize().await?;
335+
server.initialized().await?;
336+
337+
server.shutdown().await?;
338+
reader.abort();
339+
340+
Ok(())
341+
}

0 commit comments

Comments
 (0)