diff options
Diffstat (limited to 'crates/test_utils')
-rw-r--r-- | crates/test_utils/Cargo.toml | 1 | ||||
-rw-r--r-- | crates/test_utils/src/bench_fixture.rs | 6 | ||||
-rw-r--r-- | crates/test_utils/src/lib.rs | 142 | ||||
-rw-r--r-- | crates/test_utils/src/mark.rs | 78 |
4 files changed, 43 insertions, 184 deletions
diff --git a/crates/test_utils/Cargo.toml b/crates/test_utils/Cargo.toml index 2a65000b8..87bab7a08 100644 --- a/crates/test_utils/Cargo.toml +++ b/crates/test_utils/Cargo.toml | |||
@@ -13,7 +13,6 @@ doctest = false | |||
13 | # Avoid adding deps here, this crate is widely used in tests it should compile fast! | 13 | # Avoid adding deps here, this crate is widely used in tests it should compile fast! |
14 | dissimilar = "1.0.2" | 14 | dissimilar = "1.0.2" |
15 | text-size = "1.0.0" | 15 | text-size = "1.0.0" |
16 | serde_json = "1.0.48" | ||
17 | rustc-hash = "1.1.0" | 16 | rustc-hash = "1.1.0" |
18 | 17 | ||
19 | stdx = { path = "../stdx", version = "0.0.0" } | 18 | stdx = { path = "../stdx", version = "0.0.0" } |
diff --git a/crates/test_utils/src/bench_fixture.rs b/crates/test_utils/src/bench_fixture.rs index d775e2cc9..3a37c4473 100644 --- a/crates/test_utils/src/bench_fixture.rs +++ b/crates/test_utils/src/bench_fixture.rs | |||
@@ -4,7 +4,7 @@ use std::fs; | |||
4 | 4 | ||
5 | use stdx::format_to; | 5 | use stdx::format_to; |
6 | 6 | ||
7 | use crate::project_dir; | 7 | use crate::project_root; |
8 | 8 | ||
9 | pub fn big_struct() -> String { | 9 | pub fn big_struct() -> String { |
10 | let n = 1_000; | 10 | let n = 1_000; |
@@ -32,11 +32,11 @@ struct S{} {{ | |||
32 | } | 32 | } |
33 | 33 | ||
34 | pub fn glorious_old_parser() -> String { | 34 | pub fn glorious_old_parser() -> String { |
35 | let path = project_dir().join("bench_data/glorious_old_parser"); | 35 | let path = project_root().join("bench_data/glorious_old_parser"); |
36 | fs::read_to_string(&path).unwrap() | 36 | fs::read_to_string(&path).unwrap() |
37 | } | 37 | } |
38 | 38 | ||
39 | pub fn numerous_macro_rules() -> String { | 39 | pub fn numerous_macro_rules() -> String { |
40 | let path = project_dir().join("bench_data/numerous_macro_rules"); | 40 | let path = project_root().join("bench_data/numerous_macro_rules"); |
41 | fs::read_to_string(&path).unwrap() | 41 | fs::read_to_string(&path).unwrap() |
42 | } | 42 | } |
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 | } | ||
diff --git a/crates/test_utils/src/mark.rs b/crates/test_utils/src/mark.rs deleted file mode 100644 index 97f5a93ad..000000000 --- a/crates/test_utils/src/mark.rs +++ /dev/null | |||
@@ -1,78 +0,0 @@ | |||
1 | //! This module implements manually tracked test coverage, which is useful for | ||
2 | //! quickly finding a test responsible for testing a particular bit of code. | ||
3 | //! | ||
4 | //! See <https://matklad.github.io/2018/06/18/a-trick-for-test-maintenance.html> | ||
5 | //! for details, but the TL;DR is that you write your test as | ||
6 | //! | ||
7 | //! ``` | ||
8 | //! #[test] | ||
9 | //! fn test_foo() { | ||
10 | //! mark::check!(test_foo); | ||
11 | //! } | ||
12 | //! ``` | ||
13 | //! | ||
14 | //! and in the code under test you write | ||
15 | //! | ||
16 | //! ``` | ||
17 | //! # use test_utils::mark; | ||
18 | //! # fn some_condition() -> bool { true } | ||
19 | //! fn foo() { | ||
20 | //! if some_condition() { | ||
21 | //! mark::hit!(test_foo); | ||
22 | //! } | ||
23 | //! } | ||
24 | //! ``` | ||
25 | //! | ||
26 | //! This module then checks that executing the test indeed covers the specified | ||
27 | //! function. This is useful if you come back to the `foo` function ten years | ||
28 | //! later and wonder where the test are: now you can grep for `test_foo`. | ||
29 | use std::sync::atomic::{AtomicUsize, Ordering}; | ||
30 | |||
31 | #[macro_export] | ||
32 | macro_rules! _hit { | ||
33 | ($ident:ident) => {{ | ||
34 | #[cfg(test)] | ||
35 | { | ||
36 | extern "C" { | ||
37 | #[no_mangle] | ||
38 | static $ident: std::sync::atomic::AtomicUsize; | ||
39 | } | ||
40 | unsafe { | ||
41 | $ident.fetch_add(1, std::sync::atomic::Ordering::SeqCst); | ||
42 | } | ||
43 | } | ||
44 | }}; | ||
45 | } | ||
46 | pub use _hit as hit; | ||
47 | |||
48 | #[macro_export] | ||
49 | macro_rules! _check { | ||
50 | ($ident:ident) => { | ||
51 | #[no_mangle] | ||
52 | static $ident: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); | ||
53 | let _checker = $crate::mark::MarkChecker::new(&$ident); | ||
54 | }; | ||
55 | } | ||
56 | pub use _check as check; | ||
57 | |||
58 | pub struct MarkChecker { | ||
59 | mark: &'static AtomicUsize, | ||
60 | value_on_entry: usize, | ||
61 | } | ||
62 | |||
63 | impl MarkChecker { | ||
64 | pub fn new(mark: &'static AtomicUsize) -> MarkChecker { | ||
65 | let value_on_entry = mark.load(Ordering::Relaxed); | ||
66 | MarkChecker { mark, value_on_entry } | ||
67 | } | ||
68 | } | ||
69 | |||
70 | impl Drop for MarkChecker { | ||
71 | fn drop(&mut self) { | ||
72 | if std::thread::panicking() { | ||
73 | return; | ||
74 | } | ||
75 | let value_on_exit = self.mark.load(Ordering::Relaxed); | ||
76 | assert!(value_on_exit > self.value_on_entry, "mark was not hit") | ||
77 | } | ||
78 | } | ||