Add MockAnalysis to make testing easier

This commit is contained in:
Aleksey Kladov 2018-10-31 22:34:31 +03:00
parent 41adf1bc4f
commit dfba29e4fb
7 changed files with 166 additions and 75 deletions

View file

@ -9,10 +9,8 @@ log = "0.4.5"
relative-path = "0.4.0"
rayon = "1.0.2"
fst = "0.3.1"
ra_syntax = { path = "../ra_syntax" }
ra_editor = { path = "../ra_editor" }
salsa = "0.7.0"
rustc-hash = "1.0"
[dev-dependencies]
ra_syntax = { path = "../ra_syntax" }
ra_editor = { path = "../ra_editor" }
test_utils = { path = "../test_utils" }

View file

@ -368,18 +368,15 @@ fn complete_fn(name_ref: ast::NameRef, scopes: &FnScopes, acc: &mut Vec<Completi
#[cfg(test)]
mod tests {
use test_utils::{assert_eq_dbg, extract_offset};
use test_utils::{assert_eq_dbg};
use crate::FileId;
use crate::mock_analysis::MockAnalysis;
use crate::mock_analysis::{single_file_with_position};
use super::*;
fn check_scope_completion(code: &str, expected_completions: &str) {
let (off, code) = extract_offset(&code);
let analysis = MockAnalysis::with_files(&[("/main.rs", &code)]).analysis();
let file_id = FileId(1);
let completions = scope_completion(&analysis.imp.db, file_id, off)
let (analysis, position) = single_file_with_position(code);
let completions = scope_completion(&analysis.imp.db, position.file_id, position.offset)
.unwrap()
.into_iter()
.filter(|c| c.snippet.is_none())
@ -388,10 +385,8 @@ mod tests {
}
fn check_snippet_completion(code: &str, expected_completions: &str) {
let (off, code) = extract_offset(&code);
let analysis = MockAnalysis::with_files(&[("/main.rs", &code)]).analysis();
let file_id = FileId(1);
let completions = scope_completion(&analysis.imp.db, file_id, off)
let (analysis, position) = single_file_with_position(code);
let completions = scope_completion(&analysis.imp.db, position.file_id, position.offset)
.unwrap()
.into_iter()
.filter(|c| c.snippet.is_some())

View file

@ -25,7 +25,7 @@ impl CrateGraph {
pub fn new() -> CrateGraph {
CrateGraph::default()
}
pub fn add_crate_root(&mut self, file_id: FileId) -> CrateId{
pub fn add_crate_root(&mut self, file_id: FileId) -> CrateId {
let crate_id = CrateId(self.crate_roots.len() as u32);
let prev = self.crate_roots.insert(crate_id, file_id);
assert!(prev.is_none());

View file

@ -13,7 +13,7 @@ mod imp;
mod symbol_index;
mod completion;
mod syntax_ptr;
mod mock_analysis;
pub mod mock_analysis;
use std::{
fmt,
@ -33,7 +33,6 @@ pub use crate::{
descriptors::function::FnDescriptor,
completion::CompletionItem,
input::{FileId, FileResolver, CrateGraph, CrateId},
mock_analysis::MockAnalysis,
};
pub use ra_editor::{
FileSymbol, Fold, FoldKind, HighlightedRange, LineIndex, Runnable,

View file

@ -2,11 +2,19 @@
use std::sync::Arc;
use relative_path::{RelativePath, RelativePathBuf};
use ra_syntax::TextUnit;
use test_utils::{extract_offset, parse_fixture, CURSOR_MARKER};
use crate::{
AnalysisChange, Analysis, AnalysisHost, FileId, FileResolver,
};
#[derive(Debug)]
pub struct FilePosition {
pub file_id: FileId,
pub offset: TextUnit,
}
/// Mock analysis is used in test to bootstrap an AnalysisHost/Analysis
/// from a set of in-memory files.
#[derive(Debug, Default)]
@ -18,11 +26,57 @@ impl MockAnalysis {
pub fn new() -> MockAnalysis {
MockAnalysis::default()
}
pub fn with_files(files: &[(&str, &str)]) -> MockAnalysis {
let files = files.iter()
.map(|it| (it.0.to_string(), it.1.to_string()))
.collect();
MockAnalysis { files }
/// Creates `MockAnalysis` using a fixture data in the following format:
///
/// ```notrust
/// //- /main.rs
/// mod foo;
/// fn main() {}
///
/// //- /foo.rs
/// struct Baz;
/// ```
pub fn with_files(fixture: &str) -> MockAnalysis {
let mut res = MockAnalysis::new();
for entry in parse_fixture(fixture) {
res.add_file(&entry.meta, &entry.text);
}
res
}
/// Same as `with_files`, but requires that a single file contains a `<|>` marker,
/// whose position is also returned.
pub fn with_files_and_position(fixture: &str) -> (MockAnalysis, FilePosition) {
let mut position = None;
let mut res = MockAnalysis::new();
for entry in parse_fixture(fixture) {
if entry.text.contains(CURSOR_MARKER) {
assert!(position.is_none(), "only one marker (<|>) per fixture is allowed");
position = Some(res.add_file_with_position(&entry.meta, &entry.text));
} else {
res.add_file(&entry.meta, &entry.text);
}
}
let position = position.expect("expected a marker (<|>)");
(res, position)
}
pub fn add_file(&mut self, path: &str, text: &str) -> FileId {
let file_id = FileId((self.files.len() + 1) as u32);
self.files.push((path.to_string(), text.to_string()));
file_id
}
pub fn add_file_with_position(&mut self, path: &str, text: &str) -> FilePosition {
let (offset, text) = extract_offset(text);
let file_id = FileId((self.files.len() + 1) as u32);
self.files.push((path.to_string(), text.to_string()));
FilePosition { file_id, offset }
}
pub fn id_of(&self, path: &str) -> FileId {
let (idx, _) = self.files.iter().enumerate()
.find(|(_, (p, _text))| path == p)
.expect("no file in this mock");
FileId(idx as u32 + 1)
}
pub fn analysis_host(self) -> AnalysisHost {
let mut host = AnalysisHost::new();
@ -44,6 +98,26 @@ impl MockAnalysis {
}
}
/// Creates analysis from a multi-file fixture, returns positions marked with <|>.
pub fn analysis_and_position(fixture: &str) -> (Analysis, FilePosition) {
let (mock, position) = MockAnalysis::with_files_and_position(fixture);
(mock.analysis(), position)
}
/// Creates analysis for a single file.
pub fn single_file(code: &str) -> (Analysis, FileId) {
let mut mock = MockAnalysis::new();
let file_id = mock.add_file("/main.rs", code);
(mock.analysis(), file_id)
}
/// Creates analysis for a single file, returns position marked with <|>.
pub fn single_file_with_position(code: &str) -> (Analysis, FilePosition) {
let mut mock = MockAnalysis::new();
let pos = mock.add_file_with_position("/main.rs", code);
(mock.analysis(), pos)
}
#[derive(Debug)]
struct FileMap(Vec<(FileId, RelativePathBuf)>);

View file

@ -5,38 +5,42 @@ extern crate relative_path;
extern crate rustc_hash;
extern crate test_utils;
use ra_syntax::TextRange;
use test_utils::{assert_eq_dbg, extract_offset};
use ra_syntax::{TextRange};
use test_utils::{assert_eq_dbg};
use ra_analysis::{
MockAnalysis,
AnalysisChange, Analysis, CrateGraph, CrateId, FileId, FnDescriptor,
AnalysisChange, CrateGraph, FileId, FnDescriptor,
mock_analysis::{MockAnalysis, single_file, single_file_with_position, analysis_and_position},
};
fn analysis(files: &[(&str, &str)]) -> Analysis {
MockAnalysis::with_files(files).analysis()
}
fn get_signature(text: &str) -> (FnDescriptor, Option<usize>) {
let (offset, code) = extract_offset(text);
let code = code.as_str();
let snap = analysis(&[("/lib.rs", code)]);
snap.resolve_callable(FileId(1), offset).unwrap().unwrap()
let (analysis, position) = single_file_with_position(text);
analysis.resolve_callable(position.file_id, position.offset).unwrap().unwrap()
}
#[test]
fn test_resolve_module() {
let snap = analysis(&[("/lib.rs", "mod foo;"), ("/foo.rs", "")]);
let symbols = snap.approximately_resolve_symbol(FileId(1), 4.into()).unwrap();
let (analysis, pos) = analysis_and_position("
//- /lib.rs
mod <|>foo;
//- /foo.rs
// empty
");
let symbols = analysis.approximately_resolve_symbol(pos.file_id, pos.offset).unwrap();
assert_eq_dbg(
r#"[(FileId(2), FileSymbol { name: "foo", node_range: [0; 0), kind: MODULE })]"#,
&symbols,
);
let snap = analysis(&[("/lib.rs", "mod foo;"), ("/foo/mod.rs", "")]);
let symbols = snap.approximately_resolve_symbol(FileId(1), 4.into()).unwrap();
let (analysis, pos) = analysis_and_position("
//- /lib.rs
mod <|>foo;
//- /foo/mod.rs
// empty
");
let symbols = analysis.approximately_resolve_symbol(pos.file_id, pos.offset).unwrap();
assert_eq_dbg(
r#"[(FileId(2), FileSymbol { name: "foo", node_range: [0; 0), kind: MODULE })]"#,
&symbols,
@ -45,8 +49,8 @@ fn test_resolve_module() {
#[test]
fn test_unresolved_module_diagnostic() {
let snap = analysis(&[("/lib.rs", "mod foo;")]);
let diagnostics = snap.diagnostics(FileId(1)).unwrap();
let (analysis, file_id) = single_file("mod foo;");
let diagnostics = analysis.diagnostics(file_id).unwrap();
assert_eq_dbg(
r#"[Diagnostic {
message: "unresolved module",
@ -62,15 +66,20 @@ fn test_unresolved_module_diagnostic() {
#[test]
fn test_unresolved_module_diagnostic_no_diag_for_inline_mode() {
let snap = analysis(&[("/lib.rs", "mod foo {}")]);
let diagnostics = snap.diagnostics(FileId(1)).unwrap();
let (analysis, file_id) = single_file("mod foo {}");
let diagnostics = analysis.diagnostics(file_id).unwrap();
assert_eq_dbg(r#"[]"#, &diagnostics);
}
#[test]
fn test_resolve_parent_module() {
let snap = analysis(&[("/lib.rs", "mod foo;"), ("/foo.rs", "")]);
let symbols = snap.parent_module(FileId(2)).unwrap();
let (analysis, pos) = analysis_and_position("
//- /lib.rs
mod foo;
//- /foo.rs
<|>// empty
");
let symbols = analysis.parent_module(pos.file_id).unwrap();
assert_eq_dbg(
r#"[(FileId(1), FileSymbol { name: "foo", node_range: [0; 8), kind: MODULE })]"#,
&symbols,
@ -79,23 +88,24 @@ fn test_resolve_parent_module() {
#[test]
fn test_resolve_crate_root() {
let mut host = MockAnalysis::with_files(
&[("/lib.rs", "mod foo;"), ("/foo.rs", "")]
).analysis_host();
let snap = host.analysis();
assert!(snap.crate_for(FileId(2)).unwrap().is_empty());
let mock = MockAnalysis::with_files("
//- /lib.rs
mod foo;
//- /foo.rs
// emtpy <|>
");
let root_file = mock.id_of("/lib.rs");
let mod_file = mock.id_of("/foo.rs");
let mut host = mock.analysis_host();
assert!(host.analysis().crate_for(mod_file).unwrap().is_empty());
let crate_graph = {
let mut g = CrateGraph::new();
g.add_crate_root(FileId(1));
g
};
let mut crate_graph = CrateGraph::new();
let crate_id = crate_graph.add_crate_root(root_file);
let mut change = AnalysisChange::new();
change.set_crate_graph(crate_graph);
host.apply_change(change);
let snap = host.analysis();
assert_eq!(snap.crate_for(FileId(2)).unwrap(), vec![CrateId(0)],);
assert_eq!(host.analysis().crate_for(mod_file).unwrap(), vec![crate_id]);
}
#[test]
@ -186,12 +196,8 @@ fn bar() {
}
fn get_all_refs(text: &str) -> Vec<(FileId, TextRange)> {
let (offset, code) = extract_offset(text);
let code = code.as_str();
let snap = analysis(&[("/lib.rs", code)]);
snap.find_all_refs(FileId(1), offset).unwrap()
let (analysis, position) = single_file_with_position(text);
analysis.find_all_refs(position.file_id, position.offset).unwrap()
}
#[test]
@ -226,11 +232,14 @@ fn test_find_all_refs_for_param_inside() {
#[test]
fn test_complete_crate_path() {
let snap = analysis(&[
("/lib.rs", "mod foo; struct Spam;"),
("/foo.rs", "use crate::Sp"),
]);
let completions = snap.completions(FileId(2), 13.into()).unwrap().unwrap();
let (analysis, position) = analysis_and_position("
//- /lib.rs
mod foo;
struct Spam;
//- /foo.rs
use crate::Sp<|>
");
let completions = analysis.completions(position.file_id, position.offset).unwrap().unwrap();
assert_eq_dbg(
r#"[CompletionItem { label: "foo", lookup: None, snippet: None },
CompletionItem { label: "Spam", lookup: None, snippet: None }]"#,

View file

@ -8,6 +8,8 @@ use text_unit::{TextRange, TextUnit};
pub use self::difference::Changeset as __Changeset;
pub const CURSOR_MARKER: &str = "<|>";
#[macro_export]
macro_rules! assert_eq_text {
($expected:expr, $actual:expr) => {{
@ -45,11 +47,10 @@ pub fn extract_offset(text: &str) -> (TextUnit, String) {
}
pub fn try_extract_offset(text: &str) -> Option<(TextUnit, String)> {
let cursor = "<|>";
let cursor_pos = text.find(cursor)?;
let mut new_text = String::with_capacity(text.len() - cursor.len());
let cursor_pos = text.find(CURSOR_MARKER)?;
let mut new_text = String::with_capacity(text.len() - CURSOR_MARKER.len());
new_text.push_str(&text[..cursor_pos]);
new_text.push_str(&text[cursor_pos + cursor.len()..]);
new_text.push_str(&text[cursor_pos + CURSOR_MARKER.len()..]);
let cursor_pos = TextUnit::from(cursor_pos as u32);
Some((cursor_pos, new_text))
}
@ -116,7 +117,22 @@ pub fn parse_fixture(fixture: &str) -> Vec<FixtureEntry> {
}
};
};
for line in fixture.lines() {
let margin = fixture.lines()
.filter(|it| it.trim_start().starts_with("//-"))
.map(|it| it.len() - it.trim_start().len())
.next().expect("empty fixture");
let lines = fixture.lines()
.filter_map(|line| {
if line.len() >= margin {
assert!(line[..margin].trim().is_empty());
Some(&line[margin..])
} else {
assert!(line.trim().is_empty());
None
}
});
for line in lines {
if line.starts_with("//-") {
flush!();
buf.clear();