//! Tests for the `cargo fix` command, specifically targeting the logic around //! running rustc multiple times to apply and verify fixes. //! //! These tests use a replacement of rustc ("rustc-fix-shim") which emits JSON //! messages based on what the test is exercising. It uses an environment //! variable RUSTC_FIX_SHIM_SEQUENCE which determines how it should behave //! based on how many times `rustc` has run. It keeps track of how many times //! rustc has run in a local file. //! //! For example, a sequence of `[Step::OneFix, Step::Error]` will emit one //! suggested fix the first time `rustc` is run, and then the next time it is //! run it will generate an error. //! //! The [`expect_fix_runs_rustc_n_times`] function handles setting everything //! up, and verifying the results. use std::path::PathBuf; use std::sync::{Mutex, OnceLock}; use cargo_test_support::prelude::*; use cargo_test_support::{basic_manifest, paths, project, str, tools, Execs}; /// The action that the `rustc` shim should take in the current sequence of /// events. #[derive(Clone, Copy, PartialEq)] #[repr(u8)] enum Step { /// Exits with success with no messages. SuccessNoOutput = b'0', /// Emits one suggested fix. /// /// The suggested fix involves updating the number of the first line /// comment which starts as `// fix-count 0`. OneFix = b'1', /// Emits two suggested fixes which overlap, which rustfix can only apply /// one of them, and fails for the other. /// /// The suggested fix is the same as `Step::OneFix`, it just shows up /// twice. TwoFixOverlapping = b'2', /// Generates a warning without a suggestion. Warning = b'w', /// Generates an error message with no suggestion. Error = b'e', /// Emits one suggested fix and an error. OneFixError = b'f', } /// Verifies `cargo fix` behavior based on the given sequence of behaviors for /// `rustc`. /// /// - `sequence` is the sequence of behaviors for each call to `rustc`. /// If rustc is called more often than the number of steps, then it will panic. /// - `extra_execs` a callback that allows extra customization of the [`Execs`]. /// - `expected_stderr` is the expected output from cargo. /// - `expected_lib_rs` is the expected contents of `src/lib.rs` after the /// fixes have been applied. The file starts out with the content `// /// fix-count 0`, and the number increases based on which suggestions are /// applied. fn expect_fix_runs_rustc_n_times( sequence: &[Step], extra_execs: impl FnOnce(&mut Execs), expected_stderr: impl IntoData, expected_lib_rs: &str, ) { let rustc = rustc_for_cargo_fix(); let p = project().file("src/lib.rs", "// fix-count 0").build(); let sequence_vec: Vec<_> = sequence.iter().map(|x| *x as u8).collect(); let sequence_str = std::str::from_utf8(&sequence_vec).unwrap(); let mut execs = p.cargo("fix --allow-no-vcs --lib"); execs .env("RUSTC", &rustc) .env("RUSTC_FIX_SHIM_SEQUENCE", sequence_str) .with_stderr_data(expected_stderr); extra_execs(&mut execs); execs.run(); let lib_rs = p.read_file("src/lib.rs"); assert_eq!(expected_lib_rs, lib_rs); let count: usize = p.read_file("rustc-fix-shim-count").parse().unwrap(); assert_eq!(sequence.len(), count); } /// Returns the path to the rustc replacement executable. fn rustc_for_cargo_fix() -> PathBuf { static FIX_SHIM: OnceLock>> = OnceLock::new(); let mut lock = FIX_SHIM.get_or_init(|| Default::default()).lock().unwrap(); if let Some(path) = &*lock { return path.clone(); } let p = project() .at(paths::global_root().join("rustc-fix-shim")) .file("Cargo.toml", &basic_manifest("rustc-fix-shim", "1.0.0")) .file( "src/main.rs", r##" fn main() { if std::env::var_os("CARGO_PKG_NAME").is_none() { // Handle things like rustc -Vv let r = std::process::Command::new("rustc") .args(std::env::args_os().skip(1)) .status(); std::process::exit(r.unwrap().code().unwrap_or(2)); } // Keep track of which step in the sequence that needs to run. let successful_count = std::fs::read_to_string("rustc-fix-shim-count") .map(|c| c.parse().unwrap()) .unwrap_or(0); std::fs::write("rustc-fix-shim-count", format!("{}", successful_count + 1)).unwrap(); // The sequence tells us which behavior we should have. let seq = std::env::var("RUSTC_FIX_SHIM_SEQUENCE").unwrap(); if successful_count >= seq.len() { panic!("rustc called too many times count={}, \ make sure to update the Step sequence", successful_count); } match seq.as_bytes()[successful_count] { b'0' => return, b'1' => { output_suggestion(successful_count + 1); } b'2' => { output_suggestion(successful_count + 1); output_suggestion(successful_count + 2); } b'w' => { output_message("warning", successful_count + 1); } b'e' => { output_message("error", successful_count + 1); std::process::exit(1); } b'f' => { output_suggestion(successful_count + 1); output_message("error", successful_count + 2); std::process::exit(1); } _ => panic!("unexpected sequence"), } } fn output_suggestion(count: usize) { let json = format!( r#"{{ "$message_type": "diagnostic", "message": "rustc fix shim comment {count}", "code": null, "level": "warning", "spans": [ {{ "file_name": "src/lib.rs", "byte_start": 13, "byte_end": 14, "line_start": 1, "line_end": 1, "column_start": 14, "column_end": 15, "is_primary": true, "text": [ {{ "text": "// fix-count 0", "highlight_start": 14, "highlight_end": 15 }} ], "label": "increase this number", "suggested_replacement": null, "suggestion_applicability": null, "expansion": null }} ], "children": [ {{ "message": "update the number here", "code": null, "level": "help", "spans": [ {{ "file_name": "src/lib.rs", "byte_start": 13, "byte_end": 14, "line_start": 1, "line_end": 1, "column_start": 14, "column_end": 15, "is_primary": true, "text": [ {{ "text": "// fix-count 0", "highlight_start": 14, "highlight_end": 15 }} ], "label": null, "suggested_replacement": "{count}", "suggestion_applicability": "MachineApplicable", "expansion": null }} ], "children": [], "rendered": null }} ], "rendered": "rustc fix shim comment {count}" }}"#, ) .replace("\n", ""); eprintln!("{json}"); } fn output_message(level: &str, count: usize) { let json = format!( r#"{{ "$message_type": "diagnostic", "message": "rustc fix shim {level} count={count}", "code": null, "level": "{level}", "spans": [ {{ "file_name": "src/lib.rs", "byte_start": 0, "byte_end": 0, "line_start": 1, "line_end": 1, "column_start": 1, "column_end": 1, "is_primary": true, "text": [ {{ "text": "// fix-count 0", "highlight_start": 1, "highlight_end": 4 }} ], "label": "forced error", "suggested_replacement": null, "suggestion_applicability": null, "expansion": null }} ], "children": [], "rendered": "rustc fix shim {level} count={count}" }}"#, ) .replace("\n", ""); eprintln!("{json}"); } "##, ) .build(); p.cargo("build").run(); let path = p.bin("rustc-fix-shim"); *lock = Some(path.clone()); path } #[cargo_test] fn fix_no_suggestions() { // No suggested fixes. expect_fix_runs_rustc_n_times( &[Step::SuccessNoOutput], |_execs| {}, str![[r#" [CHECKING] foo v0.0.1 ([ROOT]/foo) [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]], "// fix-count 0", ); } #[cargo_test] fn fix_one_suggestion() { // One suggested fix, with a successful verification, no output. expect_fix_runs_rustc_n_times( &[Step::OneFix, Step::SuccessNoOutput], |_execs| {}, str![[r#" [CHECKING] foo v0.0.1 ([ROOT]/foo) [FIXED] src/lib.rs (1 fix) [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]], "// fix-count 1", ); } #[cargo_test] fn fix_one_overlapping() { // Two suggested fixes, where one fails, then the next step returns no suggestions. expect_fix_runs_rustc_n_times( &[Step::TwoFixOverlapping, Step::SuccessNoOutput], |_execs| {}, str![[r#" [CHECKING] foo v0.0.1 ([ROOT]/foo) [FIXED] src/lib.rs (1 fix) [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]], "// fix-count 2", ); } #[cargo_test] fn fix_overlapping_max() { // Rustc repeatedly spits out suggestions that overlap, which should hit // the limit of 4 attempts. It should show the output from the 5th attempt. expect_fix_runs_rustc_n_times( &[ Step::TwoFixOverlapping, Step::TwoFixOverlapping, Step::TwoFixOverlapping, Step::TwoFixOverlapping, Step::TwoFixOverlapping, ], |_execs| {}, str![[r#" [CHECKING] foo v0.0.1 ([ROOT]/foo) [WARNING] error applying suggestions to `src/lib.rs` The full error message was: > cannot replace slice of data that was already replaced This likely indicates a bug in either rustc or cargo itself, and we would appreciate a bug report! You're likely to see a number of compiler warnings after this message which cargo attempted to fix but failed. If you could open an issue at https://github.com/rust-lang/rust/issues quoting the full output of this command we'd be very appreciative! Note that you may be able to make some more progress in the near-term fixing code with the `--broken-code` flag [FIXED] src/lib.rs (4 fixes) rustc fix shim comment 5 rustc fix shim comment 6 [WARNING] `foo` (lib) generated 2 warnings (run `cargo fix --lib -p foo` to apply 2 suggestions) [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]], "// fix-count 5", ); } #[cargo_test] fn fix_verification_failed() { // One suggested fix, with an error in the verification step. // This should cause `cargo fix` to back out the changes. expect_fix_runs_rustc_n_times( &[Step::OneFix, Step::Error], |_execs| {}, str![[r#" [CHECKING] foo v0.0.1 ([ROOT]/foo) [WARNING] failed to automatically apply fixes suggested by rustc to crate `foo` after fixes were automatically applied the compiler reported errors within these files: * src/lib.rs This likely indicates a bug in either rustc or cargo itself, and we would appreciate a bug report! You're likely to see a number of compiler warnings after this message which cargo attempted to fix but failed. If you could open an issue at https://github.com/rust-lang/rust/issues quoting the full output of this command we'd be very appreciative! Note that you may be able to make some more progress in the near-term fixing code with the `--broken-code` flag The following errors were reported: rustc fix shim error count=2 Original diagnostics will follow. rustc fix shim comment 1 [WARNING] `foo` (lib) generated 1 warning (run `cargo fix --lib -p foo` to apply 1 suggestion) [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]], "// fix-count 0", ); } #[cargo_test] fn fix_verification_failed_clippy() { // This is the same as `fix_verification_failed_clippy`, except it checks // the error message has the customization for the clippy URL and // subcommand. expect_fix_runs_rustc_n_times( &[Step::OneFix, Step::Error], |execs| { execs.env("RUSTC_WORKSPACE_WRAPPER", tools::wrapped_clippy_driver()); }, str![[r#" [CHECKING] foo v0.0.1 ([ROOT]/foo) [WARNING] failed to automatically apply fixes suggested by rustc to crate `foo` after fixes were automatically applied the compiler reported errors within these files: * src/lib.rs This likely indicates a bug in either rustc or cargo itself, and we would appreciate a bug report! You're likely to see a number of compiler warnings after this message which cargo attempted to fix but failed. If you could open an issue at https://github.com/rust-lang/rust-clippy/issues quoting the full output of this command we'd be very appreciative! Note that you may be able to make some more progress in the near-term fixing code with the `--broken-code` flag The following errors were reported: rustc fix shim error count=2 Original diagnostics will follow. rustc fix shim comment 1 [WARNING] `foo` (lib) generated 1 warning (run `cargo clippy --fix --lib -p foo` to apply 1 suggestion) [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]], "// fix-count 0", ); } #[cargo_test] fn warnings() { // Only emits warnings. expect_fix_runs_rustc_n_times( &[Step::Warning], |_execs| {}, str![[r#" [CHECKING] foo v0.0.1 ([ROOT]/foo) rustc fix shim warning count=1 [WARNING] `foo` (lib) generated 1 warning [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s "#]], "// fix-count 0", ); } #[cargo_test] fn starts_with_error() { // The source code doesn't compile to start with. expect_fix_runs_rustc_n_times( &[Step::Error], |execs| { execs.with_status(101); }, str![[r#" [CHECKING] foo v0.0.1 ([ROOT]/foo) rustc fix shim error count=1 [ERROR] could not compile `foo` (lib) due to 1 previous error "#]], "// fix-count 0", ); } #[cargo_test] fn broken_code_no_suggestions() { // --broken-code with no suggestions expect_fix_runs_rustc_n_times( &[Step::Error], |execs| { execs.arg("--broken-code").with_status(101); }, str![[r#" [CHECKING] foo v0.0.1 ([ROOT]/foo) rustc fix shim error count=1 [ERROR] could not compile `foo` (lib) due to 1 previous error "#]], "// fix-count 0", ); } #[cargo_test] fn broken_code_one_suggestion() { // --broken-code where there is an error and a suggestion. expect_fix_runs_rustc_n_times( &[Step::OneFixError, Step::Error], |execs| { execs.arg("--broken-code").with_status(101); }, str![[r#" [CHECKING] foo v0.0.1 ([ROOT]/foo) [WARNING] failed to automatically apply fixes suggested by rustc to crate `foo` after fixes were automatically applied the compiler reported errors within these files: * src/lib.rs This likely indicates a bug in either rustc or cargo itself, and we would appreciate a bug report! You're likely to see a number of compiler warnings after this message which cargo attempted to fix but failed. If you could open an issue at https://github.com/rust-lang/rust/issues quoting the full output of this command we'd be very appreciative! Note that you may be able to make some more progress in the near-term fixing code with the `--broken-code` flag The following errors were reported: rustc fix shim error count=2 Original diagnostics will follow. rustc fix shim comment 1 rustc fix shim error count=2 [WARNING] `foo` (lib) generated 1 warning [ERROR] could not compile `foo` (lib) due to 1 previous error; 1 warning emitted "#]], "// fix-count 1", ); }