|
7 | 7 | //! compares the result with the corresponding `.fixed.rs` file. If they don't |
8 | 8 | //! match, then the test fails. |
9 | 9 | //! |
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 |
11 | 11 | //! |
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. |
22 | 14 |
|
23 | 15 | #![allow(clippy::disallowed_methods, clippy::print_stdout, clippy::print_stderr)] |
24 | 16 |
|
25 | | -use anyhow::{anyhow, ensure, Context, Error}; |
| 17 | +use anyhow::{anyhow, Context, Error}; |
26 | 18 | use rustfix::apply_suggestions; |
| 19 | +use serde_json::Value; |
| 20 | +use snapbox::data::DataFormat; |
| 21 | +use snapbox::{Assert, Data}; |
27 | 22 | use std::collections::HashSet; |
28 | 23 | use std::env; |
29 | 24 | use std::ffi::OsString; |
30 | 25 | use std::fs; |
31 | | -use std::path::{Path, PathBuf}; |
| 26 | +use std::path::Path; |
32 | 27 | use std::process::{Command, Output}; |
33 | 28 | use tempfile::tempdir; |
34 | | -use tracing::{debug, info, warn}; |
35 | 29 |
|
36 | 30 | mod fixmode { |
37 | 31 | pub const EVERYTHING: &str = "yolo"; |
38 | 32 | } |
39 | 33 |
|
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 | | - |
48 | 34 | static mut VERSION: (u32, bool) = (0, false); |
49 | 35 |
|
50 | 36 | // Temporarily copy from `cargo_test_macro::version`. |
@@ -108,170 +94,98 @@ fn compiles_without_errors(file: &Path) -> Result<(), Error> { |
108 | 94 |
|
109 | 95 | match res.status.code() { |
110 | 96 | 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 | + )), |
149 | 103 | } |
150 | | - |
151 | | - res |
152 | 104 | } |
153 | 105 |
|
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) { |
155 | 107 | let file: &Path = file.as_ref(); |
156 | 108 | 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"); |
158 | 110 |
|
159 | 111 | let filter_suggestions = if mode == fixmode::EVERYTHING { |
160 | 112 | rustfix::Filter::Everything |
161 | 113 | } else { |
162 | 114 | rustfix::Filter::MachineApplicableOnly |
163 | 115 | }; |
164 | 116 |
|
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(); |
176 | 118 |
|
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(); |
183 | 122 |
|
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(); |
193 | 127 |
|
194 | 128 | 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() |
196 | 131 | .replace('\r', ""); |
197 | 132 |
|
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)), |
217 | 137 | ); |
218 | 138 |
|
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 | + } |
223 | 161 |
|
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(); |
233 | 163 | } |
234 | 164 |
|
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 | + } |
259 | 176 | } |
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 | + }; |
271 | 178 | } |
272 | 179 |
|
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" |
277 | 183 | } |
| 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