Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 99 additions & 1 deletion crates/ra_syntax/src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use std::{
env,
fmt::Write,
fs,
path::{Component, Path, PathBuf},
};

use test_utils::{collect_rust_files, dir_tests, project_dir, read_text};
use test_utils::{assert_eq_text, project_dir};

use crate::{fuzz, tokenize, SourceFile, SyntaxError, TextRange, TextSize, Token};

Expand Down Expand Up @@ -200,3 +202,99 @@ where
}
});
}

/// Calls callback `f` with input code and file paths for each `.rs` file in `test_data_dir`
/// subdirectories defined by `paths`.
///
/// If the content of the matching output file differs from the output of `f()`
/// the test will fail.
///
/// If there is no matching output file it will be created and filled with the
/// output of `f()`, but the test will fail.
fn dir_tests<F>(test_data_dir: &Path, paths: &[&str], outfile_extension: &str, f: F)
where
F: Fn(&str, &Path) -> String,
{
for (path, input_code) in collect_rust_files(test_data_dir, paths) {
let actual = f(&input_code, &path);
let path = path.with_extension(outfile_extension);
if !path.exists() {
println!("\nfile: {}", path.display());
println!("No .txt file with expected result, creating...\n");
println!("{}\n{}", input_code, actual);
fs::write(&path, &actual).unwrap();
panic!("No expected result");
}
let expected = read_text(&path);
assert_equal_text(&expected, &actual, &path);
}
}

/// Collects all `.rs` files from `dir` subdirectories defined by `paths`.
fn collect_rust_files(root_dir: &Path, paths: &[&str]) -> Vec<(PathBuf, String)> {
paths
.iter()
.flat_map(|path| {
let path = root_dir.to_owned().join(path);
rust_files_in_dir(&path).into_iter()
})
.map(|path| {
let text = read_text(&path);
(path, text)
})
.collect()
}

/// Collects paths to all `.rs` files from `dir` in a sorted `Vec<PathBuf>`.
fn rust_files_in_dir(dir: &Path) -> Vec<PathBuf> {
let mut acc = Vec::new();
for file in fs::read_dir(&dir).unwrap() {
let file = file.unwrap();
let path = file.path();
if path.extension().unwrap_or_default() == "rs" {
acc.push(path);
}
}
acc.sort();
acc
}

/// Asserts that `expected` and `actual` strings are equal. If they differ only
/// in trailing or leading whitespace the test won't fail and
/// the contents of `actual` will be written to the file located at `path`.
fn assert_equal_text(expected: &str, actual: &str, path: &Path) {
if expected == actual {
return;
}
let dir = project_dir();
let pretty_path = path.strip_prefix(&dir).unwrap_or_else(|_| path);
if expected.trim() == actual.trim() {
println!("whitespace difference, rewriting");
println!("file: {}\n", pretty_path.display());
fs::write(path, actual).unwrap();
return;
}
if env::var("UPDATE_EXPECT").is_ok() {
println!("rewriting {}", pretty_path.display());
fs::write(path, actual).unwrap();
return;
}
assert_eq_text!(expected, actual, "file: {}", pretty_path.display());
}

/// Read file and normalize newlines.
///
/// `rustc` seems to always normalize `\r\n` newlines to `\n`:
///
/// ```
/// let s = "
/// ";
/// assert_eq!(s.as_bytes(), &[10]);
/// ```
///
/// so this should always be correct.
fn read_text(path: &Path) -> String {
fs::read_to_string(path)
.unwrap_or_else(|_| panic!("File at {:?} should be valid", path))
.replace("\r\n", "\n")
}
106 changes: 5 additions & 101 deletions crates/test_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ mod fixture;
use std::{
convert::{TryFrom, TryInto},
env, fs,
path::{Path, PathBuf},
path::PathBuf,
};

use serde_json::Value;
Expand Down Expand Up @@ -299,85 +299,6 @@ pub fn find_mismatch<'a>(expected: &'a Value, actual: &'a Value) -> Option<(&'a
}
}

