aboutsummaryrefslogtreecommitdiff
path: root/crates/test_utils/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/test_utils/src/lib.rs')
-rw-r--r--crates/test_utils/src/lib.rs164
1 files changed, 49 insertions, 115 deletions
diff --git a/crates/test_utils/src/lib.rs b/crates/test_utils/src/lib.rs
index caf847273..e4aa894ac 100644
--- a/crates/test_utils/src/lib.rs
+++ b/crates/test_utils/src/lib.rs
@@ -11,8 +11,9 @@ pub mod mark;
11mod fixture; 11mod fixture;
12 12
13use std::{ 13use std::{
14 convert::{TryFrom, TryInto},
14 env, fs, 15 env, fs,
15 path::{Path, PathBuf}, 16 path::PathBuf,
16}; 17};
17 18
18use serde_json::Value; 19use serde_json::Value;
@@ -117,8 +118,8 @@ pub fn extract_range_or_offset(text: &str) -> (RangeOrOffset, String) {
117} 118}
118 119
119/// Extracts ranges, marked with `<tag> </tag>` pairs from the `text` 120/// Extracts ranges, marked with `<tag> </tag>` pairs from the `text`
120pub fn extract_ranges(mut text: &str, tag: &str) -> (Vec<TextRange>, String) { 121pub fn extract_tags(mut text: &str, tag: &str) -> (Vec<(TextRange, Option<String>)>, String) {
121 let open = format!("<{}>", tag); 122 let open = format!("<{}", tag);
122 let close = format!("</{}>", tag); 123 let close = format!("</{}>", tag);
123 let mut ranges = Vec::new(); 124 let mut ranges = Vec::new();
124 let mut res = String::new(); 125 let mut res = String::new();
@@ -133,22 +134,35 @@ pub fn extract_ranges(mut text: &str, tag: &str) -> (Vec<TextRange>, String) {
133 res.push_str(&text[..i]); 134 res.push_str(&text[..i]);
134 text = &text[i..]; 135 text = &text[i..];
135 if text.starts_with(&open) { 136 if text.starts_with(&open) {
136 text = &text[open.len()..]; 137 let close_open = text.find('>').unwrap();
138 let attr = text[open.len()..close_open].trim();
139 let attr = if attr.is_empty() { None } else { Some(attr.to_string()) };
140 text = &text[close_open + '>'.len_utf8()..];
137 let from = TextSize::of(&res); 141 let from = TextSize::of(&res);
138 stack.push(from); 142 stack.push((from, attr));
139 } else if text.starts_with(&close) { 143 } else if text.starts_with(&close) {
140 text = &text[close.len()..]; 144 text = &text[close.len()..];
141 let from = stack.pop().unwrap_or_else(|| panic!("unmatched </{}>", tag)); 145 let (from, attr) =
146 stack.pop().unwrap_or_else(|| panic!("unmatched </{}>", tag));
142 let to = TextSize::of(&res); 147 let to = TextSize::of(&res);
143 ranges.push(TextRange::new(from, to)); 148 ranges.push((TextRange::new(from, to), attr));
149 } else {
150 res.push('<');
151 text = &text['<'.len_utf8()..];
144 } 152 }
145 } 153 }
146 } 154 }
147 } 155 }
148 assert!(stack.is_empty(), "unmatched <{}>", tag); 156 assert!(stack.is_empty(), "unmatched <{}>", tag);
149 ranges.sort_by_key(|r| (r.start(), r.end())); 157 ranges.sort_by_key(|r| (r.0.start(), r.0.end()));
150 (ranges, res) 158 (ranges, res)
151} 159}
160#[test]
161fn test_extract_tags() {
162 let (tags, text) = extract_tags(r#"<tag fn>fn <tag>main</tag>() {}</tag>"#, "tag");
163 let actual = tags.into_iter().map(|(range, attr)| (&text[range], attr)).collect::<Vec<_>>();
164 assert_eq!(actual, vec![("fn main() {}", Some("fn".into())), ("main", None),]);
165}
152 166
153/// Inserts `<|>` marker into the `text` at `offset`. 167/// Inserts `<|>` marker into the `text` at `offset`.
154pub fn add_cursor(text: &str, offset: TextSize) -> String { 168pub fn add_cursor(text: &str, offset: TextSize) -> String {
@@ -168,8 +182,9 @@ pub fn extract_annotations(text: &str) -> Vec<(TextRange, String)> {
168 for line in lines_with_ends(text) { 182 for line in lines_with_ends(text) {
169 if let Some(idx) = line.find("//^") { 183 if let Some(idx) = line.find("//^") {
170 let offset = prev_line_start.unwrap() + TextSize::of(&line[..idx + "//".len()]); 184 let offset = prev_line_start.unwrap() + TextSize::of(&line[..idx + "//".len()]);
171 let data = line[idx + "//^".len()..].trim().to_string(); 185 for (line_range, text) in extract_line_annotations(&line[idx + "//".len()..]) {
172 res.push((TextRange::at(offset, 1.into()), data)) 186 res.push((line_range + offset, text))
187 }
173 } 188 }
174 prev_line_start = Some(line_start); 189 prev_line_start = Some(line_start);
175 line_start += TextSize::of(line); 190 line_start += TextSize::of(line);
@@ -177,22 +192,37 @@ pub fn extract_annotations(text: &str) -> Vec<(TextRange, String)> {
177 res 192 res
178} 193}
179 194
195fn extract_line_annotations(mut line: &str) -> Vec<(TextRange, String)> {
196 let mut res = Vec::new();
197 let mut offset: TextSize = 0.into();
198 while !line.is_empty() {
199 let len = line.chars().take_while(|&it| it == '^').count();
200 assert!(len > 0);
201 let range = TextRange::at(offset, len.try_into().unwrap());
202 let next = line[len..].find('^').map_or(line.len(), |it| it + len);
203 res.push((range, line[len..][..next - len].trim().to_string()));
204 line = &line[next..];
205 offset += TextSize::try_from(next).unwrap();
206 }
207 res
208}
209
180#[test] 210#[test]
181fn test_extract_annotations() { 211fn test_extract_annotations() {
182 let text = stdx::trim_indent( 212 let text = stdx::trim_indent(
183 r#" 213 r#"
184fn main() { 214fn main() {
185 let x = 92; 215 let (x, y) = (9, 2);
186 //^ def 216 //^ def ^ def
187 z + 1 217 zoo + 1
188} //^ i32 218} //^^^ i32
189 "#, 219 "#,
190 ); 220 );
191 let res = extract_annotations(&text) 221 let res = extract_annotations(&text)
192 .into_iter() 222 .into_iter()
193 .map(|(range, ann)| (&text[range], ann)) 223 .map(|(range, ann)| (&text[range], ann))
194 .collect::<Vec<_>>(); 224 .collect::<Vec<_>>();
195 assert_eq!(res, vec![("x", "def".into()), ("z", "i32".into()),]); 225 assert_eq!(res, vec![("x", "def".into()), ("y", "def".into()), ("zoo", "i32".into()),]);
196} 226}
197 227
198// Comparison functionality borrowed from cargo: 228// Comparison functionality borrowed from cargo:
@@ -282,85 +312,6 @@ pub fn find_mismatch<'a>(expected: &'a Value, actual: &'a Value) -> Option<(&'a
282 } 312 }
283} 313}
284 314
285/// Calls callback `f` with input code and file paths for each `.rs` file in `test_data_dir`
286/// subdirectories defined by `paths`.
287///
288/// If the content of the matching output file differs from the output of `f()`
289/// the test will fail.
290///
291/// If there is no matching output file it will be created and filled with the
292/// output of `f()`, but the test will fail.
293pub fn dir_tests<F>(test_data_dir: &Path, paths: &[&str], outfile_extension: &str, f: F)
294where
295 F: Fn(&str, &Path) -> String,
296{
297 for (path, input_code) in collect_rust_files(test_data_dir, paths) {
298 let actual = f(&input_code, &path);
299 let path = path.with_extension(outfile_extension);
300 if !path.exists() {
301 println!("\nfile: {}", path.display());
302 println!("No .txt file with expected result, creating...\n");
303 println!("{}\n{}", input_code, actual);
304 fs::write(&path, &actual).unwrap();
305 panic!("No expected result");
306 }
307 let expected = read_text(&path);
308 assert_equal_text(&expected, &actual, &path);
309 }
310}
311
312/// Collects all `.rs` files from `dir` subdirectories defined by `paths`.
313pub fn collect_rust_files(root_dir: &Path, paths: &[&str]) -> Vec<(PathBuf, String)> {
314 paths
315 .iter()
316 .flat_map(|path| {
317 let path = root_dir.to_owned().join(path);
318 rust_files_in_dir(&path).into_iter()
319 })
320 .map(|path| {
321 let text = read_text(&path);
322 (path, text)
323 })
324 .collect()
325}
326
327/// Collects paths to all `.rs` files from `dir` in a sorted `Vec<PathBuf>`.
328fn rust_files_in_dir(dir: &Path) -> Vec<PathBuf> {
329 let mut acc = Vec::new();
330 for file in fs::read_dir(&dir).unwrap() {
331 let file = file.unwrap();
332 let path = file.path();
333 if path.extension().unwrap_or_default() == "rs" {
334 acc.push(path);
335 }
336 }
337 acc.sort();
338 acc
339}
340
341/// Returns the path to the root directory of `rust-analyzer` project.
342pub fn project_dir() -> PathBuf {
343 let dir = env!("CARGO_MANIFEST_DIR");
344 PathBuf::from(dir).parent().unwrap().parent().unwrap().to_owned()
345}
346
347/// Read file and normalize newlines.
348///
349/// `rustc` seems to always normalize `\r\n` newlines to `\n`:
350///
351/// ```
352/// let s = "
353/// ";
354/// assert_eq!(s.as_bytes(), &[10]);
355/// ```
356///
357/// so this should always be correct.
358pub fn read_text(path: &Path) -> String {
359 fs::read_to_string(path)
360 .unwrap_or_else(|_| panic!("File at {:?} should be valid", path))
361 .replace("\r\n", "\n")
362}
363
364/// Returns `false` if slow tests should not run, otherwise returns `true` and 315/// Returns `false` if slow tests should not run, otherwise returns `true` and
365/// also creates a file at `./target/.slow_tests_cookie` which serves as a flag 316/// also creates a file at `./target/.slow_tests_cookie` which serves as a flag
366/// that slow tests did run. 317/// that slow tests did run.
@@ -375,25 +326,8 @@ pub fn skip_slow_tests() -> bool {
375 should_skip 326 should_skip
376} 327}
377 328
378/// Asserts that `expected` and `actual` strings are equal. If they differ only 329/// Returns the path to the root directory of `rust-analyzer` project.
379/// in trailing or leading whitespace the test won't fail and 330pub fn project_dir() -> PathBuf {
380/// the contents of `actual` will be written to the file located at `path`. 331 let dir = env!("CARGO_MANIFEST_DIR");
381fn assert_equal_text(expected: &str, actual: &str, path: &Path) { 332 PathBuf::from(dir).parent().unwrap().parent().unwrap().to_owned()
382 if expected == actual {
383 return;
384 }
385 let dir = project_dir();
386 let pretty_path = path.strip_prefix(&dir).unwrap_or_else(|_| path);
387 if expected.trim() == actual.trim() {
388 println!("whitespace difference, rewriting");
389 println!("file: {}\n", pretty_path.display());
390 fs::write(path, actual).unwrap();
391 return;
392 }
393 if env::var("UPDATE_EXPECTATIONS").is_ok() {
394 println!("rewriting {}", pretty_path.display());
395 fs::write(path, actual).unwrap();
396 return;
397 }
398 assert_eq_text!(expected, actual, "file: {}", pretty_path.display());
399} 333}