From 6cff076513924430c8cdf422fa56dbe711faee40 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 18 Aug 2020 10:38:57 +0200 Subject: Revive cache cleaning The idea here is that, on CI, we only want to cache crates.io dependencies, and not local crates. This keeps the size of the cache low, and also improves performance, as network and moving files on disk (on Windows) can be slow. --- crates/rust-analyzer/tests/heavy_tests/main.rs | 684 -------------------- crates/rust-analyzer/tests/heavy_tests/support.rs | 262 -------- crates/rust-analyzer/tests/heavy_tests/testdir.rs | 62 -- crates/rust-analyzer/tests/rust-analyzer/main.rs | 694 +++++++++++++++++++++ .../rust-analyzer/tests/rust-analyzer/support.rs | 262 ++++++++ .../rust-analyzer/tests/rust-analyzer/testdir.rs | 62 ++ xtask/src/lib.rs | 45 +- xtask/src/main.rs | 5 +- xtask/src/pre_cache.rs | 80 +++ 9 files changed, 1106 insertions(+), 1050 deletions(-) delete mode 100644 crates/rust-analyzer/tests/heavy_tests/main.rs delete mode 100644 crates/rust-analyzer/tests/heavy_tests/support.rs delete mode 100644 crates/rust-analyzer/tests/heavy_tests/testdir.rs create mode 100644 crates/rust-analyzer/tests/rust-analyzer/main.rs create mode 100644 crates/rust-analyzer/tests/rust-analyzer/support.rs create mode 100644 crates/rust-analyzer/tests/rust-analyzer/testdir.rs create mode 100644 xtask/src/pre_cache.rs diff --git a/crates/rust-analyzer/tests/heavy_tests/main.rs b/crates/rust-analyzer/tests/heavy_tests/main.rs deleted file mode 100644 index 7370505f8..000000000 --- a/crates/rust-analyzer/tests/heavy_tests/main.rs +++ /dev/null @@ -1,684 +0,0 @@ -mod testdir; -mod support; - -use std::{collections::HashMap, path::PathBuf, time::Instant}; - -use lsp_types::{ - notification::DidOpenTextDocument, - request::{CodeActionRequest, Completion, Formatting, GotoTypeDefinition, HoverRequest}, - CodeActionContext, CodeActionParams, CompletionParams, DidOpenTextDocumentParams, - DocumentFormattingParams, FormattingOptions, GotoDefinitionParams, HoverParams, - PartialResultParams, Position, Range, TextDocumentItem, TextDocumentPositionParams, - WorkDoneProgressParams, -}; -use rust_analyzer::lsp_ext::{OnEnter, Runnables, RunnablesParams}; -use serde_json::json; -use test_utils::skip_slow_tests; - -use crate::{ - support::{project, Project}, - testdir::TestDir, -}; - -const PROFILE: &str = ""; -// const PROFILE: &'static str = "*@3>100"; - -#[test] -fn completes_items_from_standard_library() { - if skip_slow_tests() { - return; - } - - let project_start = Instant::now(); - let server = Project::with_fixture( - r#" -//- /Cargo.toml -[package] -name = "foo" -version = "0.0.0" - -//- /src/lib.rs -use std::collections::Spam; -"#, - ) - .with_sysroot(true) - .server(); - server.wait_until_workspace_is_loaded(); - eprintln!("loading took {:?}", project_start.elapsed()); - let completion_start = Instant::now(); - let res = server.send_request::(CompletionParams { - text_document_position: TextDocumentPositionParams::new( - server.doc_id("src/lib.rs"), - Position::new(0, 23), - ), - context: None, - partial_result_params: PartialResultParams::default(), - work_done_progress_params: WorkDoneProgressParams::default(), - }); - assert!(res.to_string().contains("HashMap")); - eprintln!("completion took {:?}", completion_start.elapsed()); -} - -#[test] -fn test_runnables_project() { - if skip_slow_tests() { - return; - } - - let code = r#" -//- /foo/Cargo.toml -[package] -name = "foo" -version = "0.0.0" - -//- /foo/src/lib.rs -pub fn foo() {} - -//- /foo/tests/spam.rs -#[test] -fn test_eggs() {} - -//- /bar/Cargo.toml -[package] -name = "bar" -version = "0.0.0" - -//- /bar/src/main.rs -fn main() {} -"#; - - let server = Project::with_fixture(code).root("foo").root("bar").server(); - - server.wait_until_workspace_is_loaded(); - server.request::( - RunnablesParams { text_document: server.doc_id("foo/tests/spam.rs"), position: None }, - json!([ - { - "args": { - "cargoArgs": ["test", "--package", "foo", "--test", "spam"], - "executableArgs": ["test_eggs", "--exact", "--nocapture"], - "workspaceRoot": server.path().join("foo") - }, - "kind": "cargo", - "label": "test test_eggs", - "location": { - "targetRange": { - "end": { "character": 17, "line": 1 }, - "start": { "character": 0, "line": 0 } - }, - "targetSelectionRange": { - "end": { "character": 12, "line": 1 }, - "start": { "character": 3, "line": 1 } - }, - "targetUri": "file:///[..]/tests/spam.rs" - } - }, - { - "args": { - "cargoArgs": ["check", "--package", "foo", "--all-targets"], - "executableArgs": [], - "workspaceRoot": server.path().join("foo") - }, - "kind": "cargo", - "label": "cargo check -p foo --all-targets" - }, - { - "args": { - "cargoArgs": ["test", "--package", "foo", "--all-targets"], - "executableArgs": [], - "workspaceRoot": server.path().join("foo") - }, - "kind": "cargo", - "label": "cargo test -p foo --all-targets" - } - ]), - ); -} - -#[test] -fn test_format_document() { - if skip_slow_tests() { - return; - } - - let server = project( - r#" -//- /Cargo.toml -[package] -name = "foo" -version = "0.0.0" - -//- /src/lib.rs -mod bar; - -fn main() { -} - -pub use std::collections::HashMap; -"#, - ); - server.wait_until_workspace_is_loaded(); - - server.request::( - DocumentFormattingParams { - text_document: server.doc_id("src/lib.rs"), - options: FormattingOptions { - tab_size: 4, - insert_spaces: false, - insert_final_newline: None, - trim_final_newlines: None, - trim_trailing_whitespace: None, - properties: HashMap::new(), - }, - work_done_progress_params: WorkDoneProgressParams::default(), - }, - json!([ - { - "newText": r#"mod bar; - -fn main() {} - -pub use std::collections::HashMap; -"#, - "range": { - "end": { "character": 0, "line": 6 }, - "start": { "character": 0, "line": 0 } - } - } - ]), - ); -} - -#[test] -fn test_format_document_2018() { - if skip_slow_tests() { - return; - } - - let server = project( - r#" -//- /Cargo.toml -[package] -name = "foo" -version = "0.0.0" -edition = "2018" - -//- /src/lib.rs -mod bar; - -async fn test() { -} - -fn main() { -} - -pub use std::collections::HashMap; -"#, - ); - server.wait_until_workspace_is_loaded(); - - server.request::( - DocumentFormattingParams { - text_document: server.doc_id("src/lib.rs"), - options: FormattingOptions { - tab_size: 4, - insert_spaces: false, - properties: HashMap::new(), - insert_final_newline: None, - trim_final_newlines: None, - trim_trailing_whitespace: None, - }, - work_done_progress_params: WorkDoneProgressParams::default(), - }, - json!([ - { - "newText": r#"mod bar; - -async fn test() {} - -fn main() {} - -pub use std::collections::HashMap; -"#, - "range": { - "end": { "character": 0, "line": 9 }, - "start": { "character": 0, "line": 0 } - } - } - ]), - ); -} - -#[test] -fn test_missing_module_code_action() { - if skip_slow_tests() { - return; - } - - let server = project( - r#" -//- /Cargo.toml -[package] -name = "foo" -version = "0.0.0" - -//- /src/lib.rs -mod bar; - -fn main() {} -"#, - ); - server.wait_until_workspace_is_loaded(); - let empty_context = || CodeActionContext { diagnostics: Vec::new(), only: None }; - server.request::( - CodeActionParams { - text_document: server.doc_id("src/lib.rs"), - range: Range::new(Position::new(0, 4), Position::new(0, 7)), - context: empty_context(), - partial_result_params: PartialResultParams::default(), - work_done_progress_params: WorkDoneProgressParams::default(), - }, - json!([{ - "edit": { - "documentChanges": [ - { - "kind": "create", - "uri": "file:///[..]/src/bar.rs" - } - ] - }, - "isPreferred": false, - "kind": "quickfix", - "title": "Create module" - }]), - ); - - server.request::( - CodeActionParams { - text_document: server.doc_id("src/lib.rs"), - range: Range::new(Position::new(2, 4), Position::new(2, 7)), - context: empty_context(), - partial_result_params: PartialResultParams::default(), - work_done_progress_params: WorkDoneProgressParams::default(), - }, - json!([]), - ); -} - -#[test] -fn test_missing_module_code_action_in_json_project() { - if skip_slow_tests() { - return; - } - - let tmp_dir = TestDir::new(); - - let path = tmp_dir.path(); - - let project = json!({ - "roots": [path], - "crates": [ { - "root_module": path.join("src/lib.rs"), - "deps": [], - "edition": "2015", - "cfg": [ "cfg_atom_1", "feature=\"cfg_1\""], - } ] - }); - - let code = format!( - r#" -//- /rust-project.json -{PROJECT} - -//- /src/lib.rs -mod bar; - -fn main() {{}} -"#, - PROJECT = project.to_string(), - ); - - let server = Project::with_fixture(&code).tmp_dir(tmp_dir).server(); - - server.wait_until_workspace_is_loaded(); - let empty_context = || CodeActionContext { diagnostics: Vec::new(), only: None }; - server.request::( - CodeActionParams { - text_document: server.doc_id("src/lib.rs"), - range: Range::new(Position::new(0, 4), Position::new(0, 7)), - context: empty_context(), - partial_result_params: PartialResultParams::default(), - work_done_progress_params: WorkDoneProgressParams::default(), - }, - json!([{ - "edit": { - "documentChanges": [ - { - "kind": "create", - "uri": "file://[..]/src/bar.rs" - } - ] - }, - "isPreferred": false, - "kind": "quickfix", - "title": "Create module" - }]), - ); - - server.request::( - CodeActionParams { - text_document: server.doc_id("src/lib.rs"), - range: Range::new(Position::new(2, 4), Position::new(2, 7)), - context: empty_context(), - partial_result_params: PartialResultParams::default(), - work_done_progress_params: WorkDoneProgressParams::default(), - }, - json!([]), - ); -} - -#[test] -fn diagnostics_dont_block_typing() { - if skip_slow_tests() { - return; - } - - let librs: String = (0..10).map(|i| format!("mod m{};", i)).collect(); - let libs: String = (0..10).map(|i| format!("//- /src/m{}.rs\nfn foo() {{}}\n\n", i)).collect(); - let server = Project::with_fixture(&format!( - r#" -//- /Cargo.toml -[package] -name = "foo" -version = "0.0.0" - -//- /src/lib.rs -{} - -{} - -fn main() {{}} -"#, - librs, libs - )) - .with_sysroot(true) - .server(); - - server.wait_until_workspace_is_loaded(); - for i in 0..10 { - server.notification::(DidOpenTextDocumentParams { - text_document: TextDocumentItem { - uri: server.doc_id(&format!("src/m{}.rs", i)).uri, - language_id: "rust".to_string(), - version: 0, - text: "/// Docs\nfn foo() {}".to_string(), - }, - }); - } - let start = std::time::Instant::now(); - server.request::( - TextDocumentPositionParams { - text_document: server.doc_id("src/m0.rs"), - position: Position { line: 0, character: 5 }, - }, - json!([{ - "insertTextFormat": 2, - "newText": "\n/// $0", - "range": { - "end": { "character": 5, "line": 0 }, - "start": { "character": 5, "line": 0 } - } - }]), - ); - let elapsed = start.elapsed(); - assert!(elapsed.as_millis() < 2000, "typing enter took {:?}", elapsed); -} - -#[test] -fn preserves_dos_line_endings() { - if skip_slow_tests() { - return; - } - - let server = Project::with_fixture( - &" -//- /Cargo.toml -[package] -name = \"foo\" -version = \"0.0.0\" - -//- /src/main.rs -/// Some Docs\r\nfn main() {} -", - ) - .server(); - server.wait_until_workspace_is_loaded(); - - server.request::( - TextDocumentPositionParams { - text_document: server.doc_id("src/main.rs"), - position: Position { line: 0, character: 8 }, - }, - json!([{ - "insertTextFormat": 2, - "newText": "\r\n/// $0", - "range": { - "end": { "line": 0, "character": 8 }, - "start": { "line": 0, "character": 8 } - } - }]), - ); -} - -#[test] -fn out_dirs_check() { - if skip_slow_tests() { - return; - } - - let server = Project::with_fixture( - r###" -//- /Cargo.toml -[package] -name = "foo" -version = "0.0.0" - -//- /build.rs -use std::{env, fs, path::Path}; - -fn main() { - let out_dir = env::var_os("OUT_DIR").unwrap(); - let dest_path = Path::new(&out_dir).join("hello.rs"); - fs::write( - &dest_path, - r#"pub fn message() -> &'static str { "Hello, World!" }"#, - ) - .unwrap(); - println!("cargo:rustc-cfg=atom_cfg"); - println!("cargo:rustc-cfg=featlike=\"set\""); - println!("cargo:rerun-if-changed=build.rs"); -} -//- /src/main.rs -#[rustc_builtin_macro] macro_rules! include {} -#[rustc_builtin_macro] macro_rules! include_str {} -#[rustc_builtin_macro] macro_rules! concat {} -#[rustc_builtin_macro] macro_rules! env {} - -include!(concat!(env!("OUT_DIR"), "/hello.rs")); - -#[cfg(atom_cfg)] -struct A; -#[cfg(bad_atom_cfg)] -struct A; -#[cfg(featlike = "set")] -struct B; -#[cfg(featlike = "not_set")] -struct B; - -fn main() { - let va = A; - let vb = B; - let should_be_str = message(); - let another_str = include_str!("main.rs"); -} -"###, - ) - .with_config(|config| { - config.cargo.load_out_dirs_from_check = true; - }) - .server(); - server.wait_until_workspace_is_loaded(); - let res = server.send_request::(HoverParams { - text_document_position_params: TextDocumentPositionParams::new( - server.doc_id("src/main.rs"), - Position::new(19, 10), - ), - work_done_progress_params: Default::default(), - }); - assert!(res.to_string().contains("&str")); - let res = server.send_request::(HoverParams { - text_document_position_params: TextDocumentPositionParams::new( - server.doc_id("src/main.rs"), - Position::new(20, 10), - ), - work_done_progress_params: Default::default(), - }); - assert!(res.to_string().contains("&str")); - server.request::( - GotoDefinitionParams { - text_document_position_params: TextDocumentPositionParams::new( - server.doc_id("src/main.rs"), - Position::new(17, 9), - ), - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }, - json!([{ - "originSelectionRange": { - "end": { "character": 10, "line": 17 }, - "start": { "character": 8, "line": 17 } - }, - "targetRange": { - "end": { "character": 9, "line": 8 }, - "start": { "character": 0, "line": 7 } - }, - "targetSelectionRange": { - "end": { "character": 8, "line": 8 }, - "start": { "character": 7, "line": 8 } - }, - "targetUri": "file:///[..]src/main.rs" - }]), - ); - server.request::( - GotoDefinitionParams { - text_document_position_params: TextDocumentPositionParams::new( - server.doc_id("src/main.rs"), - Position::new(18, 9), - ), - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }, - json!([{ - "originSelectionRange": { - "end": { "character": 10, "line": 18 }, - "start": { "character": 8, "line": 18 } - }, - "targetRange": { - "end": { "character": 9, "line": 12 }, - "start": { "character": 0, "line":11 } - }, - "targetSelectionRange": { - "end": { "character": 8, "line": 12 }, - "start": { "character": 7, "line": 12 } - }, - "targetUri": "file:///[..]src/main.rs" - }]), - ); -} - -#[test] -fn resolve_proc_macro() { - if skip_slow_tests() { - return; - } - let server = Project::with_fixture( - r###" -//- /foo/Cargo.toml -[package] -name = "foo" -version = "0.0.0" -edition = "2018" -[dependencies] -bar = {path = "../bar"} - -//- /foo/src/main.rs -use bar::Bar; -trait Bar { - fn bar(); -} -#[derive(Bar)] -struct Foo {} -fn main() { - Foo::bar(); -} - -//- /bar/Cargo.toml -[package] -name = "bar" -version = "0.0.0" -edition = "2018" - -[lib] -proc-macro = true - -//- /bar/src/lib.rs -extern crate proc_macro; -use proc_macro::{Delimiter, Group, Ident, Span, TokenStream, TokenTree}; -macro_rules! t { - ($n:literal) => { - TokenTree::from(Ident::new($n, Span::call_site())) - }; - ({}) => { - TokenTree::from(Group::new(Delimiter::Brace, TokenStream::new())) - }; - (()) => { - TokenTree::from(Group::new(Delimiter::Parenthesis, TokenStream::new())) - }; -} -#[proc_macro_derive(Bar)] -pub fn foo(_input: TokenStream) -> TokenStream { - // We hard code the output here for preventing to use any deps - let mut res = TokenStream::new(); - - // impl Bar for Foo { fn bar() {} } - let mut tokens = vec![t!("impl"), t!("Bar"), t!("for"), t!("Foo")]; - let mut fn_stream = TokenStream::new(); - fn_stream.extend(vec![t!("fn"), t!("bar"), t!(()), t!({})]); - tokens.push(Group::new(Delimiter::Brace, fn_stream).into()); - res.extend(tokens); - res -} - -"###, - ) - .with_config(|config| { - let macro_srv_path = PathBuf::from(env!("CARGO_BIN_EXE_rust-analyzer")); - - config.cargo.load_out_dirs_from_check = true; - config.proc_macro_srv = Some((macro_srv_path, vec!["proc-macro".into()])); - }) - .root("foo") - .root("bar") - .server(); - server.wait_until_workspace_is_loaded(); - let res = server.send_request::(HoverParams { - text_document_position_params: TextDocumentPositionParams::new( - server.doc_id("foo/src/main.rs"), - Position::new(7, 9), - ), - work_done_progress_params: Default::default(), - }); - - let value = res.get("contents").unwrap().get("value").unwrap().to_string(); - assert_eq!(value, r#""```rust\nfoo::Bar\n```\n\n```rust\nfn bar()\n```""#) -} diff --git a/crates/rust-analyzer/tests/heavy_tests/support.rs b/crates/rust-analyzer/tests/heavy_tests/support.rs deleted file mode 100644 index 5bafeba79..000000000 --- a/crates/rust-analyzer/tests/heavy_tests/support.rs +++ /dev/null @@ -1,262 +0,0 @@ -use std::{ - cell::{Cell, RefCell}, - fs, - path::{Path, PathBuf}, - sync::Once, - time::Duration, -}; - -use crossbeam_channel::{after, select, Receiver}; -use lsp_server::{Connection, Message, Notification, Request}; -use lsp_types::{ - notification::Exit, request::Shutdown, TextDocumentIdentifier, Url, WorkDoneProgress, -}; -use lsp_types::{ProgressParams, ProgressParamsValue}; -use project_model::ProjectManifest; -use rust_analyzer::{ - config::{ClientCapsConfig, Config, FilesConfig, FilesWatcher, LinkedProject}, - main_loop, -}; -use serde::Serialize; -use serde_json::{to_string_pretty, Value}; -use test_utils::{find_mismatch, Fixture}; -use vfs::AbsPathBuf; - -use crate::testdir::TestDir; - -pub struct Project<'a> { - fixture: &'a str, - with_sysroot: bool, - tmp_dir: Option, - roots: Vec, - config: Option>, -} - -impl<'a> Project<'a> { - pub fn with_fixture(fixture: &str) -> Project { - Project { fixture, tmp_dir: None, roots: vec![], with_sysroot: false, config: None } - } - - pub fn tmp_dir(mut self, tmp_dir: TestDir) -> Project<'a> { - self.tmp_dir = Some(tmp_dir); - self - } - - pub(crate) fn root(mut self, path: &str) -> Project<'a> { - self.roots.push(path.into()); - self - } - - pub fn with_sysroot(mut self, sysroot: bool) -> Project<'a> { - self.with_sysroot = sysroot; - self - } - - pub fn with_config(mut self, config: impl Fn(&mut Config) + 'static) -> Project<'a> { - self.config = Some(Box::new(config)); - self - } - - pub fn server(self) -> Server { - let tmp_dir = self.tmp_dir.unwrap_or_else(|| TestDir::new()); - static INIT: Once = Once::new(); - INIT.call_once(|| { - env_logger::builder().is_test(true).try_init().unwrap(); - profile::init_from(crate::PROFILE); - }); - - for entry in Fixture::parse(self.fixture) { - let path = tmp_dir.path().join(&entry.path['/'.len_utf8()..]); - fs::create_dir_all(path.parent().unwrap()).unwrap(); - fs::write(path.as_path(), entry.text.as_bytes()).unwrap(); - } - - let tmp_dir_path = AbsPathBuf::assert(tmp_dir.path().to_path_buf()); - let mut roots = - self.roots.into_iter().map(|root| tmp_dir_path.join(root)).collect::>(); - if roots.is_empty() { - roots.push(tmp_dir_path.clone()); - } - let linked_projects = roots - .into_iter() - .map(|it| ProjectManifest::discover_single(&it).unwrap()) - .map(LinkedProject::from) - .collect::>(); - - let mut config = Config { - client_caps: ClientCapsConfig { - location_link: true, - code_action_literals: true, - work_done_progress: true, - ..Default::default() - }, - with_sysroot: self.with_sysroot, - linked_projects, - files: FilesConfig { watcher: FilesWatcher::Client, exclude: Vec::new() }, - ..Config::new(tmp_dir_path) - }; - if let Some(f) = &self.config { - f(&mut config) - } - - Server::new(tmp_dir, config) - } -} - -pub fn project(fixture: &str) -> Server { - Project::with_fixture(fixture).server() -} - -pub struct Server { - req_id: Cell, - messages: RefCell>, - _thread: jod_thread::JoinHandle<()>, - client: Connection, - /// XXX: remove the tempdir last - dir: TestDir, -} - -impl Server { - fn new(dir: TestDir, config: Config) -> Server { - let (connection, client) = Connection::memory(); - - let _thread = jod_thread::Builder::new() - .name("test server".to_string()) - .spawn(move || main_loop(config, connection).unwrap()) - .expect("failed to spawn a thread"); - - Server { req_id: Cell::new(1), dir, messages: Default::default(), client, _thread } - } - - 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 notification(&self, params: N::Params) - where - N: lsp_types::notification::Notification, - N::Params: Serialize, - { - let r = Notification::new(N::METHOD.to_string(), params); - self.send_notification(r) - } - - pub fn request(&self, params: R::Params, expected_resp: Value) - where - R: lsp_types::request::Request, - R::Params: Serialize, - { - let actual = self.send_request::(params); - if let Some((expected_part, actual_part)) = find_mismatch(&expected_resp, &actual) { - panic!( - "JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n", - to_string_pretty(&expected_resp).unwrap(), - to_string_pretty(&actual).unwrap(), - to_string_pretty(expected_part).unwrap(), - to_string_pretty(actual_part).unwrap(), - ); - } - } - - pub fn send_request(&self, params: R::Params) -> Value - where - R: lsp_types::request::Request, - R::Params: Serialize, - { - let id = self.req_id.get(); - self.req_id.set(id + 1); - - let r = Request::new(id.into(), R::METHOD.to_string(), params); - self.send_request_(r) - } - fn send_request_(&self, r: Request) -> Value { - let id = r.id.clone(); - self.client.sender.send(r.into()).unwrap(); - while let Some(msg) = self.recv() { - match msg { - Message::Request(req) => { - if req.method == "window/workDoneProgress/create" { - continue; - } - if req.method == "client/registerCapability" { - let params = req.params.to_string(); - if ["workspace/didChangeWatchedFiles", "textDocument/didSave"] - .iter() - .any(|&it| params.contains(it)) - { - continue; - } - } - panic!("unexpected request: {:?}", req) - } - Message::Notification(_) => (), - Message::Response(res) => { - assert_eq!(res.id, id); - if let Some(err) = res.error { - panic!("error response: {:#?}", err); - } - return res.result.unwrap(); - } - } - } - panic!("no response"); - } - pub fn wait_until_workspace_is_loaded(&self) { - self.wait_for_message_cond(1, &|msg: &Message| match msg { - Message::Notification(n) if n.method == "$/progress" => { - match n.clone().extract::("$/progress").unwrap() { - ProgressParams { - token: lsp_types::ProgressToken::String(ref token), - value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(_)), - } if token == "rustAnalyzer/roots scanned" => true, - _ => false, - } - } - _ => false, - }) - } - fn wait_for_message_cond(&self, n: usize, cond: &dyn Fn(&Message) -> bool) { - let mut total = 0; - for msg in self.messages.borrow().iter() { - if cond(msg) { - total += 1 - } - } - while total < n { - let msg = self.recv().expect("no response"); - if cond(&msg) { - total += 1; - } - } - } - fn recv(&self) -> Option { - recv_timeout(&self.client.receiver).map(|msg| { - self.messages.borrow_mut().push(msg.clone()); - msg - }) - } - fn send_notification(&self, not: Notification) { - self.client.sender.send(Message::Notification(not)).unwrap(); - } - - pub fn path(&self) -> &Path { - self.dir.path() - } -} - -impl Drop for Server { - fn drop(&mut self) { - self.request::((), Value::Null); - self.notification::(()); - } -} - -fn recv_timeout(receiver: &Receiver) -> Option { - let timeout = - if cfg!(target_os = "macos") { Duration::from_secs(300) } else { Duration::from_secs(120) }; - select! { - recv(receiver) -> msg => msg.ok(), - recv(after(timeout)) -> _ => panic!("timed out"), - } -} diff --git a/crates/rust-analyzer/tests/heavy_tests/testdir.rs b/crates/rust-analyzer/tests/heavy_tests/testdir.rs deleted file mode 100644 index 7487e7429..000000000 --- a/crates/rust-analyzer/tests/heavy_tests/testdir.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::{ - fs, io, - path::{Path, PathBuf}, - sync::atomic::{AtomicUsize, Ordering}, -}; - -pub struct TestDir { - path: PathBuf, - keep: bool, -} - -impl TestDir { - pub fn new() -> TestDir { - let base = std::env::temp_dir().join("testdir"); - let pid = std::process::id(); - - static CNT: AtomicUsize = AtomicUsize::new(0); - for _ in 0..100 { - let cnt = CNT.fetch_add(1, Ordering::Relaxed); - let path = base.join(format!("{}_{}", pid, cnt)); - if path.is_dir() { - continue; - } - fs::create_dir_all(&path).unwrap(); - return TestDir { path, keep: false }; - } - panic!("Failed to create a temporary directory") - } - #[allow(unused)] - pub fn keep(mut self) -> TestDir { - self.keep = true; - self - } - pub fn path(&self) -> &Path { - &self.path - } -} - -impl Drop for TestDir { - fn drop(&mut self) { - if self.keep { - return; - } - remove_dir_all(&self.path).unwrap() - } -} - -#[cfg(not(windows))] -fn remove_dir_all(path: &Path) -> io::Result<()> { - fs::remove_dir_all(path) -} - -#[cfg(windows)] -fn remove_dir_all(path: &Path) -> io::Result<()> { - for _ in 0..99 { - if fs::remove_dir_all(path).is_ok() { - return Ok(()); - } - std::thread::sleep(std::time::Duration::from_millis(10)) - } - fs::remove_dir_all(path) -} diff --git a/crates/rust-analyzer/tests/rust-analyzer/main.rs b/crates/rust-analyzer/tests/rust-analyzer/main.rs new file mode 100644 index 000000000..fa315ff8e --- /dev/null +++ b/crates/rust-analyzer/tests/rust-analyzer/main.rs @@ -0,0 +1,694 @@ +//! The most high-level integrated tests for rust-analyzer. +//! +//! This tests run a full LSP event loop, spawn cargo and process stdlib from +//! sysroot. For this reason, the tests here are very slow, and should be +//! avoided unless absolutely necessary. +//! +//! In particular, it's fine *not* to test that client & server agree on +//! specific JSON shapes here -- there's little value in such tests, as we can't +//! be sure without a real client anyway. + +mod testdir; +mod support; + +use std::{collections::HashMap, path::PathBuf, time::Instant}; + +use lsp_types::{ + notification::DidOpenTextDocument, + request::{CodeActionRequest, Completion, Formatting, GotoTypeDefinition, HoverRequest}, + CodeActionContext, CodeActionParams, CompletionParams, DidOpenTextDocumentParams, + DocumentFormattingParams, FormattingOptions, GotoDefinitionParams, HoverParams, + PartialResultParams, Position, Range, TextDocumentItem, TextDocumentPositionParams, + WorkDoneProgressParams, +}; +use rust_analyzer::lsp_ext::{OnEnter, Runnables, RunnablesParams}; +use serde_json::json; +use test_utils::skip_slow_tests; + +use crate::{ + support::{project, Project}, + testdir::TestDir, +}; + +const PROFILE: &str = ""; +// const PROFILE: &'static str = "*@3>100"; + +#[test] +fn completes_items_from_standard_library() { + if skip_slow_tests() { + return; + } + + let project_start = Instant::now(); + let server = Project::with_fixture( + r#" +//- /Cargo.toml +[package] +name = "foo" +version = "0.0.0" + +//- /src/lib.rs +use std::collections::Spam; +"#, + ) + .with_sysroot(true) + .server(); + server.wait_until_workspace_is_loaded(); + eprintln!("loading took {:?}", project_start.elapsed()); + let completion_start = Instant::now(); + let res = server.send_request::(CompletionParams { + text_document_position: TextDocumentPositionParams::new( + server.doc_id("src/lib.rs"), + Position::new(0, 23), + ), + context: None, + partial_result_params: PartialResultParams::default(), + work_done_progress_params: WorkDoneProgressParams::default(), + }); + assert!(res.to_string().contains("HashMap")); + eprintln!("completion took {:?}", completion_start.elapsed()); +} + +#[test] +fn test_runnables_project() { + if skip_slow_tests() { + return; + } + + let code = r#" +//- /foo/Cargo.toml +[package] +name = "foo" +version = "0.0.0" + +//- /foo/src/lib.rs +pub fn foo() {} + +//- /foo/tests/spam.rs +#[test] +fn test_eggs() {} + +//- /bar/Cargo.toml +[package] +name = "bar" +version = "0.0.0" + +//- /bar/src/main.rs +fn main() {} +"#; + + let server = Project::with_fixture(code).root("foo").root("bar").server(); + + server.wait_until_workspace_is_loaded(); + server.request::( + RunnablesParams { text_document: server.doc_id("foo/tests/spam.rs"), position: None }, + json!([ + { + "args": { + "cargoArgs": ["test", "--package", "foo", "--test", "spam"], + "executableArgs": ["test_eggs", "--exact", "--nocapture"], + "workspaceRoot": server.path().join("foo") + }, + "kind": "cargo", + "label": "test test_eggs", + "location": { + "targetRange": { + "end": { "character": 17, "line": 1 }, + "start": { "character": 0, "line": 0 } + }, + "targetSelectionRange": { + "end": { "character": 12, "line": 1 }, + "start": { "character": 3, "line": 1 } + }, + "targetUri": "file:///[..]/tests/spam.rs" + } + }, + { + "args": { + "cargoArgs": ["check", "--package", "foo", "--all-targets"], + "executableArgs": [], + "workspaceRoot": server.path().join("foo") + }, + "kind": "cargo", + "label": "cargo check -p foo --all-targets" + }, + { + "args": { + "cargoArgs": ["test", "--package", "foo", "--all-targets"], + "executableArgs": [], + "workspaceRoot": server.path().join("foo") + }, + "kind": "cargo", + "label": "cargo test -p foo --all-targets" + } + ]), + ); +} + +#[test] +fn test_format_document() { + if skip_slow_tests() { + return; + } + + let server = project( + r#" +//- /Cargo.toml +[package] +name = "foo" +version = "0.0.0" + +//- /src/lib.rs +mod bar; + +fn main() { +} + +pub use std::collections::HashMap; +"#, + ); + server.wait_until_workspace_is_loaded(); + + server.request::( + DocumentFormattingParams { + text_document: server.doc_id("src/lib.rs"), + options: FormattingOptions { + tab_size: 4, + insert_spaces: false, + insert_final_newline: None, + trim_final_newlines: None, + trim_trailing_whitespace: None, + properties: HashMap::new(), + }, + work_done_progress_params: WorkDoneProgressParams::default(), + }, + json!([ + { + "newText": r#"mod bar; + +fn main() {} + +pub use std::collections::HashMap; +"#, + "range": { + "end": { "character": 0, "line": 6 }, + "start": { "character": 0, "line": 0 } + } + } + ]), + ); +} + +#[test] +fn test_format_document_2018() { + if skip_slow_tests() { + return; + } + + let server = project( + r#" +//- /Cargo.toml +[package] +name = "foo" +version = "0.0.0" +edition = "2018" + +//- /src/lib.rs +mod bar; + +async fn test() { +} + +fn main() { +} + +pub use std::collections::HashMap; +"#, + ); + server.wait_until_workspace_is_loaded(); + + server.request::( + DocumentFormattingParams { + text_document: server.doc_id("src/lib.rs"), + options: FormattingOptions { + tab_size: 4, + insert_spaces: false, + properties: HashMap::new(), + insert_final_newline: None, + trim_final_newlines: None, + trim_trailing_whitespace: None, + }, + work_done_progress_params: WorkDoneProgressParams::default(), + }, + json!([ + { + "newText": r#"mod bar; + +async fn test() {} + +fn main() {} + +pub use std::collections::HashMap; +"#, + "range": { + "end": { "character": 0, "line": 9 }, + "start": { "character": 0, "line": 0 } + } + } + ]), + ); +} + +#[test] +fn test_missing_module_code_action() { + if skip_slow_tests() { + return; + } + + let server = project( + r#" +//- /Cargo.toml +[package] +name = "foo" +version = "0.0.0" + +//- /src/lib.rs +mod bar; + +fn main() {} +"#, + ); + server.wait_until_workspace_is_loaded(); + let empty_context = || CodeActionContext { diagnostics: Vec::new(), only: None }; + server.request::( + CodeActionParams { + text_document: server.doc_id("src/lib.rs"), + range: Range::new(Position::new(0, 4), Position::new(0, 7)), + context: empty_context(), + partial_result_params: PartialResultParams::default(), + work_done_progress_params: WorkDoneProgressParams::default(), + }, + json!([{ + "edit": { + "documentChanges": [ + { + "kind": "create", + "uri": "file:///[..]/src/bar.rs" + } + ] + }, + "isPreferred": false, + "kind": "quickfix", + "title": "Create module" + }]), + ); + + server.request::( + CodeActionParams { + text_document: server.doc_id("src/lib.rs"), + range: Range::new(Position::new(2, 4), Position::new(2, 7)), + context: empty_context(), + partial_result_params: PartialResultParams::default(), + work_done_progress_params: WorkDoneProgressParams::default(), + }, + json!([]), + ); +} + +#[test] +fn test_missing_module_code_action_in_json_project() { + if skip_slow_tests() { + return; + } + + let tmp_dir = TestDir::new(); + + let path = tmp_dir.path(); + + let project = json!({ + "roots": [path], + "crates": [ { + "root_module": path.join("src/lib.rs"), + "deps": [], + "edition": "2015", + "cfg": [ "cfg_atom_1", "feature=\"cfg_1\""], + } ] + }); + + let code = format!( + r#" +//- /rust-project.json +{PROJECT} + +//- /src/lib.rs +mod bar; + +fn main() {{}} +"#, + PROJECT = project.to_string(), + ); + + let server = Project::with_fixture(&code).tmp_dir(tmp_dir).server(); + + server.wait_until_workspace_is_loaded(); + let empty_context = || CodeActionContext { diagnostics: Vec::new(), only: None }; + server.request::( + CodeActionParams { + text_document: server.doc_id("src/lib.rs"), + range: Range::new(Position::new(0, 4), Position::new(0, 7)), + context: empty_context(), + partial_result_params: PartialResultParams::default(), + work_done_progress_params: WorkDoneProgressParams::default(), + }, + json!([{ + "edit": { + "documentChanges": [ + { + "kind": "create", + "uri": "file://[..]/src/bar.rs" + } + ] + }, + "isPreferred": false, + "kind": "quickfix", + "title": "Create module" + }]), + ); + + server.request::( + CodeActionParams { + text_document: server.doc_id("src/lib.rs"), + range: Range::new(Position::new(2, 4), Position::new(2, 7)), + context: empty_context(), + partial_result_params: PartialResultParams::default(), + work_done_progress_params: WorkDoneProgressParams::default(), + }, + json!([]), + ); +} + +#[test] +fn diagnostics_dont_block_typing() { + if skip_slow_tests() { + return; + } + + let librs: String = (0..10).map(|i| format!("mod m{};", i)).collect(); + let libs: String = (0..10).map(|i| format!("//- /src/m{}.rs\nfn foo() {{}}\n\n", i)).collect(); + let server = Project::with_fixture(&format!( + r#" +//- /Cargo.toml +[package] +name = "foo" +version = "0.0.0" + +//- /src/lib.rs +{} + +{} + +fn main() {{}} +"#, + librs, libs + )) + .with_sysroot(true) + .server(); + + server.wait_until_workspace_is_loaded(); + for i in 0..10 { + server.notification::(DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: server.doc_id(&format!("src/m{}.rs", i)).uri, + language_id: "rust".to_string(), + version: 0, + text: "/// Docs\nfn foo() {}".to_string(), + }, + }); + } + let start = std::time::Instant::now(); + server.request::( + TextDocumentPositionParams { + text_document: server.doc_id("src/m0.rs"), + position: Position { line: 0, character: 5 }, + }, + json!([{ + "insertTextFormat": 2, + "newText": "\n/// $0", + "range": { + "end": { "character": 5, "line": 0 }, + "start": { "character": 5, "line": 0 } + } + }]), + ); + let elapsed = start.elapsed(); + assert!(elapsed.as_millis() < 2000, "typing enter took {:?}", elapsed); +} + +#[test] +fn preserves_dos_line_endings() { + if skip_slow_tests() { + return; + } + + let server = Project::with_fixture( + &" +//- /Cargo.toml +[package] +name = \"foo\" +version = \"0.0.0\" + +//- /src/main.rs +/// Some Docs\r\nfn main() {} +", + ) + .server(); + server.wait_until_workspace_is_loaded(); + + server.request::( + TextDocumentPositionParams { + text_document: server.doc_id("src/main.rs"), + position: Position { line: 0, character: 8 }, + }, + json!([{ + "insertTextFormat": 2, + "newText": "\r\n/// $0", + "range": { + "end": { "line": 0, "character": 8 }, + "start": { "line": 0, "character": 8 } + } + }]), + ); +} + +#[test] +fn out_dirs_check() { + if skip_slow_tests() { + return; + } + + let server = Project::with_fixture( + r###" +//- /Cargo.toml +[package] +name = "foo" +version = "0.0.0" + +//- /build.rs +use std::{env, fs, path::Path}; + +fn main() { + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("hello.rs"); + fs::write( + &dest_path, + r#"pub fn message() -> &'static str { "Hello, World!" }"#, + ) + .unwrap(); + println!("cargo:rustc-cfg=atom_cfg"); + println!("cargo:rustc-cfg=featlike=\"set\""); + println!("cargo:rerun-if-changed=build.rs"); +} +//- /src/main.rs +#[rustc_builtin_macro] macro_rules! include {} +#[rustc_builtin_macro] macro_rules! include_str {} +#[rustc_builtin_macro] macro_rules! concat {} +#[rustc_builtin_macro] macro_rules! env {} + +include!(concat!(env!("OUT_DIR"), "/hello.rs")); + +#[cfg(atom_cfg)] +struct A; +#[cfg(bad_atom_cfg)] +struct A; +#[cfg(featlike = "set")] +struct B; +#[cfg(featlike = "not_set")] +struct B; + +fn main() { + let va = A; + let vb = B; + let should_be_str = message(); + let another_str = include_str!("main.rs"); +} +"###, + ) + .with_config(|config| { + config.cargo.load_out_dirs_from_check = true; + }) + .server(); + server.wait_until_workspace_is_loaded(); + let res = server.send_request::(HoverParams { + text_document_position_params: TextDocumentPositionParams::new( + server.doc_id("src/main.rs"), + Position::new(19, 10), + ), + work_done_progress_params: Default::default(), + }); + assert!(res.to_string().contains("&str")); + let res = server.send_request::(HoverParams { + text_document_position_params: TextDocumentPositionParams::new( + server.doc_id("src/main.rs"), + Position::new(20, 10), + ), + work_done_progress_params: Default::default(), + }); + assert!(res.to_string().contains("&str")); + server.request::( + GotoDefinitionParams { + text_document_position_params: TextDocumentPositionParams::new( + server.doc_id("src/main.rs"), + Position::new(17, 9), + ), + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + }, + json!([{ + "originSelectionRange": { + "end": { "character": 10, "line": 17 }, + "start": { "character": 8, "line": 17 } + }, + "targetRange": { + "end": { "character": 9, "line": 8 }, + "start": { "character": 0, "line": 7 } + }, + "targetSelectionRange": { + "end": { "character": 8, "line": 8 }, + "start": { "character": 7, "line": 8 } + }, + "targetUri": "file:///[..]src/main.rs" + }]), + ); + server.request::( + GotoDefinitionParams { + text_document_position_params: TextDocumentPositionParams::new( + server.doc_id("src/main.rs"), + Position::new(18, 9), + ), + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + }, + json!([{ + "originSelectionRange": { + "end": { "character": 10, "line": 18 }, + "start": { "character": 8, "line": 18 } + }, + "targetRange": { + "end": { "character": 9, "line": 12 }, + "start": { "character": 0, "line":11 } + }, + "targetSelectionRange": { + "end": { "character": 8, "line": 12 }, + "start": { "character": 7, "line": 12 } + }, + "targetUri": "file:///[..]src/main.rs" + }]), + ); +} + +#[test] +fn resolve_proc_macro() { + if skip_slow_tests() { + return; + } + let server = Project::with_fixture( + r###" +//- /foo/Cargo.toml +[package] +name = "foo" +version = "0.0.0" +edition = "2018" +[dependencies] +bar = {path = "../bar"} + +//- /foo/src/main.rs +use bar::Bar; +trait Bar { + fn bar(); +} +#[derive(Bar)] +struct Foo {} +fn main() { + Foo::bar(); +} + +//- /bar/Cargo.toml +[package] +name = "bar" +version = "0.0.0" +edition = "2018" + +[lib] +proc-macro = true + +//- /bar/src/lib.rs +extern crate proc_macro; +use proc_macro::{Delimiter, Group, Ident, Span, TokenStream, TokenTree}; +macro_rules! t { + ($n:literal) => { + TokenTree::from(Ident::new($n, Span::call_site())) + }; + ({}) => { + TokenTree::from(Group::new(Delimiter::Brace, TokenStream::new())) + }; + (()) => { + TokenTree::from(Group::new(Delimiter::Parenthesis, TokenStream::new())) + }; +} +#[proc_macro_derive(Bar)] +pub fn foo(_input: TokenStream) -> TokenStream { + // We hard code the output here for preventing to use any deps + let mut res = TokenStream::new(); + + // impl Bar for Foo { fn bar() {} } + let mut tokens = vec![t!("impl"), t!("Bar"), t!("for"), t!("Foo")]; + let mut fn_stream = TokenStream::new(); + fn_stream.extend(vec![t!("fn"), t!("bar"), t!(()), t!({})]); + tokens.push(Group::new(Delimiter::Brace, fn_stream).into()); + res.extend(tokens); + res +} + +"###, + ) + .with_config(|config| { + let macro_srv_path = PathBuf::from(env!("CARGO_BIN_EXE_rust-analyzer")); + + config.cargo.load_out_dirs_from_check = true; + config.proc_macro_srv = Some((macro_srv_path, vec!["proc-macro".into()])); + }) + .root("foo") + .root("bar") + .server(); + server.wait_until_workspace_is_loaded(); + let res = server.send_request::(HoverParams { + text_document_position_params: TextDocumentPositionParams::new( + server.doc_id("foo/src/main.rs"), + Position::new(7, 9), + ), + work_done_progress_params: Default::default(), + }); + + let value = res.get("contents").unwrap().get("value").unwrap().to_string(); + assert_eq!(value, r#""```rust\nfoo::Bar\n```\n\n```rust\nfn bar()\n```""#) +} diff --git a/crates/rust-analyzer/tests/rust-analyzer/support.rs b/crates/rust-analyzer/tests/rust-analyzer/support.rs new file mode 100644 index 000000000..5bafeba79 --- /dev/null +++ b/crates/rust-analyzer/tests/rust-analyzer/support.rs @@ -0,0 +1,262 @@ +use std::{ + cell::{Cell, RefCell}, + fs, + path::{Path, PathBuf}, + sync::Once, + time::Duration, +}; + +use crossbeam_channel::{after, select, Receiver}; +use lsp_server::{Connection, Message, Notification, Request}; +use lsp_types::{ + notification::Exit, request::Shutdown, TextDocumentIdentifier, Url, WorkDoneProgress, +}; +use lsp_types::{ProgressParams, ProgressParamsValue}; +use project_model::ProjectManifest; +use rust_analyzer::{ + config::{ClientCapsConfig, Config, FilesConfig, FilesWatcher, LinkedProject}, + main_loop, +}; +use serde::Serialize; +use serde_json::{to_string_pretty, Value}; +use test_utils::{find_mismatch, Fixture}; +use vfs::AbsPathBuf; + +use crate::testdir::TestDir; + +pub struct Project<'a> { + fixture: &'a str, + with_sysroot: bool, + tmp_dir: Option, + roots: Vec, + config: Option>, +} + +impl<'a> Project<'a> { + pub fn with_fixture(fixture: &str) -> Project { + Project { fixture, tmp_dir: None, roots: vec![], with_sysroot: false, config: None } + } + + pub fn tmp_dir(mut self, tmp_dir: TestDir) -> Project<'a> { + self.tmp_dir = Some(tmp_dir); + self + } + + pub(crate) fn root(mut self, path: &str) -> Project<'a> { + self.roots.push(path.into()); + self + } + + pub fn with_sysroot(mut self, sysroot: bool) -> Project<'a> { + self.with_sysroot = sysroot; + self + } + + pub fn with_config(mut self, config: impl Fn(&mut Config) + 'static) -> Project<'a> { + self.config = Some(Box::new(config)); + self + } + + pub fn server(self) -> Server { + let tmp_dir = self.tmp_dir.unwrap_or_else(|| TestDir::new()); + static INIT: Once = Once::new(); + INIT.call_once(|| { + env_logger::builder().is_test(true).try_init().unwrap(); + profile::init_from(crate::PROFILE); + }); + + for entry in Fixture::parse(self.fixture) { + let path = tmp_dir.path().join(&entry.path['/'.len_utf8()..]); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path.as_path(), entry.text.as_bytes()).unwrap(); + } + + let tmp_dir_path = AbsPathBuf::assert(tmp_dir.path().to_path_buf()); + let mut roots = + self.roots.into_iter().map(|root| tmp_dir_path.join(root)).collect::>(); + if roots.is_empty() { + roots.push(tmp_dir_path.clone()); + } + let linked_projects = roots + .into_iter() + .map(|it| ProjectManifest::discover_single(&it).unwrap()) + .map(LinkedProject::from) + .collect::>(); + + let mut config = Config { + client_caps: ClientCapsConfig { + location_link: true, + code_action_literals: true, + work_done_progress: true, + ..Default::default() + }, + with_sysroot: self.with_sysroot, + linked_projects, + files: FilesConfig { watcher: FilesWatcher::Client, exclude: Vec::new() }, + ..Config::new(tmp_dir_path) + }; + if let Some(f) = &self.config { + f(&mut config) + } + + Server::new(tmp_dir, config) + } +} + +pub fn project(fixture: &str) -> Server { + Project::with_fixture(fixture).server() +} + +pub struct Server { + req_id: Cell, + messages: RefCell>, + _thread: jod_thread::JoinHandle<()>, + client: Connection, + /// XXX: remove the tempdir last + dir: TestDir, +} + +impl Server { + fn new(dir: TestDir, config: Config) -> Server { + let (connection, client) = Connection::memory(); + + let _thread = jod_thread::Builder::new() + .name("test server".to_string()) + .spawn(move || main_loop(config, connection).unwrap()) + .expect("failed to spawn a thread"); + + Server { req_id: Cell::new(1), dir, messages: Default::default(), client, _thread } + } + + 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 notification(&self, params: N::Params) + where + N: lsp_types::notification::Notification, + N::Params: Serialize, + { + let r = Notification::new(N::METHOD.to_string(), params); + self.send_notification(r) + } + + pub fn request(&self, params: R::Params, expected_resp: Value) + where + R: lsp_types::request::Request, + R::Params: Serialize, + { + let actual = self.send_request::(params); + if let Some((expected_part, actual_part)) = find_mismatch(&expected_resp, &actual) { + panic!( + "JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n", + to_string_pretty(&expected_resp).unwrap(), + to_string_pretty(&actual).unwrap(), + to_string_pretty(expected_part).unwrap(), + to_string_pretty(actual_part).unwrap(), + ); + } + } + + pub fn send_request(&self, params: R::Params) -> Value + where + R: lsp_types::request::Request, + R::Params: Serialize, + { + let id = self.req_id.get(); + self.req_id.set(id + 1); + + let r = Request::new(id.into(), R::METHOD.to_string(), params); + self.send_request_(r) + } + fn send_request_(&self, r: Request) -> Value { + let id = r.id.clone(); + self.client.sender.send(r.into()).unwrap(); + while let Some(msg) = self.recv() { + match msg { + Message::Request(req) => { + if req.method == "window/workDoneProgress/create" { + continue; + } + if req.method == "client/registerCapability" { + let params = req.params.to_string(); + if ["workspace/didChangeWatchedFiles", "textDocument/didSave"] + .iter() + .any(|&it| params.contains(it)) + { + continue; + } + } + panic!("unexpected request: {:?}", req) + } + Message::Notification(_) => (), + Message::Response(res) => { + assert_eq!(res.id, id); + if let Some(err) = res.error { + panic!("error response: {:#?}", err); + } + return res.result.unwrap(); + } + } + } + panic!("no response"); + } + pub fn wait_until_workspace_is_loaded(&self) { + self.wait_for_message_cond(1, &|msg: &Message| match msg { + Message::Notification(n) if n.method == "$/progress" => { + match n.clone().extract::("$/progress").unwrap() { + ProgressParams { + token: lsp_types::ProgressToken::String(ref token), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(_)), + } if token == "rustAnalyzer/roots scanned" => true, + _ => false, + } + } + _ => false, + }) + } + fn wait_for_message_cond(&self, n: usize, cond: &dyn Fn(&Message) -> bool) { + let mut total = 0; + for msg in self.messages.borrow().iter() { + if cond(msg) { + total += 1 + } + } + while total < n { + let msg = self.recv().expect("no response"); + if cond(&msg) { + total += 1; + } + } + } + fn recv(&self) -> Option { + recv_timeout(&self.client.receiver).map(|msg| { + self.messages.borrow_mut().push(msg.clone()); + msg + }) + } + fn send_notification(&self, not: Notification) { + self.client.sender.send(Message::Notification(not)).unwrap(); + } + + pub fn path(&self) -> &Path { + self.dir.path() + } +} + +impl Drop for Server { + fn drop(&mut self) { + self.request::((), Value::Null); + self.notification::(()); + } +} + +fn recv_timeout(receiver: &Receiver) -> Option { + let timeout = + if cfg!(target_os = "macos") { Duration::from_secs(300) } else { Duration::from_secs(120) }; + select! { + recv(receiver) -> msg => msg.ok(), + recv(after(timeout)) -> _ => panic!("timed out"), + } +} diff --git a/crates/rust-analyzer/tests/rust-analyzer/testdir.rs b/crates/rust-analyzer/tests/rust-analyzer/testdir.rs new file mode 100644 index 000000000..7487e7429 --- /dev/null +++ b/crates/rust-analyzer/tests/rust-analyzer/testdir.rs @@ -0,0 +1,62 @@ +use std::{ + fs, io, + path::{Path, PathBuf}, + sync::atomic::{AtomicUsize, Ordering}, +}; + +pub struct TestDir { + path: PathBuf, + keep: bool, +} + +impl TestDir { + pub fn new() -> TestDir { + let base = std::env::temp_dir().join("testdir"); + let pid = std::process::id(); + + static CNT: AtomicUsize = AtomicUsize::new(0); + for _ in 0..100 { + let cnt = CNT.fetch_add(1, Ordering::Relaxed); + let path = base.join(format!("{}_{}", pid, cnt)); + if path.is_dir() { + continue; + } + fs::create_dir_all(&path).unwrap(); + return TestDir { path, keep: false }; + } + panic!("Failed to create a temporary directory") + } + #[allow(unused)] + pub fn keep(mut self) -> TestDir { + self.keep = true; + self + } + pub fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for TestDir { + fn drop(&mut self) { + if self.keep { + return; + } + remove_dir_all(&self.path).unwrap() + } +} + +#[cfg(not(windows))] +fn remove_dir_all(path: &Path) -> io::Result<()> { + fs::remove_dir_all(path) +} + +#[cfg(windows)] +fn remove_dir_all(path: &Path) -> io::Result<()> { + for _ in 0..99 { + if fs::remove_dir_all(path).is_ok() { + return Ok(()); + } + std::thread::sleep(std::time::Duration::from_millis(10)) + } + fs::remove_dir_all(path) +} diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs index f3ad81ba7..e790d995f 100644 --- a/xtask/src/lib.rs +++ b/xtask/src/lib.rs @@ -3,14 +3,15 @@ //! See https://github.com/matklad/cargo-xtask/ pub mod not_bash; +pub mod codegen; +mod ast_src; + pub mod install; pub mod release; pub mod dist; pub mod pre_commit; pub mod metrics; - -pub mod codegen; -mod ast_src; +pub mod pre_cache; use std::{ env, @@ -21,7 +22,7 @@ use walkdir::{DirEntry, WalkDir}; use crate::{ codegen::Mode, - not_bash::{fs2, pushd, pushenv, rm_rf}, + not_bash::{pushd, pushenv}, }; pub use anyhow::{bail, Context as _, Result}; @@ -108,42 +109,6 @@ pub fn run_fuzzer() -> Result<()> { Ok(()) } -/// Cleans the `./target` dir after the build such that only -/// dependencies are cached on CI. -pub fn run_pre_cache() -> Result<()> { - let slow_tests_cookie = Path::new("./target/.slow_tests_cookie"); - if !slow_tests_cookie.exists() { - panic!("slow tests were skipped on CI!") - } - rm_rf(slow_tests_cookie)?; - - for entry in Path::new("./target/debug").read_dir()? { - let entry = entry?; - if entry.file_type().map(|it| it.is_file()).ok() == Some(true) { - // Can't delete yourself on windows :-( - if !entry.path().ends_with("xtask.exe") { - rm_rf(&entry.path())? - } - } - } - - fs2::remove_file("./target/.rustc_info.json")?; - let to_delete = ["hir", "heavy_test", "xtask", "ide", "rust-analyzer"]; - for &dir in ["./target/debug/deps", "target/debug/.fingerprint"].iter() { - for entry in Path::new(dir).read_dir()? { - let entry = entry?; - if to_delete.iter().any(|&it| entry.path().display().to_string().contains(it)) { - // Can't delete yourself on windows :-( - if !entry.path().ends_with("xtask.exe") { - rm_rf(&entry.path())? - } - } - } - } - - Ok(()) -} - fn is_release_tag(tag: &str) -> bool { tag.len() == "2020-02-24".len() && tag.starts_with(|c: char| c.is_ascii_digit()) } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index b69b884e5..fb38fdc92 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -17,9 +17,10 @@ use xtask::{ install::{ClientOpt, InstallCmd, Malloc, ServerOpt}, metrics::MetricsCmd, not_bash::pushd, + pre_cache::PreCacheCmd, pre_commit, project_root, release::{PromoteCmd, ReleaseCmd}, - run_clippy, run_fuzzer, run_pre_cache, run_rustfmt, Result, + run_clippy, run_fuzzer, run_rustfmt, Result, }; fn main() -> Result<()> { @@ -100,7 +101,7 @@ FLAGS: } "pre-cache" => { args.finish()?; - run_pre_cache() + PreCacheCmd.run() } "release" => { let dry_run = args.contains("--dry-run"); diff --git a/xtask/src/pre_cache.rs b/xtask/src/pre_cache.rs new file mode 100644 index 000000000..47ba6ba24 --- /dev/null +++ b/xtask/src/pre_cache.rs @@ -0,0 +1,80 @@ +use std::{ + fs::FileType, + path::{Path, PathBuf}, +}; + +use anyhow::Result; + +use crate::not_bash::{fs2, rm_rf}; + +pub struct PreCacheCmd; + +impl PreCacheCmd { + /// Cleans the `./target` dir after the build such that only + /// dependencies are cached on CI. + pub fn run(self) -> Result<()> { + let slow_tests_cookie = Path::new("./target/.slow_tests_cookie"); + if !slow_tests_cookie.exists() { + panic!("slow tests were skipped on CI!") + } + rm_rf(slow_tests_cookie)?; + + for path in read_dir("./target/debug", FileType::is_file)? { + // Can't delete yourself on windows :-( + if !path.ends_with("xtask.exe") { + rm_rf(&path)? + } + } + + fs2::remove_file("./target/.rustc_info.json")?; + + let to_delete = read_dir("./crates", FileType::is_dir)? + .into_iter() + .map(|path| path.file_name().unwrap().to_string_lossy().replace('-', "_")) + .collect::>(); + + for &dir in ["./target/debug/deps", "target/debug/.fingerprint"].iter() { + for path in read_dir(dir, |_file_type| true)? { + if path.ends_with("xtask.exe") { + continue; + } + let file_name = path.file_name().unwrap().to_string_lossy(); + let (stem, _) = match rsplit_once(&file_name, '-') { + Some(it) => it, + None => { + rm_rf(path)?; + continue; + } + }; + let stem = stem.replace('-', "_"); + if to_delete.contains(&stem) { + rm_rf(path)?; + } + } + } + + Ok(()) + } +} +fn read_dir(path: impl AsRef, cond: impl Fn(&FileType) -> bool) -> Result> { + read_dir_impl(path.as_ref(), &cond) +} + +fn read_dir_impl(path: &Path, cond: &dyn Fn(&FileType) -> bool) -> Result> { + let mut res = Vec::new(); + for entry in path.read_dir()? { + let entry = entry?; + let file_type = entry.file_type()?; + if cond(&file_type) { + res.push(entry.path()) + } + } + Ok(res) +} + +fn rsplit_once(haystack: &str, delim: char) -> Option<(&str, &str)> { + let mut split = haystack.rsplitn(2, delim); + let suffix = split.next()?; + let prefix = split.next()?; + Some((prefix, suffix)) +} -- cgit v1.2.3