Skip to content

Commit db99ddc

Browse files
authored
test(rustfix): Use snapbox for snapshot testing (#15429)
### What does this PR try to resolve? - separates each test into different test cases - `snapbox` is used to test the snapshots - difference in `.json` file alone should never cause a test to fail - `.json` files updated only if expected fix != actual fix && `SNAPSHOTS=overwrite` - when `.json` files are updated, the json is pretty printed - replaced environment variables `RUSTFIX_TEST_*` for overwriting test snapshots with `SNAPSHOTS=overwrite` - ❗ The `RUSTFIX_TEST_RECORD_FIXED_RUST` feature is removed (generate a `*.fixed.rs` on demand`). We can add it back whenever needed. Fixes #13891 ### How should we test and review this PR? Run tests with: ```sh cargo test -p rustfix ``` All the test should run as different test cases nightly tests run only when using nightly version of rustc is used
2 parents c6b777d + 2c4f3e4 commit db99ddc

File tree

6 files changed

+80
-164
lines changed

6 files changed

+80
-164
lines changed

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rustfix/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rustfix"
3-
version = "0.9.0"
3+
version = "0.9.1"
44
authors = [
55
"Pascal Hertleif <[email protected]>",
66
"Oliver Schneider <[email protected]>",
@@ -29,6 +29,7 @@ proptest.workspace = true
2929
similar.workspace = true
3030
tempfile.workspace = true
3131
tracing-subscriber.workspace = true
32+
snapbox.workspace = true
3233

3334
[lints]
3435
workspace = true

crates/rustfix/tests/parse_and_replace.rs

Lines changed: 76 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,30 @@
77
//! compares the result with the corresponding `.fixed.rs` file. If they don't
88
//! match, then the test fails.
99
//!
10-
//! There are several debugging environment variables for this test that you can set:
10+
//! The files ending in `.nightly.rs` will run only on the nightly toolchain
1111
//!
12-
//! - `RUST_LOG=parse_and_replace=debug`: Print debug information.
13-
//! - `RUSTFIX_TEST_BLESS=test-name.rs`: When given the name of a test, this
14-
//! will overwrite the `.json` and `.fixed.rs` files with the expected
15-
//! values. This can be used when adding a new test.
16-
//! - `RUSTFIX_TEST_RECORD_JSON=1`: Records the JSON output to
17-
//! `*.recorded.json` files. You can then move that to `.json` or whatever
18-
//! you need.
19-
//! - `RUSTFIX_TEST_RECORD_FIXED_RUST=1`: Records the fixed result to
20-
//! `*.recorded.rs` files. You can then move that to `.rs` or whatever you
21-
//! need.
12+
//! To override snapshots, run `SNAPSHOTS=overwrite cargo test`.
13+
//! See [`snapbox::assert::Action`] for different actions.
2214
2315
#![allow(clippy::disallowed_methods, clippy::print_stdout, clippy::print_stderr)]
2416

25-
use anyhow::{anyhow, ensure, Context, Error};
17+
use anyhow::{anyhow, Context, Error};
2618
use rustfix::apply_suggestions;
19+
use serde_json::Value;
20+
use snapbox::data::DataFormat;
21+
use snapbox::{Assert, Data};
2722
use std::collections::HashSet;
2823
use std::env;
2924
use std::ffi::OsString;
3025
use std::fs;
31-
use std::path::{Path, PathBuf};
26+
use std::path::Path;
3227
use std::process::{Command, Output};
3328
use tempfile::tempdir;
34-
use tracing::{debug, info, warn};
3529

3630
mod fixmode {
3731
pub const EVERYTHING: &str = "yolo";
3832
}
3933

40-
mod settings {
41-
// can be set as env var to debug
42-
pub const CHECK_JSON: &str = "RUSTFIX_TEST_CHECK_JSON";
43-
pub const RECORD_JSON: &str = "RUSTFIX_TEST_RECORD_JSON";
44-
pub const RECORD_FIXED_RUST: &str = "RUSTFIX_TEST_RECORD_FIXED_RUST";
45-
pub const BLESS: &str = "RUSTFIX_TEST_BLESS";
46-
}
47-
4834
static mut VERSION: (u32, bool) = (0, false);
4935

5036
// Temporarily copy from `cargo_test_macro::version`.
@@ -108,170 +94,98 @@ fn compiles_without_errors(file: &Path) -> Result<(), Error> {
10894

10995
match res.status.code() {
11096
Some(0) => Ok(()),
111-
_ => {
112-
info!(
113-
"file {:?} failed to compile:\n{}",
114-
file,
115-
String::from_utf8(res.stderr)?
116-
);
117-
Err(anyhow!(
118-
"failed with status {:?} (`env RUST_LOG=parse_and_replace=info` for more info)",
119-
res.status.code(),
120-
))
121-
}
122-
}
123-
}
124-
125-
fn diff(expected: &str, actual: &str) -> String {
126-
use similar::{ChangeTag, TextDiff};
127-
use std::fmt::Write;
128-
129-
let mut res = String::new();
130-
let diff = TextDiff::from_lines(expected.trim(), actual.trim());
131-
132-
let mut different = false;
133-
for op in diff.ops() {
134-
for change in diff.iter_changes(op) {
135-
let prefix = match change.tag() {
136-
ChangeTag::Equal => continue,
137-
ChangeTag::Insert => "+",
138-
ChangeTag::Delete => "-",
139-
};
140-
if !different {
141-
writeln!(&mut res, "differences found (+ == actual, - == expected):").unwrap();
142-
different = true;
143-
}
144-
write!(&mut res, "{} {}", prefix, change.value()).unwrap();
145-
}
146-
}
147-
if different {
148-
write!(&mut res, "").unwrap();
97+
_ => Err(anyhow!(
98+
"file {:?} failed compile with status {:?}:\n {}",
99+
file,
100+
res.status.code(),
101+
String::from_utf8(res.stderr)?
102+
)),
149103
}
150-
151-
res
152104
}
153105

154-
fn test_rustfix_with_file<P: AsRef<Path>>(file: P, mode: &str) -> Result<(), Error> {
106+
fn test_rustfix_with_file<P: AsRef<Path>>(file: P, mode: &str) {
155107
let file: &Path = file.as_ref();
156108
let json_file = file.with_extension("json");
157-
let fixed_file = file.with_extension("fixed.rs");
109+
let expected_fixed_file = file.with_extension("fixed.rs");
158110

159111
let filter_suggestions = if mode == fixmode::EVERYTHING {
160112
rustfix::Filter::Everything
161113
} else {
162114
rustfix::Filter::MachineApplicableOnly
163115
};
164116

165-
debug!("next up: {:?}", file);
166-
let code = fs::read_to_string(file)?;
167-
let errors = compile_and_get_json_errors(file)
168-
.with_context(|| format!("could not compile {}", file.display()))?;
169-
let suggestions =
170-
rustfix::get_suggestions_from_json(&errors, &HashSet::new(), filter_suggestions)
171-
.context("could not load suggestions")?;
172-
173-
if std::env::var(settings::RECORD_JSON).is_ok() {
174-
fs::write(file.with_extension("recorded.json"), &errors)?;
175-
}
117+
let code = fs::read_to_string(file).unwrap();
176118

177-
if std::env::var(settings::CHECK_JSON).is_ok() {
178-
let expected_json = fs::read_to_string(&json_file)
179-
.with_context(|| format!("could not load json fixtures for {}", file.display()))?;
180-
let expected_suggestions =
181-
rustfix::get_suggestions_from_json(&expected_json, &HashSet::new(), filter_suggestions)
182-
.context("could not load expected suggestions")?;
119+
let json = compile_and_get_json_errors(file)
120+
.with_context(|| format!("could not compile {}", file.display()))
121+
.unwrap();
183122

184-
ensure!(
185-
expected_suggestions == suggestions,
186-
"got unexpected suggestions from clippy:\n{}",
187-
diff(
188-
&format!("{:?}", expected_suggestions),
189-
&format!("{:?}", suggestions)
190-
)
191-
);
192-
}
123+
let suggestions =
124+
rustfix::get_suggestions_from_json(&json, &HashSet::new(), filter_suggestions)
125+
.context("could not load suggestions")
126+
.unwrap();
193127

194128
let fixed = apply_suggestions(&code, &suggestions)
195-
.with_context(|| format!("could not apply suggestions to {}", file.display()))?
129+
.with_context(|| format!("could not apply suggestions to {}", file.display()))
130+
.unwrap()
196131
.replace('\r', "");
197132

198-
if std::env::var(settings::RECORD_FIXED_RUST).is_ok() {
199-
fs::write(file.with_extension("recorded.rs"), &fixed)?;
200-
}
201-
202-
if let Some(bless_name) = std::env::var_os(settings::BLESS) {
203-
if bless_name == file.file_name().unwrap() {
204-
std::fs::write(&json_file, &errors)?;
205-
std::fs::write(&fixed_file, &fixed)?;
206-
}
207-
}
208-
209-
let expected_fixed = fs::read_to_string(&fixed_file)
210-
.with_context(|| format!("could read fixed file for {}", file.display()))?
211-
.replace('\r', "");
212-
ensure!(
213-
fixed.trim() == expected_fixed.trim(),
214-
"file {} doesn't look fixed:\n{}",
215-
file.display(),
216-
diff(fixed.trim(), expected_fixed.trim())
133+
let assert = Assert::new().action_env(snapbox::assert::DEFAULT_ACTION_ENV);
134+
let (actual_fix, expected_fix) = assert.normalize(
135+
Data::text(&fixed),
136+
Data::read_from(expected_fixed_file.as_path(), Some(DataFormat::Text)),
217137
);
218138

219-
compiles_without_errors(&fixed_file)?;
220-
221-
Ok(())
222-
}
139+
if actual_fix != expected_fix {
140+
let fixed_assert = assert.try_eq(Some(&"Current Fix"), actual_fix, expected_fix);
141+
assert!(fixed_assert.is_ok(), "{}", fixed_assert.err().unwrap());
142+
143+
let expected_json = Data::read_from(json_file.as_path(), Some(DataFormat::Text));
144+
145+
let pretty_json = json
146+
.split("\n")
147+
.filter(|j| !j.is_empty())
148+
.map(|j| {
149+
serde_json::to_string_pretty(&serde_json::from_str::<Value>(j).unwrap()).unwrap()
150+
})
151+
.collect::<Vec<String>>()
152+
.join("\n");
153+
154+
let json_assert = assert.try_eq(
155+
Some(&"Compiler Error"),
156+
Data::text(pretty_json),
157+
expected_json,
158+
);
159+
assert!(json_assert.is_ok(), "{}", json_assert.err().unwrap());
160+
}
223161

224-
fn get_fixture_files(p: &str) -> Result<Vec<PathBuf>, Error> {
225-
Ok(fs::read_dir(p)?
226-
.map(|e| e.unwrap().path())
227-
.filter(|p| p.is_file())
228-
.filter(|p| {
229-
let x = p.to_string_lossy();
230-
x.ends_with(".rs") && !x.ends_with(".fixed.rs") && !x.ends_with(".recorded.rs")
231-
})
232-
.collect())
162+
compiles_without_errors(&expected_fixed_file).unwrap();
233163
}
234164

235-
fn assert_fixtures(dir: &str, mode: &str) {
236-
let files = get_fixture_files(dir)
237-
.with_context(|| format!("couldn't load dir `{dir}`"))
238-
.unwrap();
239-
let mut failures = 0;
240-
241-
let is_not_nightly = !version().1;
242-
243-
for file in &files {
244-
if file
245-
.file_stem()
246-
.unwrap()
247-
.to_str()
248-
.unwrap()
249-
.ends_with(".nightly")
250-
&& is_not_nightly
251-
{
252-
info!("skipped: {file:?}");
253-
continue;
254-
}
255-
if let Err(err) = test_rustfix_with_file(file, mode) {
256-
println!("failed: {}", file.display());
257-
warn!("{:?}", err);
258-
failures += 1;
165+
macro_rules! run_test {
166+
($name:ident, $file:expr) => {
167+
#[test]
168+
#[allow(non_snake_case)]
169+
fn $name() {
170+
let (_, nightly) = version();
171+
if !$file.ends_with(".nightly.rs") || nightly {
172+
let file = Path::new(concat!("./tests/everything/", $file));
173+
assert!(file.is_file(), "could not load {}", $file);
174+
test_rustfix_with_file(file, fixmode::EVERYTHING);
175+
}
259176
}
260-
info!("passed: {:?}", file);
261-
}
262-
263-
if failures > 0 {
264-
panic!(
265-
"{} out of {} fixture asserts failed\n\
266-
(run with `env RUST_LOG=parse_and_replace=info` to get more details)",
267-
failures,
268-
files.len(),
269-
);
270-
}
177+
};
271178
}
272179

273-
#[test]
274-
fn everything() {
275-
tracing_subscriber::fmt::init();
276-
assert_fixtures("./tests/everything", fixmode::EVERYTHING);
180+
run_test! {
181+
closure_immutable_outer_variable,
182+
"closure-immutable-outer-variable.rs"
277183
}
184+
run_test! {dedup_suggestions, "dedup-suggestions.rs"}
185+
run_test! {E0178, "E0178.rs"}
186+
run_test! {handle_insert_only, "handle-insert-only.rs"}
187+
run_test! {lt_generic_comp, "lt-generic-comp.rs"}
188+
run_test! {multiple_solutions, "multiple-solutions.rs"}
189+
run_test! {replace_only_one_char, "replace-only-one-char.rs"}
190+
run_test! {str_lit_type_mismatch, "str-lit-type-mismatch.rs"}
191+
run_test! {use_insert, "use-insert.rs"}

0 commit comments

Comments
 (0)