blob: 291b6faea1bf0a7a5f2b2acee8206f2fafaa481a [file] [log] [blame] [edit]
#![allow(clippy::expect_fun_call)]
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use rls::config::{Config, Inferrable};
use rls::server as ls_server;
use rls_analysis::{AnalysisHost, Target};
use rls_vfs::Vfs;
use walkdir::WalkDir;
use super::fixtures_dir;
pub(crate) struct Environment {
pub(crate) config: Option<Config>,
pub(crate) cache: Cache,
pub(crate) target_path: PathBuf,
}
impl Environment {
pub(crate) fn generate_from_fixture(fixture_dir: impl AsRef<Path>) -> Self {
let _ = env_logger::try_init();
if env::var("RUSTC").is_err() {
env::set_var("RUSTC", "rustc");
}
let fixture_dir = fixtures_dir().join(fixture_dir.as_ref());
let scratchpad_dir = build_scratchpad_from_fixture(fixture_dir)
.expect("Can't copy fixture files to scratchpad");
let target_dir = scratchpad_dir.join("target");
let mut config = Config::default();
config.target_dir = Inferrable::Specified(Some(target_dir.clone()));
config.unstable_features = true;
let cache = Cache::new(scratchpad_dir);
Self { config: Some(config), cache, target_path: target_dir }
}
}
impl Environment {
pub(crate) fn with_config<F>(&mut self, f: F)
where
F: FnOnce(&mut Config),
{
let config = self.config.as_mut().unwrap();
f(config);
}
// Initialize and run the internals of an LS protocol RLS server.
pub(crate) fn mock_server(
&mut self,
messages: Vec<String>,
) -> (ls_server::LsService<RecordOutput>, LsResultList, Arc<Mutex<Config>>) {
let analysis = Arc::new(AnalysisHost::new(Target::Debug));
let vfs = Arc::new(Vfs::new());
let config = Arc::new(Mutex::new(self.config.take().unwrap()));
let reader = Box::new(MockMsgReader::new(messages));
let output = RecordOutput::new();
let results = output.output.clone();
(
ls_server::LsService::new(analysis, vfs, Arc::clone(&config), reader, output),
results,
config,
)
}
}
impl Drop for Environment {
fn drop(&mut self) {
use std::fs;
if fs::metadata(&self.target_path).is_ok() {
fs::remove_dir_all(&self.target_path).expect("failed to tidy up");
}
}
}
pub fn build_scratchpad_from_fixture(fixture_dir: impl AsRef<Path>) -> io::Result<PathBuf> {
let fixture_dir = fixture_dir.as_ref();
let dirname = fixture_dir
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "No filename"))?;
// FIXME: For now persist the path; ideally we should clean up after every test
let genroot = tempfile::tempdir()?.into_path().join(dirname);
// Recursively copy read-only fixture files to freshly generated scratchpad
for entry in WalkDir::new(fixture_dir).into_iter() {
let entry = entry?;
let src = entry.path();
let relative = src.strip_prefix(fixture_dir).unwrap();
let dst = genroot.join(relative);
if std::fs::metadata(src)?.is_dir() {
std::fs::create_dir(dst)?;
} else {
std::fs::copy(src, dst)?;
}
}
Ok(genroot)
}
struct MockMsgReader {
messages: Vec<String>,
cur: AtomicUsize,
}
impl MockMsgReader {
fn new(messages: Vec<String>) -> MockMsgReader {
MockMsgReader { messages, cur: AtomicUsize::new(0) }
}
}
impl ls_server::MessageReader for MockMsgReader {
fn read_message(&self) -> Option<String> {
// Note that we hold this lock until the end of the function, thus meaning
// that we must finish processing one message before processing the next.
let index = self.cur.fetch_add(1, Ordering::SeqCst);
if index >= self.messages.len() {
return None;
}
let message = &self.messages[index];
Some(message.to_owned())
}
}
type LsResultList = Arc<Mutex<Vec<String>>>;
#[derive(Clone)]
pub(crate) struct RecordOutput {
pub(crate) output: LsResultList,
output_id: Arc<Mutex<u64>>,
}
impl RecordOutput {
pub(crate) fn new() -> RecordOutput {
RecordOutput {
output: Arc::new(Mutex::new(vec![])),
// use some distinguishable value
output_id: Arc::new(Mutex::new(0x0100_0000)),
}
}
}
impl ls_server::Output for RecordOutput {
fn response(&self, output: String) {
let mut records = self.output.lock().unwrap();
records.push(output);
}
fn provide_id(&self) -> ls_server::RequestId {
let mut id = self.output_id.lock().unwrap();
*id += 1;
ls_server::RequestId::Num(*id)
}
}
#[derive(Clone, Debug)]
pub(crate) struct ExpectedMessage {
id: Option<u64>,
contains: Vec<String>,
}
impl ExpectedMessage {
pub(crate) fn new(id: Option<u64>) -> ExpectedMessage {
ExpectedMessage { id, contains: vec![] }
}
pub(crate) fn expect_contains(&mut self, s: &str) -> &mut ExpectedMessage {
self.contains.push(s.to_owned());
self
}
}
/// This function checks for messages with a series of constraints (expecrations)
/// to appear in the buffer, removing valid messages and returning when encountering
/// some that didn't meet the expectation
pub(crate) fn expect_series(
server: &mut ls_server::LsService<RecordOutput>,
results: LsResultList,
contains: Vec<&str>,
) {
let mut expected = ExpectedMessage::new(None);
for c in contains {
expected.expect_contains(c);
}
while try_expect_message(server, results.clone(), &expected).is_ok() {}
}
/// Expect a single message
///
/// It panics if the message wasn't valid and removes it from the buffer
/// if it was
pub(crate) fn expect_message(
server: &mut ls_server::LsService<RecordOutput>,
results: LsResultList,
expected: &ExpectedMessage,
) {
if let Err(e) = try_expect_message(server, results, expected) {
panic!("Assert failed: {}", e);
}
}
/// Check a single message without panicking
///
/// A valid message is removed from the buffer while invalid messages
/// are left in place
fn try_expect_message(
server: &mut ls_server::LsService<RecordOutput>,
results: LsResultList,
expected: &ExpectedMessage,
) -> Result<(), String> {
server.wait_for_concurrent_jobs();
let mut results = results.lock().unwrap();
let found = match results.get(0) {
Some(s) => s,
None => return Err("No message found!".into()),
};
let values: serde_json::Value = serde_json::from_str(&found).unwrap();
if values.get("jsonrpc").expect("Missing jsonrpc field").as_str().unwrap() != "2.0" {
return Err("Bad jsonrpc field".into());
}
if let Some(id) = expected.id {
if values.get("id").expect("Missing id field").as_u64().unwrap() != id {
return Err("Unexpected id".into());
}
}
for c in &expected.contains {
if found.find(c).is_none() {
return Err(format!("Could not find `{}` in `{}`", c, found));
}
}
results.remove(0);
Ok(())
}
pub(crate) fn compare_json(actual: &serde_json::Value, expected: &str) {
let expected: serde_json::Value = serde_json::from_str(expected).unwrap();
if actual != &expected {
panic!(
"JSON differs\nExpected:\n{}\nActual:\n{}\n",
serde_json::to_string_pretty(&expected).unwrap(),
serde_json::to_string_pretty(actual).unwrap(),
);
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Src<'a> {
pub(crate) file_name: &'a Path,
// 1 indexed
pub(crate) line: usize,
pub(crate) name: &'a str,
}
pub(crate) fn src<'a>(file_name: &'a Path, line: usize, name: &'a str) -> Src<'a> {
Src { file_name, line, name }
}
pub(crate) struct Cache {
base_path: PathBuf,
files: HashMap<PathBuf, Vec<String>>,
}
impl Cache {
fn new(base_path: PathBuf) -> Cache {
Cache { base_path, files: HashMap::new() }
}
pub(crate) fn mk_ls_position(&mut self, src: Src<'_>) -> lsp_types::Position {
let line = self.get_line(src);
let col = line.find(src.name).expect(&format!("Line does not contain name {}", src.name));
lsp_types::Position::new((src.line - 1) as u64, char_of_byte_index(&line, col) as u64)
}
/// Create a range covering the initial position on the line
///
/// The line number uses a 0-based index.
pub(crate) fn mk_ls_range_from_line(&mut self, line: u64) -> lsp_types::Range {
lsp_types::Range::new(lsp_types::Position::new(line, 0), lsp_types::Position::new(line, 0))
}
pub(crate) fn abs_path(&self, file_name: &Path) -> PathBuf {
let result =
self.base_path.join(file_name).canonicalize().expect("Couldn't canonicalise path");
if cfg!(windows) {
// FIXME: If the \\?\ prefix is not stripped from the canonical path, the HTTP server tests fail. Why?
let result_string = result.to_str().expect("Path contains non-utf8 characters.");
PathBuf::from(&result_string[r"\\?\".len()..])
} else {
result
}
}
fn get_line(&mut self, src: Src<'_>) -> String {
let base_path = &self.base_path;
let lines = self.files.entry(src.file_name.to_owned()).or_insert_with(|| {
let file_name = &base_path.join(src.file_name);
let file =
File::open(file_name).expect(&format!("Couldn't find file: {:?}", file_name));
let lines = BufReader::new(file).lines();
lines.collect::<Result<Vec<_>, _>>().unwrap()
});
if src.line > lines.len() {
panic!("Line {} not in file, found {} lines", src.line, lines.len());
}
lines[src.line - 1].to_owned()
}
}
fn char_of_byte_index(s: &str, byte: usize) -> usize {
for (c, (b, _)) in s.char_indices().enumerate() {
if b == byte {
return c;
}
}
panic!("Couldn't find byte {} in {:?}", byte, s);
}