9 use crossbeam_channel::{after, select, Receiver};
10 use lsp_server::{Connection, Message, Notification, Request};
12 notification::{DidOpenTextDocument, Exit},
14 DidOpenTextDocumentParams, TextDocumentIdentifier, TextDocumentItem, Url, WorkDoneProgress,
17 use serde_json::{to_string_pretty, Value};
18 use tempfile::TempDir;
19 use test_utils::{find_mismatch, parse_fixture};
21 use req::{ProgressParams, ProgressParamsValue};
23 config::{ClientCapsConfig, Config},
27 pub struct Project<'a> {
30 tmp_dir: Option<TempDir>,
32 config: Option<Box<dyn Fn(&mut Config)>>,
35 impl<'a> Project<'a> {
36 pub fn with_fixture(fixture: &str) -> Project {
37 Project { fixture, tmp_dir: None, roots: vec![], with_sysroot: false, config: None }
40 pub fn tmp_dir(mut self, tmp_dir: TempDir) -> Project<'a> {
41 self.tmp_dir = Some(tmp_dir);
45 pub fn root(mut self, path: &str) -> Project<'a> {
46 self.roots.push(path.into());
50 pub fn with_sysroot(mut self, sysroot: bool) -> Project<'a> {
51 self.with_sysroot = sysroot;
55 pub fn with_config(mut self, config: impl Fn(&mut Config) + 'static) -> Project<'a> {
56 self.config = Some(Box::new(config));
60 pub fn server(self) -> Server {
61 let tmp_dir = self.tmp_dir.unwrap_or_else(|| TempDir::new().unwrap());
62 static INIT: Once = Once::new();
64 env_logger::builder().is_test(true).try_init().unwrap();
65 ra_prof::init_from(crate::PROFILE);
68 let mut paths = vec![];
70 for entry in parse_fixture(self.fixture) {
71 let path = tmp_dir.path().join(entry.meta);
72 fs::create_dir_all(path.parent().unwrap()).unwrap();
73 fs::write(path.as_path(), entry.text.as_bytes()).unwrap();
74 paths.push((path, entry.text));
77 let roots = self.roots.into_iter().map(|root| tmp_dir.path().join(root)).collect();
79 let mut config = Config {
80 client_caps: ClientCapsConfig {
82 code_action_literals: true,
85 with_sysroot: self.with_sysroot,
89 if let Some(f) = &self.config {
93 Server::new(tmp_dir, config, roots, paths)
97 pub fn project(fixture: &str) -> Server {
98 Project::with_fixture(fixture).server()
103 messages: RefCell<Vec<Message>>,
104 _thread: jod_thread::JoinHandle<()>,
106 /// XXX: remove the tempdir last
115 files: Vec<(PathBuf, String)>,
117 let path = dir.path().to_path_buf();
119 let roots = if roots.is_empty() { vec![path] } else { roots };
120 let (connection, client) = Connection::memory();
122 let _thread = jod_thread::Builder::new()
123 .name("test server".to_string())
124 .spawn(move || main_loop(roots, config, connection).unwrap())
125 .expect("failed to spawn a thread");
128 Server { req_id: Cell::new(1), dir, messages: Default::default(), client, _thread };
130 for (path, text) in files {
131 res.notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
132 text_document: TextDocumentItem {
133 uri: Url::from_file_path(path).unwrap(),
134 language_id: "rust".to_string(),
143 pub fn doc_id(&self, rel_path: &str) -> TextDocumentIdentifier {
144 let path = self.dir.path().join(rel_path);
145 TextDocumentIdentifier { uri: Url::from_file_path(path).unwrap() }
148 pub fn notification<N>(&self, params: N::Params)
150 N: lsp_types::notification::Notification,
151 N::Params: Serialize,
153 let r = Notification::new(N::METHOD.to_string(), params);
154 self.send_notification(r)
157 pub fn request<R>(&self, params: R::Params, expected_resp: Value)
159 R: lsp_types::request::Request,
160 R::Params: Serialize,
162 let actual = self.send_request::<R>(params);
163 if let Some((expected_part, actual_part)) = find_mismatch(&expected_resp, &actual) {
165 "JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n",
166 to_string_pretty(&expected_resp).unwrap(),
167 to_string_pretty(&actual).unwrap(),
168 to_string_pretty(expected_part).unwrap(),
169 to_string_pretty(actual_part).unwrap(),
174 pub fn send_request<R>(&self, params: R::Params) -> Value
176 R: lsp_types::request::Request,
177 R::Params: Serialize,
179 let id = self.req_id.get();
180 self.req_id.set(id + 1);
182 let r = Request::new(id.into(), R::METHOD.to_string(), params);
183 self.send_request_(r)
185 fn send_request_(&self, r: Request) -> Value {
186 let id = r.id.clone();
187 self.client.sender.send(r.into()).unwrap();
188 while let Some(msg) = self.recv() {
190 Message::Request(req) if req.method == "window/workDoneProgress/create" => (),
191 Message::Request(req) => panic!("unexpected request: {:?}", req),
192 Message::Notification(_) => (),
193 Message::Response(res) => {
194 assert_eq!(res.id, id);
195 if let Some(err) = res.error {
196 panic!("error response: {:#?}", err);
198 return res.result.unwrap();
202 panic!("no response");
204 pub fn wait_until_workspace_is_loaded(&self) {
205 self.wait_for_message_cond(1, &|msg: &Message| match msg {
206 Message::Notification(n) if n.method == "$/progress" => {
207 match n.clone().extract::<ProgressParams>("$/progress").unwrap() {
209 token: req::ProgressToken::String(ref token),
210 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(_)),
211 } if token == "rustAnalyzer/startup" => true,
218 fn wait_for_message_cond(&self, n: usize, cond: &dyn Fn(&Message) -> bool) {
220 for msg in self.messages.borrow().iter() {
226 let msg = self.recv().expect("no response");
232 fn recv(&self) -> Option<Message> {
233 recv_timeout(&self.client.receiver).map(|msg| {
234 self.messages.borrow_mut().push(msg.clone());
238 fn send_notification(&self, not: Notification) {
239 self.client.sender.send(Message::Notification(not)).unwrap();
242 pub fn path(&self) -> &Path {
247 impl Drop for Server {
249 self.request::<Shutdown>((), Value::Null);
250 self.notification::<Exit>(());
254 fn recv_timeout(receiver: &Receiver<Message>) -> Option<Message> {
255 let timeout = Duration::from_secs(120);
257 recv(receiver) -> msg => msg.ok(),
258 recv(after(timeout)) -> _ => panic!("timed out"),