blob: e74fd595dc4e84cdc9a977c183d47ca41ca338a1 [file] [log] [blame]
//! 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",
);
}