diff --git a/tests/support/mod.rs b/tests/support/mod.rs new file mode 100644 index 00000000000..7a027f9f5ee --- /dev/null +++ b/tests/support/mod.rs @@ -0,0 +1,352 @@ +// Copyright 2016 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use serde_json; +use std::env; +use std::fs; +use std::io::{self, BufRead, BufReader, Read, Write}; +use std::mem; +use std::path::{Path, PathBuf}; +use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; +use std::str; +use std::sync::{Arc, Condvar, Mutex}; +use std::thread; +use std::time::Duration; + + +use support::paths::TestPathExt; + +pub mod paths; + +/// Executes `func` and panics if it takes longer than `dur`. +pub fn timeout(dur: Duration, func: F) + where F: FnOnce() + Send + 'static { + let pair = Arc::new((Mutex::new(false), Condvar::new())); + let pair2 = pair.clone(); + + thread::spawn(move|| { + let &(ref lock, ref cvar) = &*pair2; + func(); + let mut finished = lock.lock().unwrap(); + *finished = true; + // We notify the condvar that the value has changed. + cvar.notify_one(); + }); + + // Wait for the test to finish. + let &(ref lock, ref cvar) = &*pair; + let mut finished = lock.lock().unwrap(); + // As long as the value inside the `Mutex` is false, we wait. + while !*finished { + let result = cvar.wait_timeout(finished, dur).unwrap(); + if result.1.timed_out() { + panic!("Timed out") + } + finished = result.0 + } +} + +#[derive(Clone, Debug)] +pub struct ExpectedMessage { + id: Option, + contains: Vec, +} + +impl ExpectedMessage { + pub fn new(id: Option) -> ExpectedMessage { + ExpectedMessage { + id: id, + contains: vec![], + } + } + + pub fn expect_contains(&mut self, s: &str) -> &mut ExpectedMessage { + self.contains.push(s.to_owned()); + self + } +} + +pub fn read_message(reader: &mut BufReader) -> io::Result { + let mut content_length = None; + // Read the headers + loop { + let mut header = String::new(); + reader.read_line(&mut header)?; + if header.len() == 0 { + panic!("eof") + } + if header == "\r\n" { + // This is the end of the headers + break; + } + let parts: Vec<&str> = header.splitn(2, ": ").collect(); + if parts[0] == "Content-Length" { + content_length = Some(parts[1].trim().parse::().unwrap()) + } + } + + // Read the actual message + let content_length = content_length.expect("did not receive Content-Length header"); + let mut msg = vec![0; content_length]; + reader.read_exact(&mut msg)?; + let result = String::from_utf8_lossy(&msg).into_owned(); + Ok(result) +} + +pub fn expect_messages(reader: &mut BufReader, expected: &[&ExpectedMessage]) { + let mut results: Vec = Vec::new(); + while results.len() < expected.len() { + let msg = read_message(reader).unwrap(); + results.push(msg); + } + + println!( + "expect_messages:\n results: {:#?},\n expected: {:#?}", + results, + expected + ); + assert_eq!(results.len(), expected.len()); + for (found, expected) in results.iter().zip(expected.iter()) { + let values: serde_json::Value = serde_json::from_str(found).unwrap(); + assert!( + values + .get("jsonrpc") + .expect("Missing jsonrpc field") + .as_str() + .unwrap() == "2.0", + "Bad jsonrpc field" + ); + if let Some(id) = expected.id { + assert_eq!( + values + .get("id") + .expect("Missing id field") + .as_u64() + .unwrap(), + id, + "Unexpected id" + ); + } + for c in expected.contains.iter() { + found + .find(c) + .expect(&format!("Could not find `{}` in `{}`", c, found)); + } + } +} + +pub struct RlsHandle { + child: Child, + stdin: ChildStdin, + stdout: BufReader, +} + +impl RlsHandle { + pub fn new(mut child: Child) -> RlsHandle { + let stdin = mem::replace(&mut child.stdin, None).unwrap(); + let stdout = mem::replace(&mut child.stdout, None).unwrap(); + let stdout = BufReader::new(stdout); + + RlsHandle { + child, + stdin, + stdout, + } + } + + pub fn send_string(&mut self, s: &str) -> io::Result { + let full_msg = format!("Content-Length: {}\r\n\r\n{}", s.len(), s); + self.stdin.write(full_msg.as_bytes()) + } + pub fn send(&mut self, j: serde_json::Value) -> io::Result { + self.send_string(&j.to_string()) + } + pub fn notify(&mut self, method: &str, params: serde_json::Value) -> io::Result { + self.send(json!({ + "jsonrpc": "2.0", + "method": method, + "params": params, + })) + } + pub fn request(&mut self, id: u64, method: &str, params: serde_json::Value) -> io::Result { + self.send(json!({ + "jsonrpc": "2.0", + "id": id, + "method": method, + "params": params, + })) + } + pub fn shutdown_exit(&mut self) { + self.request(99999, "shutdown", json!({})).unwrap(); + + self.expect_messages(&[ + &ExpectedMessage::new(Some(99999)), + ]); + + self.notify("exit", json!({})).unwrap(); + + let ecode = self.child.wait() + .expect("failed to wait on child rls process"); + + assert!(ecode.success()); + } + + pub fn expect_messages(&mut self, expected: &[&ExpectedMessage]) { + expect_messages(&mut self.stdout, expected); + } +} + +#[derive(PartialEq,Clone)] +struct FileBuilder { + path: PathBuf, + body: String +} + +impl FileBuilder { + pub fn new(path: PathBuf, body: &str) -> FileBuilder { + FileBuilder { path: path, body: body.to_string() } + } + + fn mk(&self) { + self.dirname().mkdir_p(); + + let mut file = fs::File::create(&self.path).unwrap_or_else(|e| { + panic!("could not create file {}: {}", self.path.display(), e) + }); + + file.write_all(self.body.as_bytes()).unwrap(); + } + + fn dirname(&self) -> &Path { + self.path.parent().unwrap() + } +} + +#[derive(PartialEq,Clone)] +pub struct Project{ + root: PathBuf, +} + +#[must_use] +#[derive(PartialEq,Clone)] +pub struct ProjectBuilder { + name: String, + root: Project, + files: Vec, +} + +impl ProjectBuilder { + pub fn new(name: &str, root: PathBuf) -> ProjectBuilder { + ProjectBuilder { + name: name.to_string(), + root: Project{ root }, + files: vec![], + } + } + + pub fn file>(mut self, path: B, + body: &str) -> Self { + self._file(path.as_ref(), body); + self + } + + fn _file(&mut self, path: &Path, body: &str) { + self.files.push(FileBuilder::new(self.root.root.join(path), body)); + } + + pub fn build(self) -> Project { + // First, clean the directory if it already exists + self.rm_root(); + + // Create the empty directory + self.root.root.mkdir_p(); + + for file in self.files.iter() { + file.mk(); + } + + self.root + } + + fn rm_root(&self) { + self.root.root.rm_rf() + } +} + +impl Project { + pub fn root(&self) -> PathBuf { + self.root.clone() + } + + pub fn rls(&self) -> Command { + let mut cmd = Command::new(rls_exe()); + cmd.stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .current_dir(self.root()); + cmd + } +} + +// Generates a project layout +pub fn project(name: &str) -> ProjectBuilder { + ProjectBuilder::new(name, paths::root().join(name)) +} + +// Path to cargo executables +pub fn target_conf_dir() -> PathBuf { + let mut path = env::current_exe().unwrap(); + path.pop(); + if path.ends_with("deps") { + path.pop(); + } + path +} + +pub fn rls_exe() -> PathBuf { + target_conf_dir().join(format!("rls{}", env::consts::EXE_SUFFIX)) +} + +#[allow(dead_code)] +pub fn main_file(println: &str, deps: &[&str]) -> String { + let mut buf = String::new(); + + for dep in deps.iter() { + buf.push_str(&format!("extern crate {};\n", dep)); + } + + buf.push_str("fn main() { println!("); + buf.push_str(&println); + buf.push_str("); }\n"); + + buf.to_string() +} + +pub fn basic_bin_manifest(name: &str) -> String { + format!(r#" + [package] + name = "{}" + version = "0.5.0" + authors = ["wycats@example.com"] + [[bin]] + name = "{}" + "#, name, name) +} + +#[allow(dead_code)] +pub fn basic_lib_manifest(name: &str) -> String { + format!(r#" + [package] + name = "{}" + version = "0.5.0" + authors = ["wycats@example.com"] + [lib] + name = "{}" + "#, name, name) +} diff --git a/tests/support/paths.rs b/tests/support/paths.rs new file mode 100644 index 00000000000..4b834521f88 --- /dev/null +++ b/tests/support/paths.rs @@ -0,0 +1,117 @@ +// Copyright 2016 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::env; +use std::cell::Cell; +use std::fs; +use std::io::{self, ErrorKind}; +use std::path::{Path, PathBuf}; +use std::sync::{Once, ONCE_INIT}; +use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering}; + +static RLS_INTEGRATION_TEST_DIR : &'static str = "rlsit"; +static NEXT_ID: AtomicUsize = ATOMIC_USIZE_INIT; + +thread_local!(static TASK_ID: usize = NEXT_ID.fetch_add(1, Ordering::SeqCst)); + +fn init() { + static GLOBAL_INIT: Once = ONCE_INIT; + thread_local!(static LOCAL_INIT: Cell = Cell::new(false)); + GLOBAL_INIT.call_once(|| { + global_root().mkdir_p(); + }); + LOCAL_INIT.with(|i| { + if i.get() { + return + } + i.set(true); + root().rm_rf(); + }) +} + +fn global_root() -> PathBuf { + let mut path = env::current_exe().unwrap(); + path.pop(); // chop off exe name + path.pop(); // chop off 'debug' + + // If `cargo test` is run manually then our path looks like + // `target/debug/foo`, in which case our `path` is already pointing at + // `target`. If, however, `cargo test --target $target` is used then the + // output is `target/$target/debug/foo`, so our path is pointing at + // `target/$target`. Here we conditionally pop the `$target` name. + if path.file_name().and_then(|s| s.to_str()) != Some("target") { + path.pop(); + } + + path.join(RLS_INTEGRATION_TEST_DIR) +} + +pub fn root() -> PathBuf { + init(); + global_root().join(&TASK_ID.with(|my_id| format!("t{}", my_id))) +} + + +pub trait TestPathExt { + fn rm_rf(&self); + fn mkdir_p(&self); +} + +impl TestPathExt for Path { + /* Technically there is a potential race condition, but we don't + * care all that much for our tests + */ + fn rm_rf(&self) { + if !self.exists() { + return + } + + for file in fs::read_dir(self).unwrap() { + let file = file.unwrap().path(); + + if file.is_dir() { + file.rm_rf(); + } else { + // On windows we can't remove a readonly file, and git will + // often clone files as readonly. As a result, we have some + // special logic to remove readonly files on windows. + do_op(&file, "remove file", |p| fs::remove_file(p)); + } + } + do_op(self, "remove dir", |p| fs::remove_dir(p)); + } + + fn mkdir_p(&self) { + fs::create_dir_all(self).unwrap_or_else(|e| { + panic!("failed to mkdir_p {}: {}", self.display(), e) + }) + } + +} + +fn do_op(path: &Path, desc: &str, mut f: F) + where F: FnMut(&Path) -> io::Result<()> +{ + match f(path) { + Ok(()) => {} + Err(ref e) if cfg!(windows) && + e.kind() == ErrorKind::PermissionDenied => { + let mut p = path.metadata().unwrap().permissions(); + p.set_readonly(false); + fs::set_permissions(path, p).unwrap(); + f(path).unwrap_or_else(|e| { + panic!("failed to {} {}: {}", desc, path.display(), e); + }) + } + Err(e) => { + panic!("failed to {} {}: {}", desc, path.display(), e); + } + } +} \ No newline at end of file diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 00000000000..3ffe1e27775 --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,139 @@ + +// Copyright 2016 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +extern crate cargo; +#[macro_use] +extern crate serde_json; + +use std::time::Duration; + +mod support; +use support::{ExpectedMessage, RlsHandle, basic_bin_manifest, project, timeout}; + +const TIME_LIMIT_SECS: u64 = 300; + +#[test] +fn test_infer_bin() { + timeout(Duration::from_secs(TIME_LIMIT_SECS), ||{ + let p = project("simple_workspace") + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/main.rs", r#" + struct UnusedBin; + fn main() { + println!("Hello world!"); + } + "#) + .build(); + + let root_path = p.root(); + let rls_child = p.rls().spawn().unwrap(); + let mut rls = RlsHandle::new(rls_child); + + rls.request(0, "initialize", json!({ + "rootPath": root_path, + "capabilities": {} + })).unwrap(); + + rls.expect_messages(&[ + ExpectedMessage::new(Some(0)).expect_contains("capabilities"), + ExpectedMessage::new(None).expect_contains("beginBuild"), + ExpectedMessage::new(None).expect_contains("diagnosticsBegin"), + ExpectedMessage::new(None).expect_contains("struct is never used: `UnusedBin`"), + ExpectedMessage::new(None).expect_contains("diagnosticsEnd") + ]); + + rls.shutdown_exit(); + }); +} + +#[test] +fn test_simple_workspace() { + timeout(Duration::from_secs(300), ||{ + let p = project("simple_workspace") + .file("Cargo.toml", r#" + [workspace] + members = [ + "member_lib", + "member_bin", + ] + "#) + .file("Cargo.lock", r#" + [root] + name = "member_lib" + version = "0.1.0" + + [[package]] + name = "member_bin" + version = "0.1.0" + dependencies = [ + "member_lib 0.1.0", + ] + "#) + .file("member_bin/Cargo.toml", r#" + [package] + name = "member_bin" + version = "0.1.0" + authors = ["Igor Matuszewski "] + + [dependencies] + member_lib = { path = "../member_lib" } + "#) + .file("member_bin/src/main.rs", r#" + extern crate member_lib; + + fn main() { + let a = member_lib::MemberLibStruct; + } + "#) + .file("member_lib/Cargo.toml", r#" + [package] + name = "member_lib" + version = "0.1.0" + authors = ["Igor Matuszewski "] + + [dependencies] + "#) + .file("member_lib/src/lib.rs", r#" + pub struct MemberLibStruct; + + struct Unused; + + #[cfg(test)] + mod tests { + #[test] + fn it_works() { + } + } + "#) + .build(); + + let root_path = p.root(); + let rls_child = p.rls().spawn().unwrap(); + let mut rls = RlsHandle::new(rls_child); + + rls.request(0, "initialize", json!({ + "rootPath": root_path, + "capabilities": {} + })).unwrap(); + + // This is the expected behavior is workspace_mode is on by default + rls.expect_messages(&[ + ExpectedMessage::new(Some(0)).expect_contains("capabilities"), + ExpectedMessage::new(None).expect_contains("beginBuild"), + ExpectedMessage::new(None).expect_contains("diagnosticsBegin"), + ExpectedMessage::new(None).expect_contains("publishDiagnostics"), + ExpectedMessage::new(None).expect_contains("publishDiagnostics"), + ExpectedMessage::new(None).expect_contains("diagnosticsEnd") + ]); + + rls.shutdown_exit(); + }); +}