| //! Tests for caching compiler diagnostics. |
| |
| use cargo_test_support::prelude::*; |
| use cargo_test_support::str; |
| use cargo_test_support::tools; |
| use cargo_test_support::{basic_manifest, is_coarse_mtime, project, registry::Package, sleep_ms}; |
| |
| use super::messages::raw_rustc_output; |
| |
| fn as_str(bytes: &[u8]) -> &str { |
| std::str::from_utf8(bytes).expect("valid utf-8") |
| } |
| |
| #[cargo_test] |
| fn simple() { |
| // A simple example that generates two warnings (unused functions). |
| let p = project() |
| .file( |
| "src/lib.rs", |
| " |
| fn a() {} |
| fn b() {} |
| ", |
| ) |
| .build(); |
| |
| // Capture what rustc actually emits. This is done to avoid relying on the |
| // exact message formatting in rustc. |
| let rustc_output = raw_rustc_output(&p, "src/lib.rs", &[]); |
| |
| // -q so the output is the same as rustc (no "Compiling" or "Finished"). |
| let cargo_output1 = p |
| .cargo("check -q --color=never") |
| .exec_with_output() |
| .expect("cargo to run"); |
| assert_eq!(rustc_output, as_str(&cargo_output1.stderr)); |
| assert!(cargo_output1.stdout.is_empty()); |
| // Check that the cached version is exactly the same. |
| let cargo_output2 = p |
| .cargo("check -q") |
| .exec_with_output() |
| .expect("cargo to run"); |
| assert_eq!(rustc_output, as_str(&cargo_output2.stderr)); |
| assert!(cargo_output2.stdout.is_empty()); |
| } |
| |
| // same as `simple`, except everything is using the short format |
| #[cargo_test] |
| fn simple_short() { |
| let p = project() |
| .file( |
| "src/lib.rs", |
| " |
| fn a() {} |
| fn b() {} |
| ", |
| ) |
| .build(); |
| |
| let rustc_output = raw_rustc_output(&p, "src/lib.rs", &["--error-format=short"]); |
| |
| let cargo_output1 = p |
| .cargo("check -q --color=never --message-format=short") |
| .exec_with_output() |
| .expect("cargo to run"); |
| assert_eq!(rustc_output, as_str(&cargo_output1.stderr)); |
| // assert!(cargo_output1.stdout.is_empty()); |
| let cargo_output2 = p |
| .cargo("check -q --message-format=short") |
| .exec_with_output() |
| .expect("cargo to run"); |
| println!("{}", String::from_utf8_lossy(&cargo_output2.stdout)); |
| assert_eq!(rustc_output, as_str(&cargo_output2.stderr)); |
| assert!(cargo_output2.stdout.is_empty()); |
| } |
| |
| #[cargo_test] |
| fn color() { |
| // Check enabling/disabling color. |
| let p = project().file("src/lib.rs", "fn a() {}").build(); |
| |
| // Hack for issue in fwdansi 1.1. It is squashing multiple resets |
| // into a single reset. |
| // https://github.com/kennytm/fwdansi/issues/2 |
| fn normalize(s: &str) -> String { |
| #[cfg(windows)] |
| return s.replace("\x1b[0m\x1b[0m", "\x1b[0m"); |
| #[cfg(not(windows))] |
| return s.to_string(); |
| } |
| |
| let compare = |a, b| { |
| assert_eq!(normalize(a), normalize(b)); |
| }; |
| |
| // Capture the original color output. |
| let rustc_color = raw_rustc_output(&p, "src/lib.rs", &["--color=always"]); |
| assert!(rustc_color.contains("\x1b[")); |
| |
| // Capture the original non-color output. |
| let rustc_nocolor = raw_rustc_output(&p, "src/lib.rs", &[]); |
| assert!(!rustc_nocolor.contains("\x1b[")); |
| |
| // First pass, non-cached, with color, should be the same. |
| let cargo_output1 = p |
| .cargo("check -q --color=always") |
| .exec_with_output() |
| .expect("cargo to run"); |
| compare(&rustc_color, as_str(&cargo_output1.stderr)); |
| |
| // Replay cached, with color. |
| let cargo_output2 = p |
| .cargo("check -q --color=always") |
| .exec_with_output() |
| .expect("cargo to run"); |
| compare(&rustc_color, as_str(&cargo_output2.stderr)); |
| |
| // Replay cached, no color. |
| let cargo_output_nocolor = p |
| .cargo("check -q --color=never") |
| .exec_with_output() |
| .expect("cargo to run"); |
| compare(&rustc_nocolor, as_str(&cargo_output_nocolor.stderr)); |
| } |
| |
| #[cargo_test] |
| fn cached_as_json() { |
| // Check that cached JSON output is the same. |
| let p = project().file("src/lib.rs", "fn a() {}").build(); |
| |
| // Grab the non-cached output, feature disabled. |
| // NOTE: When stabilizing, this will need to be redone. |
| let cargo_output = p |
| .cargo("check --message-format=json") |
| .exec_with_output() |
| .expect("cargo to run"); |
| assert!(cargo_output.status.success()); |
| let orig_cargo_out = as_str(&cargo_output.stdout); |
| assert!(orig_cargo_out.contains("compiler-message")); |
| p.cargo("clean").run(); |
| |
| // Check JSON output, not fresh. |
| let cargo_output1 = p |
| .cargo("check --message-format=json") |
| .exec_with_output() |
| .expect("cargo to run"); |
| assert_eq!(as_str(&cargo_output1.stdout), orig_cargo_out); |
| |
| // Check JSON output, fresh. |
| let cargo_output2 = p |
| .cargo("check --message-format=json") |
| .exec_with_output() |
| .expect("cargo to run"); |
| // The only difference should be this field. |
| let fix_fresh = as_str(&cargo_output2.stdout).replace("\"fresh\":true", "\"fresh\":false"); |
| assert_eq!(fix_fresh, orig_cargo_out); |
| } |
| |
| #[cargo_test] |
| fn clears_cache_after_fix() { |
| // Make sure the cache is invalidated when there is no output. |
| let p = project().file("src/lib.rs", "fn asdf() {}").build(); |
| // Fill the cache. |
| p.cargo("check") |
| .with_stderr_data(str![[r#" |
| [CHECKING] foo v0.0.1 ([ROOT]/foo) |
| [WARNING] function `asdf` is never used |
| ... |
| [WARNING] `foo` (lib) generated 1 warning |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| |
| "#]]) |
| .run(); |
| let cpath = p |
| .glob("target/debug/.fingerprint/foo-*/output-*") |
| .next() |
| .unwrap() |
| .unwrap(); |
| assert!(std::fs::read_to_string(cpath).unwrap().contains("asdf")); |
| |
| // Fix it. |
| if is_coarse_mtime() { |
| sleep_ms(1000); |
| } |
| p.change_file("src/lib.rs", ""); |
| |
| p.cargo("check") |
| .with_stdout_data("") |
| .with_stderr_data(str![[r#" |
| [CHECKING] foo v0.0.1 ([ROOT]/foo) |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| |
| "#]]) |
| .run(); |
| assert_eq!( |
| p.glob("target/debug/.fingerprint/foo-*/output-*").count(), |
| 0 |
| ); |
| |
| // And again, check the cache is correct. |
| p.cargo("check") |
| .with_stdout_data("") |
| .with_stderr_data(str![[r#" |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| |
| "#]]) |
| .run(); |
| } |
| |
| #[cargo_test] |
| fn rustdoc() { |
| // Create a warning in rustdoc. |
| let p = project() |
| .file( |
| "src/lib.rs", |
| " |
| #![warn(missing_docs)] |
| pub fn f() {} |
| ", |
| ) |
| .build(); |
| |
| let rustdoc_output = p |
| .cargo("doc -q --color=always") |
| .exec_with_output() |
| .expect("rustdoc to run"); |
| assert!(rustdoc_output.status.success()); |
| let rustdoc_stderr = as_str(&rustdoc_output.stderr); |
| assert!(rustdoc_stderr.contains("missing")); |
| assert!(rustdoc_stderr.contains("\x1b[")); |
| assert_eq!( |
| p.glob("target/debug/.fingerprint/foo-*/output-*").count(), |
| 1 |
| ); |
| |
| // Check the cached output. |
| let rustdoc_output = p |
| .cargo("doc -q --color=always") |
| .exec_with_output() |
| .expect("rustdoc to run"); |
| assert_eq!(as_str(&rustdoc_output.stderr), rustdoc_stderr); |
| } |
| |
| #[cargo_test] |
| fn fix() { |
| // Make sure `fix` is not broken by caching. |
| let p = project().file("src/lib.rs", "pub fn try() {}").build(); |
| |
| p.cargo("fix --edition --allow-no-vcs").run(); |
| |
| assert_eq!(p.read_file("src/lib.rs"), "pub fn r#try() {}"); |
| } |
| |
| #[cargo_test] |
| fn very_verbose() { |
| // Handle cap-lints in dependencies. |
| Package::new("bar", "1.0.0") |
| .file("src/lib.rs", "fn not_used() {}") |
| .publish(); |
| |
| let p = project() |
| .file( |
| "Cargo.toml", |
| r#" |
| [package] |
| name = "foo" |
| version = "0.1.0" |
| edition = "2015" |
| |
| [dependencies] |
| bar = "1.0" |
| "#, |
| ) |
| .file("src/lib.rs", "") |
| .build(); |
| |
| p.cargo("check -vv") |
| .with_stderr_data(str![[r#" |
| [UPDATING] `dummy-registry` index |
| [LOCKING] 1 package to latest compatible version |
| [DOWNLOADING] crates ... |
| [DOWNLOADED] bar v1.0.0 (registry `dummy-registry`) |
| [CHECKING] bar v1.0.0 |
| [RUNNING] [..] |
| [WARNING] function `not_used` is never used |
| ... |
| [CHECKING] foo v0.1.0 ([ROOT]/foo) |
| [RUNNING] [..] |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| |
| "#]]) |
| .run(); |
| |
| p.cargo("check") |
| .with_stderr_data(str![[r#" |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| |
| "#]]) |
| .run(); |
| |
| p.cargo("check -vv") |
| .with_stderr_data(str![[r#" |
| [FRESH] bar v1.0.0 |
| [WARNING] function `not_used` is never used |
| ... |
| [WARNING] `bar` (lib) generated 1 warning |
| [FRESH] foo v0.1.0 ([ROOT]/foo) |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| |
| "#]]) |
| .run(); |
| } |
| |
| #[cargo_test] |
| fn doesnt_create_extra_files() { |
| // Ensure it doesn't create `output` files when not needed. |
| Package::new("dep", "1.0.0") |
| .file("src/lib.rs", "fn unused() {}") |
| .publish(); |
| |
| let p = project() |
| .file( |
| "Cargo.toml", |
| r#" |
| [package] |
| name = "foo" |
| version = "0.1.0" |
| edition = "2015" |
| |
| [dependencies] |
| dep = "1.0" |
| "#, |
| ) |
| .file("src/lib.rs", "") |
| .file("src/main.rs", "fn main() {}") |
| .build(); |
| |
| p.cargo("check").run(); |
| |
| assert_eq!( |
| p.glob("target/debug/.fingerprint/foo-*/output-*").count(), |
| 0 |
| ); |
| assert_eq!( |
| p.glob("target/debug/.fingerprint/dep-*/output-*").count(), |
| 0 |
| ); |
| if is_coarse_mtime() { |
| sleep_ms(1000); |
| } |
| p.change_file("src/lib.rs", "fn unused() {}"); |
| p.cargo("check").run(); |
| assert_eq!( |
| p.glob("target/debug/.fingerprint/foo-*/output-*").count(), |
| 1 |
| ); |
| } |
| |
| #[cargo_test] |
| fn replay_non_json() { |
| // Handles non-json output. |
| let rustc = project() |
| .at("rustc") |
| .file("Cargo.toml", &basic_manifest("rustc_alt", "1.0.0")) |
| .file( |
| "src/main.rs", |
| r#" |
| fn main() { |
| eprintln!("line 1"); |
| eprintln!("line 2"); |
| let r = std::process::Command::new("rustc") |
| .args(std::env::args_os().skip(1)) |
| .status(); |
| std::process::exit(r.unwrap().code().unwrap_or(2)); |
| } |
| "#, |
| ) |
| .build(); |
| rustc.cargo("build").run(); |
| let p = project().file("src/lib.rs", "").build(); |
| p.cargo("check") |
| .env("RUSTC", rustc.bin("rustc_alt")) |
| .with_stderr_data(str![[r#" |
| [CHECKING] foo v0.0.1 ([ROOT]/foo) |
| line 1 |
| line 2 |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| |
| "#]]) |
| .run(); |
| |
| p.cargo("check") |
| .env("RUSTC", rustc.bin("rustc_alt")) |
| .with_stderr_data(str![[r#" |
| line 1 |
| line 2 |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| |
| "#]]) |
| .run(); |
| } |
| |
| #[cargo_test] |
| fn caching_large_output() { |
| // Handles large number of messages. |
| // This is an arbitrary amount that is greater than the 100 used in |
| // job_queue. This is here to check for deadlocks or any other problems. |
| const COUNT: usize = 250; |
| let rustc = project() |
| .at("rustc") |
| .file("Cargo.toml", &basic_manifest("rustc_alt", "1.0.0")) |
| .file( |
| "src/main.rs", |
| &format!( |
| r#" |
| fn main() {{ |
| for i in 0..{} {{ |
| eprintln!("{{{{\"message\": \"test message {{}}\", \"level\": \"warning\", \ |
| \"spans\": [], \"children\": [], \"rendered\": \"test message {{}}\"}}}}", |
| i, i); |
| }} |
| let r = std::process::Command::new("rustc") |
| .args(std::env::args_os().skip(1)) |
| .status(); |
| std::process::exit(r.unwrap().code().unwrap_or(2)); |
| }} |
| "#, |
| COUNT |
| ), |
| ) |
| .build(); |
| |
| let mut expected = String::new(); |
| for i in 0..COUNT { |
| expected.push_str(&format!("test message {}\n", i)); |
| } |
| |
| rustc.cargo("build").run(); |
| let p = project().file("src/lib.rs", "").build(); |
| p.cargo("check") |
| .env("RUSTC", rustc.bin("rustc_alt")) |
| .with_stderr_data(&format!( |
| "\ |
| [CHECKING] foo v0.0.1 ([ROOT]/foo) |
| {}[WARNING] `foo` (lib) generated 250 warnings |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| ", |
| expected |
| )) |
| .run(); |
| |
| p.cargo("check") |
| .env("RUSTC", rustc.bin("rustc_alt")) |
| .with_stderr_data(&format!( |
| "\ |
| {}[WARNING] `foo` (lib) generated 250 warnings |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| ", |
| expected |
| )) |
| .run(); |
| } |
| |
| #[cargo_test] |
| fn rustc_workspace_wrapper() { |
| let p = project() |
| .file( |
| "src/lib.rs", |
| "pub fn f() { assert!(true); }\n\ |
| fn unused_func() {}", |
| ) |
| .build(); |
| |
| p.cargo("check -v") |
| .env("RUSTC_WORKSPACE_WRAPPER", tools::echo_wrapper()) |
| .with_stderr_data(str![[r#" |
| [CHECKING] foo v0.0.1 ([ROOT]/foo) |
| [RUNNING] [..]/rustc-echo-wrapper[EXE] rustc --crate-name foo [..] |
| WRAPPER CALLED: rustc --crate-name foo --edition=2015 src/lib.rs [..] |
| [WARNING] function `unused_func` is never used |
| ... |
| [WARNING] `foo` (lib) generated 1 warning |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| |
| "#]]) |
| .run(); |
| |
| // Check without a wrapper should rebuild |
| p.cargo("check -v") |
| .with_stderr_data(str![[r#" |
| [CHECKING] foo v0.0.1 ([ROOT]/foo) |
| [RUNNING] `rustc[..]` |
| [WARNING] function `unused_func` is never used |
| ... |
| [WARNING] `foo` (lib) generated 1 warning |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| |
| "#]]) |
| .with_stdout_data("") |
| .run(); |
| |
| // Again, reading from the cache. |
| p.cargo("check -v") |
| .env("RUSTC_WORKSPACE_WRAPPER", tools::echo_wrapper()) |
| .with_stderr_data(str![[r#" |
| [FRESH] foo v0.0.1 ([ROOT]/foo) |
| WRAPPER CALLED: rustc [..] |
| ... |
| [WARNING] `foo` (lib) generated 1 warning |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| |
| "#]]) |
| .with_stdout_data("") |
| .run(); |
| |
| // And `check` should also be fresh, reading from cache. |
| p.cargo("check -v") |
| .with_stderr_data(str![[r#" |
| [FRESH] foo v0.0.1 ([ROOT]/foo) |
| [WARNING] function `unused_func` is never used |
| ... |
| [WARNING] `foo` (lib) generated 1 warning |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| |
| "#]]) |
| .with_stdout_data("") |
| .run(); |
| } |
| |
| #[expect(deprecated)] |
| #[cargo_test] |
| fn wacky_hashless_fingerprint() { |
| // On Windows, executables don't have hashes. This checks for a bad |
| // assumption that caused bad caching. |
| let p = project() |
| .file("src/bin/a.rs", "fn main() { let unused = 1; }") |
| .file("src/bin/b.rs", "fn main() {}") |
| .build(); |
| p.cargo("check --bin b") |
| .with_stderr_does_not_contain("[..]unused[..]") |
| .run(); |
| p.cargo("check --bin a") |
| .with_stderr_data(str![[r#" |
| [CHECKING] foo v0.0.1 ([ROOT]/foo) |
| [WARNING] unused variable: `unused` |
| ... |
| [WARNING] `foo` (bin "a") generated 1 warning |
| [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s |
| |
| "#]]) |
| .run(); |
| // This should not pick up the cache from `a`. |
| p.cargo("check --bin b") |
| .with_stderr_does_not_contain("[..]unused[..]") |
| .run(); |
| } |