| //! Tests that verify rustfix applies the appropriate changes to a file. |
| //! |
| //! This test works by reading a series of `*.rs` files in the |
| //! `tests/everything` directory. For each `.rs` file, it runs `rustc` to |
| //! collect JSON diagnostics from the file. It feeds that JSON data into |
| //! rustfix and applies the recommended suggestions to the `.rs` file. It then |
| //! compares the result with the corresponding `.fixed.rs` file. If they don't |
| //! match, then the test fails. |
| //! |
| //! The files ending in `.nightly.rs` will run only on the nightly toolchain |
| //! |
| //! To override snapshots, run `SNAPSHOTS=overwrite cargo test`. |
| //! See [`snapbox::assert::Action`] for different actions. |
| |
| #![allow(clippy::disallowed_methods, clippy::print_stdout, clippy::print_stderr)] |
| |
| use anyhow::{anyhow, Context, Error}; |
| use rustfix::apply_suggestions; |
| use serde_json::Value; |
| use snapbox::data::DataFormat; |
| use snapbox::{Assert, Data}; |
| use std::collections::HashSet; |
| use std::env; |
| use std::ffi::OsString; |
| use std::fs; |
| use std::path::Path; |
| use std::process::{Command, Output}; |
| use tempfile::tempdir; |
| |
| mod fixmode { |
| pub const EVERYTHING: &str = "yolo"; |
| } |
| |
| static mut VERSION: (u32, bool) = (0, false); |
| |
| // Temporarily copy from `cargo_test_macro::version`. |
| fn version() -> (u32, bool) { |
| static INIT: std::sync::Once = std::sync::Once::new(); |
| INIT.call_once(|| { |
| let output = Command::new("rustc") |
| .arg("-V") |
| .output() |
| .expect("cargo should run"); |
| let stdout = std::str::from_utf8(&output.stdout).expect("utf8"); |
| let vers = stdout.split_whitespace().skip(1).next().unwrap(); |
| let is_nightly = option_env!("CARGO_TEST_DISABLE_NIGHTLY").is_none() |
| && (vers.contains("-nightly") || vers.contains("-dev")); |
| let minor = vers.split('.').skip(1).next().unwrap().parse().unwrap(); |
| unsafe { VERSION = (minor, is_nightly) } |
| }); |
| unsafe { VERSION } |
| } |
| |
| fn compile(file: &Path) -> Result<Output, Error> { |
| let tmp = tempdir()?; |
| |
| let args: Vec<OsString> = vec![ |
| file.into(), |
| "--error-format=json".into(), |
| "--emit=metadata".into(), |
| "--crate-name=rustfix_test".into(), |
| "--out-dir".into(), |
| tmp.path().into(), |
| ]; |
| |
| let res = Command::new(env::var_os("RUSTC").unwrap_or("rustc".into())) |
| .args(&args) |
| .env("CLIPPY_DISABLE_DOCS_LINKS", "true") |
| .env_remove("RUST_LOG") |
| .output()?; |
| |
| Ok(res) |
| } |
| |
| fn compile_and_get_json_errors(file: &Path) -> Result<String, Error> { |
| let res = compile(file)?; |
| let stderr = String::from_utf8(res.stderr)?; |
| if stderr.contains("is only accepted on the nightly compiler") { |
| panic!("rustfix tests require a nightly compiler"); |
| } |
| |
| match res.status.code() { |
| Some(0) | Some(1) | Some(101) => Ok(stderr), |
| _ => Err(anyhow!( |
| "failed with status {:?}: {}", |
| res.status.code(), |
| stderr |
| )), |
| } |
| } |
| |
| fn compiles_without_errors(file: &Path) -> Result<(), Error> { |
| let res = compile(file)?; |
| |
| match res.status.code() { |
| Some(0) => Ok(()), |
| _ => Err(anyhow!( |
| "file {:?} failed compile with status {:?}:\n {}", |
| file, |
| res.status.code(), |
| String::from_utf8(res.stderr)? |
| )), |
| } |
| } |
| |
| fn test_rustfix_with_file<P: AsRef<Path>>(file: P, mode: &str) { |
| let file: &Path = file.as_ref(); |
| let json_file = file.with_extension("json"); |
| let expected_fixed_file = file.with_extension("fixed.rs"); |
| |
| let filter_suggestions = if mode == fixmode::EVERYTHING { |
| rustfix::Filter::Everything |
| } else { |
| rustfix::Filter::MachineApplicableOnly |
| }; |
| |
| let code = fs::read_to_string(file).unwrap(); |
| |
| let json = compile_and_get_json_errors(file) |
| .with_context(|| format!("could not compile {}", file.display())) |
| .unwrap(); |
| |
| let suggestions = |
| rustfix::get_suggestions_from_json(&json, &HashSet::new(), filter_suggestions) |
| .context("could not load suggestions") |
| .unwrap(); |
| |
| let fixed = apply_suggestions(&code, &suggestions) |
| .with_context(|| format!("could not apply suggestions to {}", file.display())) |
| .unwrap() |
| .replace('\r', ""); |
| |
| let assert = Assert::new().action_env(snapbox::assert::DEFAULT_ACTION_ENV); |
| let (actual_fix, expected_fix) = assert.normalize( |
| Data::text(&fixed), |
| Data::read_from(expected_fixed_file.as_path(), Some(DataFormat::Text)), |
| ); |
| |
| if actual_fix != expected_fix { |
| let fixed_assert = assert.try_eq(Some(&"Current Fix"), actual_fix, expected_fix); |
| assert!(fixed_assert.is_ok(), "{}", fixed_assert.err().unwrap()); |
| |
| let expected_json = Data::read_from(json_file.as_path(), Some(DataFormat::Text)); |
| |
| let pretty_json = json |
| .split("\n") |
| .filter(|j| !j.is_empty()) |
| .map(|j| { |
| serde_json::to_string_pretty(&serde_json::from_str::<Value>(j).unwrap()).unwrap() |
| }) |
| .collect::<Vec<String>>() |
| .join("\n"); |
| |
| let json_assert = assert.try_eq( |
| Some(&"Compiler Error"), |
| Data::text(pretty_json), |
| expected_json, |
| ); |
| assert!(json_assert.is_ok(), "{}", json_assert.err().unwrap()); |
| } |
| |
| compiles_without_errors(&expected_fixed_file).unwrap(); |
| } |
| |
| macro_rules! run_test { |
| ($name:ident, $file:expr) => { |
| #[test] |
| #[allow(non_snake_case)] |
| fn $name() { |
| let (_, nightly) = version(); |
| if !$file.ends_with(".nightly.rs") || nightly { |
| let file = Path::new(concat!("./tests/everything/", $file)); |
| assert!(file.is_file(), "could not load {}", $file); |
| test_rustfix_with_file(file, fixmode::EVERYTHING); |
| } |
| } |
| }; |
| } |
| |
| run_test! { |
| closure_immutable_outer_variable, |
| "closure-immutable-outer-variable.rs" |
| } |
| run_test! {dedup_suggestions, "dedup-suggestions.rs"} |
| run_test! {E0178, "E0178.rs"} |
| run_test! {handle_insert_only, "handle-insert-only.rs"} |
| run_test! {lt_generic_comp, "lt-generic-comp.rs"} |
| run_test! {multiple_solutions, "multiple-solutions.rs"} |
| run_test! {replace_only_one_char, "replace-only-one-char.rs"} |
| run_test! {str_lit_type_mismatch, "str-lit-type-mismatch.rs"} |
| run_test! {use_insert, "use-insert.rs"} |