aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock8
-rw-r--r--crates/expect/Cargo.toml9
-rw-r--r--crates/expect/src/lib.rs308
-rw-r--r--crates/rust-analyzer/src/handlers.rs27
-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
8 files changed, 357 insertions, 5 deletions
diff --git a/Cargo.lock b/Cargo.lock
index fe71b9971..a3cfe5dc4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -348,6 +348,14 @@ dependencies = [
348] 348]
349 349
350[[package]] 350[[package]]
351name = "expect"
352version = "0.1.0"
353dependencies = [
354 "once_cell",
355 "stdx",
356]
357
358[[package]]
351name = "filetime" 359name = "filetime"
352version = "0.2.10" 360version = "0.2.10"
353source = "registry+https://github.com/rust-lang/crates.io-index" 361source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/crates/expect/Cargo.toml b/crates/expect/Cargo.toml
new file mode 100644
index 000000000..09eb57a43
--- /dev/null
+++ b/crates/expect/Cargo.toml
@@ -0,0 +1,9 @@
1[package]
2name = "expect"
3version = "0.1.0"
4authors = ["rust-analyzer developers"]
5edition = "2018"
6
7[dependencies]
8once_cell = "1"
9stdx = { path = "../stdx" }
diff --git a/crates/expect/src/lib.rs b/crates/expect/src/lib.rs
new file mode 100644
index 000000000..08d0eafdf
--- /dev/null
+++ b/crates/expect/src/lib.rs
@@ -0,0 +1,308 @@
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 path::{Path, PathBuf},
8 sync::Mutex,
9};
10
11use once_cell::sync::Lazy;
12use stdx::{lines_with_ends, trim_indent};
13
14const HELP: &str = "
15You can update all `expect![[]]` tests by:
16
17 env UPDATE_EXPECT=1 cargo test
18
19To update a single test, place the cursor on `expect` token and use `run` feature of rust-analyzer.
20";
21
22fn update_expect() -> bool {
23 env::var("UPDATE_EXPECT").is_ok()
24}
25
26/// expect![[""]]
27#[macro_export]
28macro_rules! expect {
29 [[$lit:literal]] => {$crate::Expect {
30 file: file!(),
31 line: line!(),
32 column: column!(),
33 data: $lit,
34 }};
35 [[]] => { $crate::expect![[""]] };
36}
37
38#[derive(Debug)]
39pub struct Expect {
40 pub file: &'static str,
41 pub line: u32,
42 pub column: u32,
43 pub data: &'static str,
44}
45
46impl Expect {
47 pub fn assert_eq(&self, actual: &str) {
48 let trimmed = self.trimmed();
49 if &trimmed == actual {
50 return;
51 }
52 Runtime::fail(self, &trimmed, actual);
53 }
54 pub fn assert_debug_eq(&self, actual: &impl fmt::Debug) {
55 let actual = format!("{:#?}\n", actual);
56 self.assert_eq(&actual)
57 }
58
59 fn trimmed(&self) -> String {
60 if !self.data.contains('\n') {
61 return self.data.to_string();
62 }
63 trim_indent(self.data)
64 }
65
66 fn locate(&self, file: &str) -> Location {
67 let mut target_line = None;
68 let mut line_start = 0;
69 for (i, line) in lines_with_ends(file).enumerate() {
70 if i == self.line as usize - 1 {
71 let pat = "expect![[";
72 let offset = line.find(pat).unwrap();
73 let literal_start = line_start + offset + pat.len();
74 let indent = line.chars().take_while(|&it| it == ' ').count();
75 target_line = Some((literal_start, indent));
76 break;
77 }
78 line_start += line.len();
79 }
80 let (literal_start, line_indent) = target_line.unwrap();
81 let literal_length = file[literal_start..].find("]]").unwrap();
82 let literal_range = literal_start..literal_start + literal_length;
83 Location { line_indent, literal_range }
84 }
85}
86
87#[derive(Default)]
88struct Runtime {
89 help_printed: bool,
90 per_file: HashMap<&'static str, FileRuntime>,
91}
92static RT: Lazy<Mutex<Runtime>> = Lazy::new(Default::default);
93
94impl Runtime {
95 fn fail(expect: &Expect, expected: &str, actual: &str) {
96 let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
97 let mut updated = "";
98 if update_expect() {
99 updated = " (updated)";
100 rt.per_file
101 .entry(expect.file)
102 .or_insert_with(|| FileRuntime::new(expect))
103 .update(expect, actual);
104 }
105 let print_help = !rt.help_printed && !update_expect();
106 rt.help_printed = true;
107
108 let help = if print_help { HELP } else { "" };
109 panic!(
110 "\n
111error: expect test failed{}
112 --> {}:{}:{}
113{}
114Expect:
115----
116{}
117----
118
119Actual:
120----
121{}
122----
123",
124 updated, expect.file, expect.line, expect.column, help, expected, actual
125 )
126 }
127}
128
129struct FileRuntime {
130 path: PathBuf,
131 original_text: String,
132 patchwork: Patchwork,
133}
134
135impl FileRuntime {
136 fn new(expect: &Expect) -> FileRuntime {
137 let path = workspace_root().join(expect.file);
138 let original_text = fs::read_to_string(&path).unwrap();
139 let patchwork = Patchwork::new(original_text.clone());
140 FileRuntime { path, original_text, patchwork }
141 }
142 fn update(&mut self, expect: &Expect, actual: &str) {
143 let loc = expect.locate(&self.original_text);
144 let patch = format_patch(loc.line_indent.clone(), actual);
145 self.patchwork.patch(loc.literal_range, &patch);
146 fs::write(&self.path, &self.patchwork.text).unwrap()
147 }
148}
149
150#[derive(Debug)]
151struct Location {
152 line_indent: usize,
153 literal_range: Range<usize>,
154}
155
156#[derive(Debug)]
157struct Patchwork {
158 text: String,
159 indels: Vec<(Range<usize>, usize)>,
160}
161
162impl Patchwork {
163 fn new(text: String) -> Patchwork {
164 Patchwork { text, indels: Vec::new() }
165 }
166 fn patch(&mut self, mut range: Range<usize>, patch: &str) {
167 self.indels.push((range.clone(), patch.len()));
168 self.indels.sort_by_key(|(delete, _insert)| delete.start);
169
170 let (delete, insert) = self
171 .indels
172 .iter()
173 .take_while(|(delete, _)| delete.start < range.start)
174 .map(|(delete, insert)| (delete.end - delete.start, insert))
175 .fold((0usize, 0usize), |(x1, y1), (x2, y2)| (x1 + x2, y1 + y2));
176
177 for pos in &mut [&mut range.start, &mut range.end] {
178 **pos += insert;
179 **pos -= delete
180 }
181
182 self.text.replace_range(range, &patch);
183 }
184}
185
186fn format_patch(line_indent: usize, patch: &str) -> String {
187 let mut max_hashes = 0;
188 let mut cur_hashes = 0;
189 for byte in patch.bytes() {
190 if byte != b'#' {
191 cur_hashes = 0;
192 continue;
193 }
194 cur_hashes += 1;
195 max_hashes = max_hashes.max(cur_hashes);
196 }
197 let hashes = &"#".repeat(max_hashes + 1);
198 let indent = &" ".repeat(line_indent);
199 let is_multiline = patch.contains('\n');
200
201 let mut buf = String::new();
202 buf.push('r');
203 buf.push_str(hashes);
204 buf.push('"');
205 if is_multiline {
206 buf.push('\n');
207 }
208 let mut final_newline = false;
209 for line in lines_with_ends(patch) {
210 if is_multiline {
211 buf.push_str(indent);
212 buf.push_str(" ");
213 }
214 buf.push_str(line);
215 final_newline = line.ends_with('\n');
216 }
217 if final_newline {
218 buf.push_str(indent);
219 }
220 buf.push('"');
221 buf.push_str(hashes);
222 buf
223}
224
225fn workspace_root() -> PathBuf {
226 Path::new(
227 &env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_owned()),
228 )
229 .ancestors()
230 .nth(2)
231 .unwrap()
232 .to_path_buf()
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_expect_macro() {
241 let empty = expect![[]];
242 expect![[r#"
243 Expect {
244 file: "crates/expect/src/lib.rs",
245 line: 241,
246 column: 21,
247 data: "",
248 }
249 "#]]
250 .assert_debug_eq(&empty);
251
252 let expect = expect![["
253 hello
254 world
255 "]];
256 expect![[r#"
257 Expect {
258 file: "crates/expect/src/lib.rs",
259 line: 252,
260 column: 22,
261 data: "\n hello\n world\n ",
262 }
263 "#]]
264 .assert_debug_eq(&expect);
265 }
266
267 #[test]
268 fn test_format_patch() {
269 let patch = format_patch(0, "hello\nworld\n");
270 expect![[r##"
271 r#"
272 hello
273 world
274 "#"##]]
275 .assert_eq(&patch);
276
277 let patch = format_patch(4, "single line");
278 expect![[r##"r#"single line"#"##]].assert_eq(&patch);
279 }
280
281 #[test]
282 fn test_patchwork() {
283 let mut patchwork = Patchwork::new("one two three".to_string());
284 patchwork.patch(4..7, "zwei");
285 patchwork.patch(0..3, "один");
286 patchwork.patch(8..13, "3");
287 expect![[r#"
288 Patchwork {
289 text: "один zwei 3",
290 indels: [
291 (
292 0..3,
293 8,
294 ),
295 (
296 4..7,
297 4,
298 ),
299 (
300 8..13,
301 1,
302 ),
303 ],
304 }
305 "#]]
306 .assert_debug_eq(&patchwork);
307 }
308}
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index 38e3c3324..4d0684b2a 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,21 @@ 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())
417 .and_then(|it| it.segment())
418 .and_then(|it| it.name_ref())
419 .map_or(false, |it| it.text() == "expect")
420 }
421 None => false,
422 };
423
424 let mut res = Vec::new();
412 for runnable in snap.analysis.runnables(file_id)? { 425 for runnable in snap.analysis.runnables(file_id)? {
413 if let Some(offset) = offset { 426 if let Some(offset) = offset {
414 if !runnable.nav.full_range().contains_inclusive(offset) { 427 if !runnable.nav.full_range().contains_inclusive(offset) {
@@ -418,8 +431,12 @@ pub(crate) fn handle_runnables(
418 if should_skip_target(&runnable, cargo_spec.as_ref()) { 431 if should_skip_target(&runnable, cargo_spec.as_ref()) {
419 continue; 432 continue;
420 } 433 }
421 434 let mut runnable = to_proto::runnable(&snap, file_id, runnable)?;
422 res.push(to_proto::runnable(&snap, file_id, runnable)?); 435 if expect_test {
436 runnable.label = format!("{} + expect", runnable.label);
437 runnable.args.expect_test = Some(true);
438 }
439 res.push(runnable);
423 } 440 }
424 441
425 // Add `cargo check` and `cargo test` for the whole package 442 // Add `cargo check` and `cargo test` for the whole package
@@ -438,6 +455,7 @@ pub(crate) fn handle_runnables(
438 spec.package.clone(), 455 spec.package.clone(),
439 ], 456 ],
440 executable_args: Vec::new(), 457 executable_args: Vec::new(),
458 expect_test: None,
441 }, 459 },
442 }) 460 })
443 } 461 }
@@ -451,6 +469,7 @@ pub(crate) fn handle_runnables(
451 workspace_root: None, 469 workspace_root: None,
452 cargo_args: vec!["check".to_string(), "--workspace".to_string()], 470 cargo_args: vec!["check".to_string(), "--workspace".to_string()],
453 executable_args: Vec::new(), 471 executable_args: Vec::new(),
472 expect_test: None,
454 }, 473 },
455 }); 474 });
456 } 475 }
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()