aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock10
-rw-r--r--crates/expect/Cargo.toml10
-rw-r--r--crates/expect/src/lib.rs293
-rw-r--r--crates/ra_ide/Cargo.toml1
-rw-r--r--crates/ra_ide/src/goto_definition.rs41
-rw-r--r--crates/rust-analyzer/src/handlers.rs25
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs2
-rw-r--r--crates/rust-analyzer/src/to_proto.rs1
-rw-r--r--editors/code/src/lsp_ext.ts1
-rw-r--r--editors/code/src/run.ts6
10 files changed, 377 insertions, 13 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ca3d14a09..c10803645 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -352,6 +352,15 @@ dependencies = [
352] 352]
353 353
354[[package]] 354[[package]]
355name = "expect"
356version = "0.1.0"
357dependencies = [
358 "difference",
359 "once_cell",
360 "stdx",
361]
362
363[[package]]
355name = "filetime" 364name = "filetime"
356version = "0.2.10" 365version = "0.2.10"
357source = "registry+https://github.com/rust-lang/crates.io-index" 366source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1134,6 +1143,7 @@ name = "ra_ide"
1134version = "0.1.0" 1143version = "0.1.0"
1135dependencies = [ 1144dependencies = [
1136 "either", 1145 "either",
1146 "expect",
1137 "indexmap", 1147 "indexmap",
1138 "insta", 1148 "insta",
1139 "itertools", 1149 "itertools",
diff --git a/crates/expect/Cargo.toml b/crates/expect/Cargo.toml
new file mode 100644
index 000000000..caee43106
--- /dev/null
+++ b/crates/expect/Cargo.toml
@@ -0,0 +1,10 @@
1[package]
2name = "expect"
3version = "0.1.0"
4authors = ["rust-analyzer developers"]
5edition = "2018"
6
7[dependencies]
8once_cell = "1"
9difference = "2"
10stdx = { path = "../stdx" }
diff --git a/crates/expect/src/lib.rs b/crates/expect/src/lib.rs
new file mode 100644
index 000000000..dd7b96aab
--- /dev/null
+++ b/crates/expect/src/lib.rs
@@ -0,0 +1,293 @@
1//! Snapshot testing library, see
2//! https://github.com/rust-analyzer/rust-analyzer/pull/5101
3use std::{
4 collections::HashMap,
5 env, fmt, fs,
6 ops::Range,
7 panic,
8 path::{Path, PathBuf},
9 sync::Mutex,
10};
11
12use difference::Changeset;
13use once_cell::sync::Lazy;
14use stdx::{lines_with_ends, trim_indent};
15
16const HELP: &str = "
17You can update all `expect![[]]` tests by:
18
19 env UPDATE_EXPECT=1 cargo test
20
21To update a single test, place the cursor on `expect` token and use `run` feature of rust-analyzer.
22";
23
24fn update_expect() -> bool {
25 env::var("UPDATE_EXPECT").is_ok()
26}
27
28/// expect![[""]]
29#[macro_export]
30macro_rules! expect {
31 [[$lit:literal]] => {$crate::Expect {
32 file: file!(),
33 line: line!(),
34 column: column!(),
35 data: $lit,
36 }};
37 [[]] => { $crate::expect![[""]] };
38}
39
40#[derive(Debug)]
41pub struct Expect {
42 pub file: &'static str,
43 pub line: u32,
44 pub column: u32,
45 pub data: &'static str,
46}
47
48impl Expect {
49 pub fn assert_eq(&self, actual: &str) {
50 let trimmed = self.trimmed();
51 if &trimmed == actual {
52 return;
53 }
54 Runtime::fail(self, &trimmed, actual);
55 }
56 pub fn assert_debug_eq(&self, actual: &impl fmt::Debug) {
57 let actual = format!("{:#?}\n", actual);
58 self.assert_eq(&actual)
59 }
60
61 fn trimmed(&self) -> String {
62 if !self.data.contains('\n') {
63 return self.data.to_string();
64 }
65 trim_indent(self.data)
66 }
67
68 fn locate(&self, file: &str) -> Location {
69 let mut target_line = None;
70 let mut line_start = 0;
71 for (i, line) in lines_with_ends(file).enumerate() {
72 if i == self.line as usize - 1 {
73 let pat = "expect![[";
74 let offset = line.find(pat).unwrap();
75 let literal_start = line_start + offset + pat.len();
76 let indent = line.chars().take_while(|&it| it == ' ').count();
77 target_line = Some((literal_start, indent));
78 break;
79 }
80 line_start += line.len();
81 }
82 let (literal_start, line_indent) = target_line.unwrap();
83 let literal_length =
84 file[literal_start..].find("]]").expect("Couldn't find matching `]]` for `expect![[`.");
85 let literal_range = literal_start..literal_start + literal_length;
86 Location { line_indent, literal_range }
87 }
88}
89
90#[derive(Default)]
91struct Runtime {
92 help_printed: bool,
93 per_file: HashMap<&'static str, FileRuntime>,
94}
95static RT: Lazy<Mutex<Runtime>> = Lazy::new(Default::default);
96
97impl Runtime {
98 fn fail(expect: &Expect, expected: &str, actual: &str) {
99 let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
100 let mut updated = "";
101 if update_expect() {
102 updated = " (updated)";
103 rt.per_file
104 .entry(expect.file)
105 .or_insert_with(|| FileRuntime::new(expect))
106 .update(expect, actual);
107 }
108 let print_help = !rt.help_printed && !update_expect();
109 rt.help_printed = true;
110
111 let help = if print_help { HELP } else { "" };
112
113 let diff = Changeset::new(actual, expected, "\n");
114
115 println!(
116 "\n
117\x1b[1m\x1b[91merror\x1b[97m: expect test failed\x1b[0m{}
118 \x1b[1m\x1b[34m-->\x1b[0m {}:{}:{}
119{}
120\x1b[1mExpect\x1b[0m:
121----
122{}
123----
124
125\x1b[1mActual\x1b[0m:
126----
127{}
128----
129
130\x1b[1mDiff\x1b[0m:
131----
132{}
133----
134",
135 updated, expect.file, expect.line, expect.column, help, expected, actual, diff
136 );
137 // Use resume_unwind instead of panic!() to prevent a backtrace, which is unnecessary noise.
138 panic::resume_unwind(Box::new(()));
139 }
140}
141
142struct FileRuntime {
143 path: PathBuf,
144 original_text: String,
145 patchwork: Patchwork,
146}
147
148impl FileRuntime {
149 fn new(expect: &Expect) -> FileRuntime {
150 let path = workspace_root().join(expect.file);
151 let original_text = fs::read_to_string(&path).unwrap();
152 let patchwork = Patchwork::new(original_text.clone());
153 FileRuntime { path, original_text, patchwork }
154 }
155 fn update(&mut self, expect: &Expect, actual: &str) {
156 let loc = expect.locate(&self.original_text);
157 let patch = format_patch(loc.line_indent.clone(), actual);
158 self.patchwork.patch(loc.literal_range, &patch);
159 fs::write(&self.path, &self.patchwork.text).unwrap()
160 }
161}
162
163#[derive(Debug)]
164struct Location {
165 line_indent: usize,
166 literal_range: Range<usize>,
167}
168
169#[derive(Debug)]
170struct Patchwork {
171 text: String,
172 indels: Vec<(Range<usize>, usize)>,
173}
174
175impl Patchwork {
176 fn new(text: String) -> Patchwork {
177 Patchwork { text, indels: Vec::new() }
178 }
179 fn patch(&mut self, mut range: Range<usize>, patch: &str) {
180 self.indels.push((range.clone(), patch.len()));
181 self.indels.sort_by_key(|(delete, _insert)| delete.start);
182
183 let (delete, insert) = self
184 .indels
185 .iter()
186 .take_while(|(delete, _)| delete.start < range.start)
187 .map(|(delete, insert)| (delete.end - delete.start, insert))
188 .fold((0usize, 0usize), |(x1, y1), (x2, y2)| (x1 + x2, y1 + y2));
189
190 for pos in &mut [&mut range.start, &mut range.end] {
191 **pos -= delete;
192 **pos += insert;
193 }
194
195 self.text.replace_range(range, &patch);
196 }
197}
198
199fn format_patch(line_indent: usize, patch: &str) -> String {
200 let mut max_hashes = 0;
201 let mut cur_hashes = 0;
202 for byte in patch.bytes() {
203 if byte != b'#' {
204 cur_hashes = 0;
205 continue;
206 }
207 cur_hashes += 1;
208 max_hashes = max_hashes.max(cur_hashes);
209 }
210 let hashes = &"#".repeat(max_hashes + 1);
211 let indent = &" ".repeat(line_indent);
212 let is_multiline = patch.contains('\n');
213
214 let mut buf = String::new();
215 buf.push('r');
216 buf.push_str(hashes);
217 buf.push('"');
218 if is_multiline {
219 buf.push('\n');
220 }
221 let mut final_newline = false;
222 for line in lines_with_ends(patch) {
223 if is_multiline {
224 buf.push_str(indent);
225 buf.push_str(" ");
226 }
227 buf.push_str(line);
228 final_newline = line.ends_with('\n');
229 }
230 if final_newline {
231 buf.push_str(indent);
232 }
233 buf.push('"');
234 buf.push_str(hashes);
235 buf
236}
237
238fn workspace_root() -> PathBuf {
239 Path::new(
240 &env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_owned()),
241 )
242 .ancestors()
243 .nth(2)
244 .unwrap()
245 .to_path_buf()
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_format_patch() {
254 let patch = format_patch(0, "hello\nworld\n");
255 expect![[r##"
256 r#"
257 hello
258 world
259 "#"##]]
260 .assert_eq(&patch);
261
262 let patch = format_patch(4, "single line");
263 expect![[r##"r#"single line"#"##]].assert_eq(&patch);
264 }
265
266 #[test]
267 fn test_patchwork() {
268 let mut patchwork = Patchwork::new("one two three".to_string());
269 patchwork.patch(4..7, "zwei");
270 patchwork.patch(0..3, "один");
271 patchwork.patch(8..13, "3");
272 expect![[r#"
273 Patchwork {
274 text: "один zwei 3",
275 indels: [
276 (
277 0..3,
278 8,
279 ),
280 (
281 4..7,
282 4,
283 ),
284 (
285 8..13,
286 1,
287 ),
288 ],
289 }
290 "#]]
291 .assert_debug_eq(&patchwork);
292 }
293}
diff --git a/crates/ra_ide/Cargo.toml b/crates/ra_ide/Cargo.toml
index bbc6a5c9b..8e8892309 100644
--- a/crates/ra_ide/Cargo.toml
+++ b/crates/ra_ide/Cargo.toml
@@ -28,6 +28,7 @@ ra_cfg = { path = "../ra_cfg" }
28ra_fmt = { path = "../ra_fmt" } 28ra_fmt = { path = "../ra_fmt" }
29ra_prof = { path = "../ra_prof" } 29ra_prof = { path = "../ra_prof" }
30test_utils = { path = "../test_utils" } 30test_utils = { path = "../test_utils" }
31expect = { path = "../expect" }
31ra_assists = { path = "../ra_assists" } 32ra_assists = { path = "../ra_assists" }
32ra_ssr = { path = "../ra_ssr" } 33ra_ssr = { path = "../ra_ssr" }
33 34
diff --git a/crates/ra_ide/src/goto_definition.rs b/crates/ra_ide/src/goto_definition.rs
index bea7fbfa7..969d5e0ff 100644
--- a/crates/ra_ide/src/goto_definition.rs
+++ b/crates/ra_ide/src/goto_definition.rs
@@ -103,6 +103,7 @@ pub(crate) fn reference_definition(
103 103
104#[cfg(test)] 104#[cfg(test)]
105mod tests { 105mod tests {
106 use expect::{expect, Expect};
106 use test_utils::assert_eq_text; 107 use test_utils::assert_eq_text;
107 108
108 use crate::mock_analysis::analysis_and_position; 109 use crate::mock_analysis::analysis_and_position;
@@ -142,16 +143,40 @@ mod tests {
142 nav.assert_match(expected); 143 nav.assert_match(expected);
143 } 144 }
144 145
146 fn check(ra_fixture: &str, expect: Expect) {
147 let (analysis, pos) = analysis_and_position(ra_fixture);
148
149 let mut navs = analysis.goto_definition(pos).unwrap().unwrap().info;
150 if navs.len() == 0 {
151 panic!("unresolved reference")
152 }
153 assert_eq!(navs.len(), 1);
154
155 let nav = navs.pop().unwrap();
156 let file_text = analysis.file_text(nav.file_id()).unwrap();
157
158 let mut actual = nav.debug_render();
159 actual += "\n";
160 actual += &file_text[nav.full_range()].to_string();
161 if let Some(focus) = nav.focus_range() {
162 actual += "|";
163 actual += &file_text[focus];
164 actual += "\n";
165 }
166 expect.assert_eq(&actual);
167 }
168
145 #[test] 169 #[test]
146 fn goto_def_in_items() { 170 fn goto_def_in_items() {
147 check_goto( 171 check(
148 " 172 r#"
149 //- /lib.rs 173struct Foo;
150 struct Foo; 174enum E { X(Foo<|>) }
151 enum E { X(Foo<|>) } 175"#,
152 ", 176 expect![[r#"
153 "Foo STRUCT_DEF FileId(1) 0..11 7..10", 177 Foo STRUCT_DEF FileId(1) 0..11 7..10
154 "struct Foo;|Foo", 178 struct Foo;|Foo
179 "#]],
155 ); 180 );
156 } 181 }
157 182
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index e35a5e846..0940fcc28 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -23,7 +23,7 @@ use ra_ide::{
23}; 23};
24use ra_prof::profile; 24use ra_prof::profile;
25use ra_project_model::TargetKind; 25use ra_project_model::TargetKind;
26use ra_syntax::{AstNode, SyntaxKind, TextRange, TextSize}; 26use ra_syntax::{algo, ast, AstNode, SyntaxKind, TextRange, TextSize};
27use serde::{Deserialize, Serialize}; 27use serde::{Deserialize, Serialize};
28use serde_json::to_value; 28use serde_json::to_value;
29use stdx::{format_to, split_delim}; 29use stdx::{format_to, split_delim};
@@ -407,8 +407,19 @@ pub(crate) fn handle_runnables(
407 let file_id = from_proto::file_id(&snap, &params.text_document.uri)?; 407 let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
408 let line_index = snap.analysis.file_line_index(file_id)?; 408 let line_index = snap.analysis.file_line_index(file_id)?;
409 let offset = params.position.map(|it| from_proto::offset(&line_index, it)); 409 let offset = params.position.map(|it| from_proto::offset(&line_index, it));
410 let mut res = Vec::new();
411 let cargo_spec = CargoTargetSpec::for_file(&snap, file_id)?; 410 let cargo_spec = CargoTargetSpec::for_file(&snap, file_id)?;
411
412 let expect_test = match offset {
413 Some(offset) => {
414 let source_file = snap.analysis.parse(file_id)?;
415 algo::find_node_at_offset::<ast::MacroCall>(source_file.syntax(), offset)
416 .and_then(|it| it.path()?.segment()?.name_ref())
417 .map_or(false, |it| it.text() == "expect")
418 }
419 None => false,
420 };
421
422 let mut res = Vec::new();
412 for runnable in snap.analysis.runnables(file_id)? { 423 for runnable in snap.analysis.runnables(file_id)? {
413 if let Some(offset) = offset { 424 if let Some(offset) = offset {
414 if !runnable.nav.full_range().contains_inclusive(offset) { 425 if !runnable.nav.full_range().contains_inclusive(offset) {
@@ -418,8 +429,12 @@ pub(crate) fn handle_runnables(
418 if should_skip_target(&runnable, cargo_spec.as_ref()) { 429 if should_skip_target(&runnable, cargo_spec.as_ref()) {
419 continue; 430 continue;
420 } 431 }
421 432 let mut runnable = to_proto::runnable(&snap, file_id, runnable)?;
422 res.push(to_proto::runnable(&snap, file_id, runnable)?); 433 if expect_test {
434 runnable.label = format!("{} + expect", runnable.label);
435 runnable.args.expect_test = Some(true);
436 }
437 res.push(runnable);
423 } 438 }
424 439
425 // Add `cargo check` and `cargo test` for the whole package 440 // Add `cargo check` and `cargo test` for the whole package
@@ -438,6 +453,7 @@ pub(crate) fn handle_runnables(
438 spec.package.clone(), 453 spec.package.clone(),
439 ], 454 ],
440 executable_args: Vec::new(), 455 executable_args: Vec::new(),
456 expect_test: None,
441 }, 457 },
442 }) 458 })
443 } 459 }
@@ -451,6 +467,7 @@ pub(crate) fn handle_runnables(
451 workspace_root: None, 467 workspace_root: None,
452 cargo_args: vec!["check".to_string(), "--workspace".to_string()], 468 cargo_args: vec!["check".to_string(), "--workspace".to_string()],
453 executable_args: Vec::new(), 469 executable_args: Vec::new(),
470 expect_test: None,
454 }, 471 },
455 }); 472 });
456 } 473 }
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 1371f6cb4..1befe678c 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -161,6 +161,8 @@ pub struct CargoRunnable {
161 pub cargo_args: Vec<String>, 161 pub cargo_args: Vec<String>,
162 // stuff after -- 162 // stuff after --
163 pub executable_args: Vec<String>, 163 pub executable_args: Vec<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub expect_test: Option<bool>,
164} 166}
165 167
166pub enum InlayHints {} 168pub enum InlayHints {}
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index f6cb8e4bb..a03222ae9 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -666,6 +666,7 @@ pub(crate) fn runnable(
666 workspace_root: workspace_root.map(|it| it.into()), 666 workspace_root: workspace_root.map(|it| it.into()),
667 cargo_args, 667 cargo_args,
668 executable_args, 668 executable_args,
669 expect_test: None,
669 }, 670 },
670 }) 671 })
671} 672}
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index e16ea799c..fdb99956b 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -60,6 +60,7 @@ export interface Runnable {
60 workspaceRoot?: string; 60 workspaceRoot?: string;
61 cargoArgs: string[]; 61 cargoArgs: string[];
62 executableArgs: string[]; 62 executableArgs: string[];
63 expectTest?: boolean;
63 }; 64 };
64} 65}
65export const runnables = new lc.RequestType<RunnablesParams, Runnable[], void>("experimental/runnables"); 66export const runnables = new lc.RequestType<RunnablesParams, Runnable[], void>("experimental/runnables");
diff --git a/editors/code/src/run.ts b/editors/code/src/run.ts
index 766b05112..e1430e31f 100644
--- a/editors/code/src/run.ts
+++ b/editors/code/src/run.ts
@@ -108,12 +108,16 @@ export async function createTask(runnable: ra.Runnable, config: Config): Promise
108 if (runnable.args.executableArgs.length > 0) { 108 if (runnable.args.executableArgs.length > 0) {
109 args.push('--', ...runnable.args.executableArgs); 109 args.push('--', ...runnable.args.executableArgs);
110 } 110 }
111 const env: { [key: string]: string } = { "RUST_BACKTRACE": "short" };
112 if (runnable.args.expectTest) {
113 env["UPDATE_EXPECT"] = "1";
114 }
111 const definition: tasks.CargoTaskDefinition = { 115 const definition: tasks.CargoTaskDefinition = {
112 type: tasks.TASK_TYPE, 116 type: tasks.TASK_TYPE,
113 command: args[0], // run, test, etc... 117 command: args[0], // run, test, etc...
114 args: args.slice(1), 118 args: args.slice(1),
115 cwd: runnable.args.workspaceRoot, 119 cwd: runnable.args.workspaceRoot,
116 env: Object.assign({}, process.env as { [key: string]: string }, { "RUST_BACKTRACE": "short" }), 120 env: Object.assign({}, process.env as { [key: string]: string }, env),
117 }; 121 };
118 122
119 const target = vscode.workspace.workspaceFolders![0]; // safe, see main activate() 123 const target = vscode.workspace.workspaceFolders![0]; // safe, see main activate()