/// Calls callback `f` with input code and file paths for each `.rs` file in `test_data_dir`
/// subdirectories defined by `paths`.
///
/// If the content of the matching output file differs from the output of `f()`
/// the test will fail.
///
/// If there is no matching output file it will be created and filled with the
/// output of `f()`, but the test will fail.
pub fn dir_tests<F>(test_data_dir: &Path, paths: &[&str], outfile_extension: &str, f: F)
where
F: Fn(&str, &Path) -> String,
{
for (path, input_code) in collect_rust_files(test_data_dir, paths) {
let actual = f(&input_code, &path);
let path = path.with_extension(outfile_extension);
if !path.exists() {
println!("\nfile: {}", path.display());
println!("No .txt file with expected result, creating...\n");
println!("{}\n{}", input_code, actual);
fs::write(&path, &actual).unwrap();
panic!("No expected result");
}
let expected = read_text(&path);
assert_equal_text(&expected, &actual, &path);
}
}

/// Collects all `.rs` files from `dir` subdirectories defined by `paths`.
pub fn collect_rust_files(root_dir: &Path, paths: &[&str]) -> Vec<(PathBuf, String)> {
paths
.iter()
.flat_map(|path| {
let path = root_dir.to_owned().join(path);
rust_files_in_dir(&path).into_iter()
})
.map(|path| {
let text = read_text(&path);
(path, text)
})
.collect()
}

/// Collects paths to all `.rs` files from `dir` in a sorted `Vec<PathBuf>`.
fn rust_files_in_dir(dir: &Path) -> Vec<PathBuf> {
let mut acc = Vec::new();
for file in fs::read_dir(&dir).unwrap() {
let file = file.unwrap();
let path = file.path();
if path.extension().unwrap_or_default() == "rs" {
acc.push(path);
}
}
acc.sort();
acc
}

/// Returns the path to the root directory of `rust-analyzer` project.
pub fn project_dir() -> PathBuf {
let dir = env!("CARGO_MANIFEST_DIR");
PathBuf::from(dir).parent().unwrap().parent().unwrap().to_owned()
}

/// Read file and normalize newlines.
///
/// `rustc` seems to always normalize `\r\n` newlines to `\n`:
///
/// ```
/// let s = "
/// ";
/// assert_eq!(s.as_bytes(), &[10]);
/// ```
///
/// so this should always be correct.
pub fn read_text(path: &Path) -> String {
fs::read_to_string(path)
.unwrap_or_else(|_| panic!("File at {:?} should be valid", path))
.replace("\r\n", "\n")
}

/// Returns `false` if slow tests should not run, otherwise returns `true` and
/// also creates a file at `./target/.slow_tests_cookie` which serves as a flag
/// that slow tests did run.
Expand All @@ -392,25 +313,8 @@ pub fn skip_slow_tests() -> bool {
should_skip
}

/// Asserts that `expected` and `actual` strings are equal. If they differ only
/// in trailing or leading whitespace the test won't fail and
/// the contents of `actual` will be written to the file located at `path`.
fn assert_equal_text(expected: &str, actual: &str, path: &Path) {
if expected == actual {
return;
}
let dir = project_dir();
let pretty_path = path.strip_prefix(&dir).unwrap_or_else(|_| path);
if expected.trim() == actual.trim() {
println!("whitespace difference, rewriting");
println!("file: {}\n", pretty_path.display());
fs::write(path, actual).unwrap();
return;
}
if env::var("UPDATE_EXPECTATIONS").is_ok() {
println!("rewriting {}", pretty_path.display());
fs::write(path, actual).unwrap();
return;
}
assert_eq_text!(expected, actual, "file: {}", pretty_path.display());
/// Returns the path to the root directory of `rust-analyzer` project.
pub fn project_dir() -> PathBuf {
let dir = env!("CARGO_MANIFEST_DIR");
PathBuf::from(dir).parent().unwrap().parent().unwrap().to_owned()
}
4 changes: 2 additions & 2 deletions docs/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,10 +361,10 @@ There are two kinds of tests:
The purpose of inline tests is not to achieve full coverage by test cases, but to explain to the reader of the code what each particular `if` and `match` is responsible for.
If you are tempted to add a large inline test, it might be a good idea to leave only the simplest example in place, and move the test to a manual `parser/ok` test.

To update test data, run with `UPDATE_EXPECTATIONS` variable:
To update test data, run with `UPDATE_EXPECT` variable:

```bash
env UPDATE_EXPECTATIONS=1 cargo qt
env UPDATE_EXPECT=1 cargo qt
```

After adding a new inline test you need to run `cargo xtest codegen` and also update the test data as described above.
Expand Down