mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-27 05:23:24 +00:00
Merge #261
261: Add heavy test for code actions r=matklad a=flodiebold Here's the test for the code actions; I didn't find anything fitting on crates.io ([assert-json-diff](https://crates.io/crates/assert-json-diff) looks kind of nice, but doesn't have anything like the wildcards), so I copied the cargo code as you suggested. Co-authored-by: Florian Diebold <florian.diebold@freiheit.com>
This commit is contained in:
commit
66f656134f
6 changed files with 177 additions and 19 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1075,6 +1075,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"serde_json 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"text_unit 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"text_unit 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
mod support;
|
mod support;
|
||||||
|
|
||||||
use ra_lsp_server::req::{Runnables, RunnablesParams};
|
use serde_json::json;
|
||||||
|
|
||||||
|
use ra_lsp_server::req::{Runnables, RunnablesParams, CodeActionRequest, CodeActionParams};
|
||||||
|
|
||||||
|
use languageserver_types::{Position, Range, CodeActionContext};
|
||||||
|
|
||||||
use crate::support::project;
|
use crate::support::project;
|
||||||
|
|
||||||
|
@ -21,7 +25,7 @@ fn foo() {
|
||||||
text_document: server.doc_id("lib.rs"),
|
text_document: server.doc_id("lib.rs"),
|
||||||
position: None,
|
position: None,
|
||||||
},
|
},
|
||||||
r#"[
|
json!([
|
||||||
{
|
{
|
||||||
"args": [ "test", "--", "foo", "--nocapture" ],
|
"args": [ "test", "--", "foo", "--nocapture" ],
|
||||||
"bin": "cargo",
|
"bin": "cargo",
|
||||||
|
@ -51,7 +55,7 @@ fn foo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]"#,
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +82,7 @@ fn test_eggs() {}
|
||||||
text_document: server.doc_id("tests/spam.rs"),
|
text_document: server.doc_id("tests/spam.rs"),
|
||||||
position: None,
|
position: None,
|
||||||
},
|
},
|
||||||
r#"[
|
json!([
|
||||||
{
|
{
|
||||||
"args": [ "test", "--package", "foo", "--test", "spam", "--", "test_eggs", "--nocapture" ],
|
"args": [ "test", "--package", "foo", "--test", "spam", "--", "test_eggs", "--nocapture" ],
|
||||||
"bin": "cargo",
|
"bin": "cargo",
|
||||||
|
@ -111,6 +115,63 @@ fn test_eggs() {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]"#
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_missing_module_code_action() {
|
||||||
|
let server = project(
|
||||||
|
r#"
|
||||||
|
//- Cargo.toml
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "0.0.0"
|
||||||
|
|
||||||
|
//- src/lib.rs
|
||||||
|
mod bar;
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
server.wait_for_feedback("workspace loaded");
|
||||||
|
let empty_context = || CodeActionContext {
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
only: None,
|
||||||
|
};
|
||||||
|
server.request::<CodeActionRequest>(
|
||||||
|
CodeActionParams {
|
||||||
|
text_document: server.doc_id("src/lib.rs"),
|
||||||
|
range: Range::new(Position::new(0, 0), Position::new(0, 7)),
|
||||||
|
context: empty_context(),
|
||||||
|
},
|
||||||
|
json!([
|
||||||
|
{
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"cursorPosition": null,
|
||||||
|
"fileSystemEdits": [
|
||||||
|
{
|
||||||
|
"type": "createFile",
|
||||||
|
"uri": "file:///[..]/src/bar.rs"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label": "create module",
|
||||||
|
"sourceFileEdits": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"command": "ra-lsp.applySourceChange",
|
||||||
|
"title": "create module"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.request::<CodeActionRequest>(
|
||||||
|
CodeActionParams {
|
||||||
|
text_document: server.doc_id("src/lib.rs"),
|
||||||
|
range: Range::new(Position::new(2, 0), Position::new(2, 7)),
|
||||||
|
context: empty_context(),
|
||||||
|
},
|
||||||
|
json!([]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,9 @@ use languageserver_types::{
|
||||||
DidOpenTextDocumentParams, TextDocumentIdentifier, TextDocumentItem, Url,
|
DidOpenTextDocumentParams, TextDocumentIdentifier, TextDocumentItem, Url,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::{from_str, to_string_pretty, Value};
|
use serde_json::{to_string_pretty, Value};
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
use test_utils::parse_fixture;
|
use test_utils::{parse_fixture, find_mismatch};
|
||||||
|
|
||||||
use ra_lsp_server::{
|
use ra_lsp_server::{
|
||||||
main_loop, req,
|
main_loop, req,
|
||||||
|
@ -88,23 +88,24 @@ impl Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request<R>(&self, params: R::Params, expected_resp: &str)
|
pub fn request<R>(&self, params: R::Params, expected_resp: Value)
|
||||||
where
|
where
|
||||||
R: Request,
|
R: Request,
|
||||||
R::Params: Serialize,
|
R::Params: Serialize,
|
||||||
{
|
{
|
||||||
let id = self.req_id.get();
|
let id = self.req_id.get();
|
||||||
self.req_id.set(id + 1);
|
self.req_id.set(id + 1);
|
||||||
let expected_resp: Value = from_str(expected_resp).unwrap();
|
|
||||||
let actual = self.send_request::<R>(id, params);
|
let actual = self.send_request::<R>(id, params);
|
||||||
assert_eq!(
|
match find_mismatch(&expected_resp, &actual) {
|
||||||
expected_resp,
|
Some((expected_part, actual_part)) => panic!(
|
||||||
actual,
|
"JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n",
|
||||||
"Expected:\n{}\n\
|
to_string_pretty(&expected_resp).unwrap(),
|
||||||
Actual:\n{}\n",
|
to_string_pretty(&actual).unwrap(),
|
||||||
to_string_pretty(&expected_resp).unwrap(),
|
to_string_pretty(expected_part).unwrap(),
|
||||||
to_string_pretty(&actual).unwrap(),
|
to_string_pretty(actual_part).unwrap(),
|
||||||
);
|
),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_request<R>(&self, id: u64, params: R::Params) -> Value
|
fn send_request<R>(&self, id: u64, params: R::Params) -> Value
|
||||||
|
@ -139,7 +140,7 @@ impl Server {
|
||||||
pub fn wait_for_feedback_n(&self, feedback: &str, n: usize) {
|
pub fn wait_for_feedback_n(&self, feedback: &str, n: usize) {
|
||||||
let f = |msg: &RawMessage| match msg {
|
let f = |msg: &RawMessage| match msg {
|
||||||
RawMessage::Notification(n) if n.method == "internalFeedback" => {
|
RawMessage::Notification(n) if n.method == "internalFeedback" => {
|
||||||
return n.clone().cast::<req::InternalFeedback>().unwrap() == feedback
|
return n.clone().cast::<req::InternalFeedback>().unwrap() == feedback;
|
||||||
}
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
|
@ -159,7 +159,7 @@ pub(super) fn maybe_item(p: &mut Parser, flavor: ItemFlavor) -> MaybeItem {
|
||||||
MaybeItem::Modifiers
|
MaybeItem::Modifiers
|
||||||
} else {
|
} else {
|
||||||
MaybeItem::None
|
MaybeItem::None
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,3 +8,4 @@ authors = ["Aleksey Kladov <aleksey.kladov@gmail.com>"]
|
||||||
difference = "2.0.0"
|
difference = "2.0.0"
|
||||||
itertools = "0.7.8"
|
itertools = "0.7.8"
|
||||||
text_unit = "0.1.2"
|
text_unit = "0.1.2"
|
||||||
|
serde_json = "1.0.24"
|
||||||
|
|
|
@ -2,6 +2,7 @@ use std::fmt;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use text_unit::{TextRange, TextUnit};
|
use text_unit::{TextRange, TextUnit};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
pub use difference::Changeset as __Changeset;
|
pub use difference::Changeset as __Changeset;
|
||||||
|
|
||||||
|
@ -145,3 +146,96 @@ pub fn parse_fixture(fixture: &str) -> Vec<FixtureEntry> {
|
||||||
flush!();
|
flush!();
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Comparison functionality borrowed from cargo:
|
||||||
|
|
||||||
|
/// Compare a line with an expected pattern.
|
||||||
|
/// - Use `[..]` as a wildcard to match 0 or more characters on the same line
|
||||||
|
/// (similar to `.*` in a regex).
|
||||||
|
pub fn lines_match(expected: &str, actual: &str) -> bool {
|
||||||
|
// Let's not deal with / vs \ (windows...)
|
||||||
|
// First replace backslash-escaped backslashes with forward slashes
|
||||||
|
// which can occur in, for example, JSON output
|
||||||
|
let expected = expected.replace("\\\\", "/").replace("\\", "/");
|
||||||
|
let mut actual: &str = &actual.replace("\\\\", "/").replace("\\", "/");
|
||||||
|
for (i, part) in expected.split("[..]").enumerate() {
|
||||||
|
match actual.find(part) {
|
||||||
|
Some(j) => {
|
||||||
|
if i == 0 && j != 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
actual = &actual[j + part.len()..];
|
||||||
|
}
|
||||||
|
None => return false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actual.is_empty() || expected.ends_with("[..]")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lines_match_works() {
|
||||||
|
assert!(lines_match("a b", "a b"));
|
||||||
|
assert!(lines_match("a[..]b", "a b"));
|
||||||
|
assert!(lines_match("a[..]", "a b"));
|
||||||
|
assert!(lines_match("[..]", "a b"));
|
||||||
|
assert!(lines_match("[..]b", "a b"));
|
||||||
|
|
||||||
|
assert!(!lines_match("[..]b", "c"));
|
||||||
|
assert!(!lines_match("b", "c"));
|
||||||
|
assert!(!lines_match("b", "cb"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares JSON object for approximate equality.
|
||||||
|
// You can use `[..]` wildcard in strings (useful for OS dependent things such
|
||||||
|
// as paths). You can use a `"{...}"` string literal as a wildcard for
|
||||||
|
// arbitrary nested JSON (useful for parts of object emitted by other programs
|
||||||
|
// (e.g. rustc) rather than Cargo itself). Arrays are sorted before comparison.
|
||||||
|
pub fn find_mismatch<'a>(expected: &'a Value, actual: &'a Value) -> Option<(&'a Value, &'a Value)> {
|
||||||
|
use serde_json::Value::*;
|
||||||
|
match (expected, actual) {
|
||||||
|
(&Number(ref l), &Number(ref r)) if l == r => None,
|
||||||
|
(&Bool(l), &Bool(r)) if l == r => None,
|
||||||
|
(&String(ref l), &String(ref r)) if lines_match(l, r) => None,
|
||||||
|
(&Array(ref l), &Array(ref r)) => {
|
||||||
|
if l.len() != r.len() {
|
||||||
|
return Some((expected, actual));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut l = l.iter().collect::<Vec<_>>();
|
||||||
|
let mut r = r.iter().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
l.retain(
|
||||||
|
|l| match r.iter().position(|r| find_mismatch(l, r).is_none()) {
|
||||||
|
Some(i) => {
|
||||||
|
r.remove(i);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
None => true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if !l.is_empty() {
|
||||||
|
assert!(!r.is_empty());
|
||||||
|
Some((&l[0], &r[0]))
|
||||||
|
} else {
|
||||||
|
assert_eq!(r.len(), 0);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(&Object(ref l), &Object(ref r)) => {
|
||||||
|
let same_keys = l.len() == r.len() && l.keys().all(|k| r.contains_key(k));
|
||||||
|
if !same_keys {
|
||||||
|
return Some((expected, actual));
|
||||||
|
}
|
||||||
|
|
||||||
|
l.values()
|
||||||
|
.zip(r.values())
|
||||||
|
.filter_map(|(l, r)| find_mismatch(l, r))
|
||||||
|
.nth(0)
|
||||||
|
}
|
||||||
|
(&Null, &Null) => None,
|
||||||
|
// magic string literal "{...}" acts as wildcard for any sub-JSON
|
||||||
|
(&String(ref l), _) if l == "{...}" => None,
|
||||||
|
_ => Some((expected, actual)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue