aboutsummaryrefslogtreecommitdiff
path: root/crates/test_utils
diff options
context:
space:
mode:
Diffstat (limited to 'crates/test_utils')
-rw-r--r--crates/test_utils/Cargo.toml1
-rw-r--r--crates/test_utils/src/bench_fixture.rs6
-rw-r--r--crates/test_utils/src/lib.rs142
-rw-r--r--crates/test_utils/src/mark.rs78
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!
14dissimilar = "1.0.2" 14dissimilar = "1.0.2"
15text-size = "1.0.0" 15text-size = "1.0.0"
16serde_json = "1.0.48"
17rustc-hash = "1.1.0" 16rustc-hash = "1.1.0"
18 17
19stdx = { path = "../stdx", version = "0.0.0" } 18stdx = { 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
5use stdx::format_to; 5use stdx::format_to;
6 6
7use crate::project_dir; 7use crate::project_root;
8 8
9pub fn big_struct() -> String { 9pub fn big_struct() -> String {
10 let n = 1_000; 10 let n = 1_000;
@@ -32,11 +32,11 @@ struct S{} {{
32} 32}
33 33
34pub fn glorious_old_parser() -> String { 34pub 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
39pub fn numerous_macro_rules() -> String { 39pub 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]
10pub mod mark;
11pub mod bench_fixture; 9pub mod bench_fixture;
12mod fixture; 10mod fixture;
13 11
14use std::{ 12use 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
20use profile::StopWatch; 18use profile::StopWatch;
21use serde_json::Value; 19use stdx::{is_ci, lines_with_ends};
22use stdx::lines_with_ends;
23use text_size::{TextRange, TextSize}; 20use text_size::{TextRange, TextSize};
24 21
25pub use dissimilar::diff as __diff; 22pub 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).
289pub 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]
310fn 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.
326pub 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.
394pub fn project_dir() -> PathBuf { 296pub 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.
357pub 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.
365pub 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
387fn 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`.
29use std::sync::atomic::{AtomicUsize, Ordering};
30
31#[macro_export]
32macro_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}
46pub use _hit as hit;
47
48#[macro_export]
49macro_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}
56pub use _check as check;
57
58pub struct MarkChecker {
59 mark: &'static AtomicUsize,
60 value_on_entry: usize,
61}
62
63impl 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
70impl 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}