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.rs142
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]
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}