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" relative-path = "0.4.0"
rayon = "1.0.2" rayon = "1.0.2"
fst = "0.3.1" fst = "0.3.1"
ra_syntax = { path = "../ra_syntax" }
ra_editor = { path = "../ra_editor" }
salsa = "0.7.0" salsa = "0.7.0"
rustc-hash = "1.0" rustc-hash = "1.0"
ra_syntax = { path = "../ra_syntax" }
[dev-dependencies] ra_editor = { path = "../ra_editor" }
test_utils = { path = "../test_utils" } 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)] #[cfg(test)]
mod tests { mod tests {
use test_utils::{assert_eq_dbg, extract_offset}; use test_utils::{assert_eq_dbg};
use crate::FileId; use crate::mock_analysis::{single_file_with_position};
use crate::mock_analysis::MockAnalysis;
use super::*; use super::*;
fn check_scope_completion(code: &str, expected_completions: &str) { fn check_scope_completion(code: &str, expected_completions: &str) {
let (off, code) = extract_offset(&code); let (analysis, position) = single_file_with_position(code);
let analysis = MockAnalysis::with_files(&[("/main.rs", &code)]).analysis(); let completions = scope_completion(&analysis.imp.db, position.file_id, position.offset)
let file_id = FileId(1);
let completions = scope_completion(&analysis.imp.db, file_id, off)
.unwrap() .unwrap()
.into_iter() .into_iter()
.filter(|c| c.snippet.is_none()) .filter(|c| c.snippet.is_none())
@ -388,10 +385,8 @@ mod tests {
} }
fn check_snippet_completion(code: &str, expected_completions: &str) { fn check_snippet_completion(code: &str, expected_completions: &str) {
let (off, code) = extract_offset(&code); let (analysis, position) = single_file_with_position(code);
let analysis = MockAnalysis::with_files(&[("/main.rs", &code)]).analysis(); let completions = scope_completion(&analysis.imp.db, position.file_id, position.offset)
let file_id = FileId(1);
let completions = scope_completion(&analysis.imp.db, file_id, off)
.unwrap() .unwrap()
.into_iter() .into_iter()
.filter(|c| c.snippet.is_some()) .filter(|c| c.snippet.is_some())

View file

@ -25,7 +25,7 @@ impl CrateGraph {
pub fn new() -> CrateGraph { pub fn new() -> CrateGraph {
CrateGraph::default() 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 crate_id = CrateId(self.crate_roots.len() as u32);
let prev = self.crate_roots.insert(crate_id, file_id); let prev = self.crate_roots.insert(crate_id, file_id);
assert!(prev.is_none()); assert!(prev.is_none());

View file

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

View file

@ -2,11 +2,19 @@
use std::sync::Arc; use std::sync::Arc;
use relative_path::{RelativePath, RelativePathBuf}; use relative_path::{RelativePath, RelativePathBuf};
use ra_syntax::TextUnit;
use test_utils::{extract_offset, parse_fixture, CURSOR_MARKER};
use crate::{ use crate::{
AnalysisChange, Analysis, AnalysisHost, FileId, FileResolver, 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 /// Mock analysis is used in test to bootstrap an AnalysisHost/Analysis
/// from a set of in-memory files. /// from a set of in-memory files.
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -18,11 +26,57 @@ impl MockAnalysis {
pub fn new() -> MockAnalysis { pub fn new() -> MockAnalysis {
MockAnalysis::default() MockAnalysis::default()
} }
pub fn with_files(files: &[(&str, &str)]) -> MockAnalysis { /// Creates `MockAnalysis` using a fixture data in the following format:
let files = files.iter() ///
.map(|it| (it.0.to_string(), it.1.to_string())) /// ```notrust
.collect(); /// //- /main.rs
MockAnalysis { files } /// 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 { pub fn analysis_host(self) -> AnalysisHost {
let mut host = AnalysisHost::new(); 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)] #[derive(Debug)]
struct FileMap(Vec<(FileId, RelativePathBuf)>); struct FileMap(Vec<(FileId, RelativePathBuf)>);

View file

@ -5,38 +5,42 @@ extern crate relative_path;
extern crate rustc_hash; extern crate rustc_hash;
extern crate test_utils; extern crate test_utils;
use ra_syntax::TextRange; use ra_syntax::{TextRange};
use test_utils::{assert_eq_dbg, extract_offset}; use test_utils::{assert_eq_dbg};
use ra_analysis::{ use ra_analysis::{
MockAnalysis, AnalysisChange, CrateGraph, FileId, FnDescriptor,
AnalysisChange, Analysis, CrateGraph, CrateId, 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>) { fn get_signature(text: &str) -> (FnDescriptor, Option<usize>) {
let (offset, code) = extract_offset(text); let (analysis, position) = single_file_with_position(text);
let code = code.as_str(); analysis.resolve_callable(position.file_id, position.offset).unwrap().unwrap()
let snap = analysis(&[("/lib.rs", code)]);
snap.resolve_callable(FileId(1), offset).unwrap().unwrap()
} }
#[test] #[test]
fn test_resolve_module() { fn test_resolve_module() {
let snap = analysis(&[("/lib.rs", "mod foo;"), ("/foo.rs", "")]); let (analysis, pos) = analysis_and_position("
let symbols = snap.approximately_resolve_symbol(FileId(1), 4.into()).unwrap(); //- /lib.rs
mod <|>foo;
//- /foo.rs
// empty
");
let symbols = analysis.approximately_resolve_symbol(pos.file_id, pos.offset).unwrap();
assert_eq_dbg( assert_eq_dbg(
r#"[(FileId(2), FileSymbol { name: "foo", node_range: [0; 0), kind: MODULE })]"#, r#"[(FileId(2), FileSymbol { name: "foo", node_range: [0; 0), kind: MODULE })]"#,
&symbols, &symbols,
); );
let snap = analysis(&[("/lib.rs", "mod foo;"), ("/foo/mod.rs", "")]); let (analysis, pos) = analysis_and_position("
let symbols = snap.approximately_resolve_symbol(FileId(1), 4.into()).unwrap(); //- /lib.rs
mod <|>foo;
//- /foo/mod.rs
// empty
");
let symbols = analysis.approximately_resolve_symbol(pos.file_id, pos.offset).unwrap();
assert_eq_dbg( assert_eq_dbg(
r#"[(FileId(2), FileSymbol { name: "foo", node_range: [0; 0), kind: MODULE })]"#, r#"[(FileId(2), FileSymbol { name: "foo", node_range: [0; 0), kind: MODULE })]"#,
&symbols, &symbols,
@ -45,8 +49,8 @@ fn test_resolve_module() {
#[test] #[test]
fn test_unresolved_module_diagnostic() { fn test_unresolved_module_diagnostic() {
let snap = analysis(&[("/lib.rs", "mod foo;")]); let (analysis, file_id) = single_file("mod foo;");
let diagnostics = snap.diagnostics(FileId(1)).unwrap(); let diagnostics = analysis.diagnostics(file_id).unwrap();
assert_eq_dbg( assert_eq_dbg(
r#"[Diagnostic { r#"[Diagnostic {
message: "unresolved module", message: "unresolved module",
@ -62,15 +66,20 @@ fn test_unresolved_module_diagnostic() {
#[test] #[test]
fn test_unresolved_module_diagnostic_no_diag_for_inline_mode() { fn test_unresolved_module_diagnostic_no_diag_for_inline_mode() {
let snap = analysis(&[("/lib.rs", "mod foo {}")]); let (analysis, file_id) = single_file("mod foo {}");
let diagnostics = snap.diagnostics(FileId(1)).unwrap(); let diagnostics = analysis.diagnostics(file_id).unwrap();
assert_eq_dbg(r#"[]"#, &diagnostics); assert_eq_dbg(r#"[]"#, &diagnostics);
} }
#[test] #[test]
fn test_resolve_parent_module() { fn test_resolve_parent_module() {
let snap = analysis(&[("/lib.rs", "mod foo;"), ("/foo.rs", "")]); let (analysis, pos) = analysis_and_position("
let symbols = snap.parent_module(FileId(2)).unwrap(); //- /lib.rs
mod foo;
//- /foo.rs
<|>// empty
");
let symbols = analysis.parent_module(pos.file_id).unwrap();
assert_eq_dbg( assert_eq_dbg(
r#"[(FileId(1), FileSymbol { name: "foo", node_range: [0; 8), kind: MODULE })]"#, r#"[(FileId(1), FileSymbol { name: "foo", node_range: [0; 8), kind: MODULE })]"#,
&symbols, &symbols,
@ -79,23 +88,24 @@ fn test_resolve_parent_module() {
#[test] #[test]
fn test_resolve_crate_root() { fn test_resolve_crate_root() {
let mut host = MockAnalysis::with_files( let mock = MockAnalysis::with_files("
&[("/lib.rs", "mod foo;"), ("/foo.rs", "")] //- /lib.rs
).analysis_host(); mod foo;
let snap = host.analysis(); //- /foo.rs
assert!(snap.crate_for(FileId(2)).unwrap().is_empty()); // 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 crate_graph = CrateGraph::new();
let mut g = CrateGraph::new(); let crate_id = crate_graph.add_crate_root(root_file);
g.add_crate_root(FileId(1));
g
};
let mut change = AnalysisChange::new(); let mut change = AnalysisChange::new();
change.set_crate_graph(crate_graph); change.set_crate_graph(crate_graph);
host.apply_change(change); 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] #[test]
@ -186,12 +196,8 @@ fn bar() {
} }
fn get_all_refs(text: &str) -> Vec<(FileId, TextRange)> { fn get_all_refs(text: &str) -> Vec<(FileId, TextRange)> {
let (offset, code) = extract_offset(text); let (analysis, position) = single_file_with_position(text);
let code = code.as_str(); analysis.find_all_refs(position.file_id, position.offset).unwrap()
let snap = analysis(&[("/lib.rs", code)]);
snap.find_all_refs(FileId(1), offset).unwrap()
} }
#[test] #[test]
@ -226,11 +232,14 @@ fn test_find_all_refs_for_param_inside() {
#[test] #[test]
fn test_complete_crate_path() { fn test_complete_crate_path() {
let snap = analysis(&[ let (analysis, position) = analysis_and_position("
("/lib.rs", "mod foo; struct Spam;"), //- /lib.rs
("/foo.rs", "use crate::Sp"), mod foo;
]); struct Spam;
let completions = snap.completions(FileId(2), 13.into()).unwrap().unwrap(); //- /foo.rs
use crate::Sp<|>
");
let completions = analysis.completions(position.file_id, position.offset).unwrap().unwrap();
assert_eq_dbg( assert_eq_dbg(
r#"[CompletionItem { label: "foo", lookup: None, snippet: None }, r#"[CompletionItem { label: "foo", lookup: None, snippet: None },
CompletionItem { label: "Spam", 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 use self::difference::Changeset as __Changeset;
pub const CURSOR_MARKER: &str = "<|>";
#[macro_export] #[macro_export]
macro_rules! assert_eq_text { macro_rules! assert_eq_text {
($expected:expr, $actual:expr) => {{ ($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)> { pub fn try_extract_offset(text: &str) -> Option<(TextUnit, String)> {
let cursor = "<|>"; let cursor_pos = text.find(CURSOR_MARKER)?;
let cursor_pos = text.find(cursor)?; let mut new_text = String::with_capacity(text.len() - CURSOR_MARKER.len());
let mut new_text = String::with_capacity(text.len() - cursor.len());
new_text.push_str(&text[..cursor_pos]); 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); let cursor_pos = TextUnit::from(cursor_pos as u32);
Some((cursor_pos, new_text)) 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("//-") { if line.starts_with("//-") {
flush!(); flush!();
buf.clear(); buf.clear();