diff options
Diffstat (limited to 'crates/test_utils/src/lib.rs')
-rw-r--r-- | crates/test_utils/src/lib.rs | 142 |
1 files changed, 40 insertions, 102 deletions
diff --git a/crates/test_utils/src/lib.rs b/crates/test_utils/src/lib.rs index 5be4a64fc..c5f859790 100644 --- a/crates/test_utils/src/lib.rs +++ b/crates/test_utils/src/lib.rs | |||
@@ -6,20 +6,17 @@ | |||
6 | //! * Extracting markup (mainly, `$0` markers) out of fixture strings. | 6 | //! * Extracting markup (mainly, `$0` markers) out of fixture strings. |
7 | //! * marks (see the eponymous module). | 7 | //! * marks (see the eponymous module). |
8 | 8 | ||
9 | #[macro_use] | ||
10 | pub mod mark; | ||
11 | pub mod bench_fixture; | 9 | pub mod bench_fixture; |
12 | mod fixture; | 10 | mod fixture; |
13 | 11 | ||
14 | use std::{ | 12 | use std::{ |
15 | convert::{TryFrom, TryInto}, | 13 | convert::{TryFrom, TryInto}, |
16 | env, fs, | 14 | env, fs, |
17 | path::PathBuf, | 15 | path::{Path, PathBuf}, |
18 | }; | 16 | }; |
19 | 17 | ||
20 | use profile::StopWatch; | 18 | use profile::StopWatch; |
21 | use serde_json::Value; | 19 | use stdx::{is_ci, lines_with_ends}; |
22 | use stdx::lines_with_ends; | ||
23 | use text_size::{TextRange, TextSize}; | 20 | use text_size::{TextRange, TextSize}; |
24 | 21 | ||
25 | pub use dissimilar::diff as __diff; | 22 | pub use dissimilar::diff as __diff; |
@@ -281,101 +278,6 @@ fn main() { | |||
281 | ); | 278 | ); |
282 | } | 279 | } |
283 | 280 | ||
284 | // Comparison functionality borrowed from cargo: | ||
285 | |||
286 | /// Compare a line with an expected pattern. | ||
287 | /// - Use `[..]` as a wildcard to match 0 or more characters on the same line | ||
288 | /// (similar to `.*` in a regex). | ||
289 | pub fn lines_match(expected: &str, actual: &str) -> bool { | ||
290 | // Let's not deal with / vs \ (windows...) | ||
291 | // First replace backslash-escaped backslashes with forward slashes | ||
292 | // which can occur in, for example, JSON output | ||
293 | let expected = expected.replace(r"\\", "/").replace(r"\", "/"); | ||
294 | let mut actual: &str = &actual.replace(r"\\", "/").replace(r"\", "/"); | ||
295 | for (i, part) in expected.split("[..]").enumerate() { | ||
296 | match actual.find(part) { | ||
297 | Some(j) => { | ||
298 | if i == 0 && j != 0 { | ||
299 | return false; | ||
300 | } | ||
301 | actual = &actual[j + part.len()..]; | ||
302 | } | ||
303 | None => return false, | ||
304 | } | ||
305 | } | ||
306 | actual.is_empty() || expected.ends_with("[..]") | ||
307 | } | ||
308 | |||
309 | #[test] | ||
310 | fn lines_match_works() { | ||
311 | assert!(lines_match("a b", "a b")); | ||
312 | assert!(lines_match("a[..]b", "a b")); | ||
313 | assert!(lines_match("a[..]", "a b")); | ||
314 | assert!(lines_match("[..]", "a b")); | ||
315 | assert!(lines_match("[..]b", "a b")); | ||
316 | |||
317 | assert!(!lines_match("[..]b", "c")); | ||
318 | assert!(!lines_match("b", "c")); | ||
319 | assert!(!lines_match("b", "cb")); | ||
320 | } | ||
321 | |||
322 | /// Compares JSON object for approximate equality. | ||
323 | /// You can use `[..]` wildcard in strings (useful for OS dependent things such | ||
324 | /// as paths). You can use a `"{...}"` string literal as a wildcard for | ||
325 | /// arbitrary nested JSON. Arrays are sorted before comparison. | ||
326 | pub fn find_mismatch<'a>(expected: &'a Value, actual: &'a Value) -> Option<(&'a Value, &'a Value)> { | ||
327 | match (expected, actual) { | ||
328 | (Value::Number(l), Value::Number(r)) if l == r => None, | ||
329 | (Value::Bool(l), Value::Bool(r)) if l == r => None, | ||
330 | (Value::String(l), Value::String(r)) if lines_match(l, r) => None, | ||
331 | (Value::Array(l), Value::Array(r)) => { | ||
332 | if l.len() != r.len() { | ||
333 | return Some((expected, actual)); | ||
334 | } | ||
335 | |||
336 | let mut l = l.iter().collect::<Vec<_>>(); | ||
337 | let mut r = r.iter().collect::<Vec<_>>(); | ||
338 | |||
339 | l.retain(|l| match r.iter().position(|r| find_mismatch(l, r).is_none()) { | ||
340 | Some(i) => { | ||
341 | r.remove(i); | ||
342 | false | ||
343 | } | ||
344 | None => true, | ||
345 | }); | ||
346 | |||
347 | if !l.is_empty() { | ||
348 | assert!(!r.is_empty()); | ||
349 | Some((&l[0], &r[0])) | ||
350 | } else { | ||
351 | assert_eq!(r.len(), 0); | ||
352 | None | ||
353 | } | ||
354 | } | ||
355 | (Value::Object(l), Value::Object(r)) => { | ||
356 | fn sorted_values(obj: &serde_json::Map<String, Value>) -> Vec<&Value> { | ||
357 | let mut entries = obj.iter().collect::<Vec<_>>(); | ||
358 | entries.sort_by_key(|it| it.0); | ||
359 | entries.into_iter().map(|(_k, v)| v).collect::<Vec<_>>() | ||
360 | } | ||
361 | |||
362 | let same_keys = l.len() == r.len() && l.keys().all(|k| r.contains_key(k)); | ||
363 | if !same_keys { | ||
364 | return Some((expected, actual)); | ||
365 | } | ||
366 | |||
367 | let l = sorted_values(l); | ||
368 | let r = sorted_values(r); | ||
369 | |||
370 | l.into_iter().zip(r).filter_map(|(l, r)| find_mismatch(l, r)).next() | ||
371 | } | ||
372 | (Value::Null, Value::Null) => None, | ||
373 | // magic string literal "{...}" acts as wildcard for any sub-JSON | ||
374 | (Value::String(l), _) if l == "{...}" => None, | ||
375 | _ => Some((expected, actual)), | ||
376 | } | ||
377 | } | ||
378 | |||
379 | /// Returns `false` if slow tests should not run, otherwise returns `true` and | 281 | /// Returns `false` if slow tests should not run, otherwise returns `true` and |
380 | /// also creates a file at `./target/.slow_tests_cookie` which serves as a flag | 282 | /// also creates a file at `./target/.slow_tests_cookie` which serves as a flag |
381 | /// that slow tests did run. | 283 | /// that slow tests did run. |
@@ -384,14 +286,14 @@ pub fn skip_slow_tests() -> bool { | |||
384 | if should_skip { | 286 | if should_skip { |
385 | eprintln!("ignoring slow test") | 287 | eprintln!("ignoring slow test") |
386 | } else { | 288 | } else { |
387 | let path = project_dir().join("./target/.slow_tests_cookie"); | 289 | let path = project_root().join("./target/.slow_tests_cookie"); |
388 | fs::write(&path, ".").unwrap(); | 290 | fs::write(&path, ".").unwrap(); |
389 | } | 291 | } |
390 | should_skip | 292 | should_skip |
391 | } | 293 | } |
392 | 294 | ||
393 | /// Returns the path to the root directory of `rust-analyzer` project. | 295 | /// Returns the path to the root directory of `rust-analyzer` project. |
394 | pub fn project_dir() -> PathBuf { | 296 | pub fn project_root() -> PathBuf { |
395 | let dir = env!("CARGO_MANIFEST_DIR"); | 297 | let dir = env!("CARGO_MANIFEST_DIR"); |
396 | PathBuf::from(dir).parent().unwrap().parent().unwrap().to_owned() | 298 | PathBuf::from(dir).parent().unwrap().parent().unwrap().to_owned() |
397 | } | 299 | } |
@@ -449,3 +351,39 @@ pub fn bench(label: &'static str) -> impl Drop { | |||
449 | 351 | ||
450 | Bencher { sw: StopWatch::start(), label } | 352 | Bencher { sw: StopWatch::start(), label } |
451 | } | 353 | } |
354 | |||
355 | /// Checks that the `file` has the specified `contents`. If that is not the | ||
356 | /// case, updates the file and then fails the test. | ||
357 | pub fn ensure_file_contents(file: &Path, contents: &str) { | ||
358 | if let Err(()) = try_ensure_file_contents(file, contents) { | ||
359 | panic!("Some files were not up-to-date"); | ||
360 | } | ||
361 | } | ||
362 | |||
363 | /// Checks that the `file` has the specified `contents`. If that is not the | ||
364 | /// case, updates the file and return an Error. | ||
365 | pub fn try_ensure_file_contents(file: &Path, contents: &str) -> Result<(), ()> { | ||
366 | match std::fs::read_to_string(file) { | ||
367 | Ok(old_contents) if normalize_newlines(&old_contents) == normalize_newlines(contents) => { | ||
368 | return Ok(()) | ||
369 | } | ||
370 | _ => (), | ||
371 | } | ||
372 | let display_path = file.strip_prefix(&project_root()).unwrap_or(file); | ||
373 | eprintln!( | ||
374 | "\n\x1b[31;1merror\x1b[0m: {} was not up-to-date, updating\n", | ||
375 | display_path.display() | ||
376 | ); | ||
377 | if is_ci() { | ||
378 | eprintln!(" NOTE: run `cargo test` locally and commit the updated files\n"); | ||
379 | } | ||
380 | if let Some(parent) = file.parent() { | ||
381 | let _ = std::fs::create_dir_all(parent); | ||
382 | } | ||
383 | std::fs::write(file, contents).unwrap(); | ||
384 | Err(()) | ||
385 | } | ||
386 | |||
387 | fn normalize_newlines(s: &str) -> String { | ||
388 | s.replace("\r\n", "\n") | ||
389 | } | ||