| //! 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 cargo_test_support::{basic_manifest, paths, project, tools, Execs}; |
| use std::path::PathBuf; |
| use std::sync::{Mutex, OnceLock}; |
| |
| /// 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: &str, |
| 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(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<Mutex<Option<PathBuf>>> = 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| {}, |
| "\ |
| [CHECKING] foo [..] |
| [FINISHED] [..] |
| ", |
| "// 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| {}, |
| "\ |
| [CHECKING] foo [..] |
| [FIXED] src/lib.rs (1 fix) |
| [FINISHED] [..] |
| ", |
| "// 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| {}, |
| "\ |
| [CHECKING] foo [..] |
| [FIXED] src/lib.rs (1 fix) |
| [FINISHED] [..] |
| ", |
| "// 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| {}, |
| "\ |
| [CHECKING] 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] [..] |
| ", |
| "// 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| {}, |
| "\ |
| [CHECKING] 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] [..] |
| ", |
| "// 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()); |
| }, |
| "\ |
| [CHECKING] 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] [..] |
| ", |
| "// fix-count 0", |
| ); |
| } |
| |
| #[cargo_test] |
| fn warnings() { |
| // Only emits warnings. |
| expect_fix_runs_rustc_n_times( |
| &[Step::Warning], |
| |_execs| {}, |
| "\ |
| [CHECKING] foo [..] |
| rustc fix shim warning count=1 |
| warning: `foo` (lib) generated 1 warning |
| [FINISHED] [..] |
| ", |
| "// 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); |
| }, |
| "\ |
| [CHECKING] 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); |
| }, |
| "\ |
| [CHECKING] 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); |
| }, |
| "\ |
| [CHECKING] 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", |
| ); |
| } |