diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44226588..0e5a513f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,30 +77,40 @@ jobs: - run: cargo install --path . --debug - name: Test cargo llvm-cov nextest run: | + unset RUSTFLAGS cargo llvm-cov nextest --text --fail-under-lines 50 cargo llvm-cov nextest --text --fail-under-lines 50 --profile default --cargo-profile dev cargo llvm-cov nextest --text --fail-under-lines 50 --profile ci cargo llvm-cov nextest --text --fail-under-lines 50 --profile ci --cargo-profile dev + cargo clean cd -- ../real1 cargo llvm-cov nextest-archive --archive-file a.tar.zst cargo llvm-cov nextest --archive-file a.tar.zst --text --fail-under-lines 70 cargo llvm-cov report --nextest-archive-file a.tar.zst --fail-under-lines 70 - rm -- a.tar.zst cargo clean + rm -- a.tar.zst cargo llvm-cov nextest-archive --archive-file a.tar.zst --release cargo llvm-cov nextest --archive-file a.tar.zst --text --fail-under-lines 70 cargo llvm-cov report --nextest-archive-file a.tar.zst --fail-under-lines 70 - rm -- a.tar.zst cargo clean + rm -- a.tar.zst cargo llvm-cov nextest-archive --archive-file a.tar.zst --cargo-profile custom cargo llvm-cov nextest --archive-file a.tar.zst --text --fail-under-lines 70 cargo llvm-cov report --nextest-archive-file a.tar.zst --fail-under-lines 70 - rm -- a.tar.zst cargo clean + rm -- a.tar.zst host=$(rustc -vV | grep -E '^host:' | cut -d' ' -f2) cargo llvm-cov nextest-archive --archive-file a.tar.zst --target "${host}" cargo llvm-cov nextest --archive-file a.tar.zst --text --fail-under-lines 70 cargo llvm-cov report --nextest-archive-file a.tar.zst --fail-under-lines 70 + cargo clean + rm -- a.tar.zst + cd -- ../build_dir + cargo llvm-cov nextest --text --fail-under-lines 100 + cargo llvm-cov nextest --text --fail-under-lines 100 --profile default --cargo-profile dev + cargo llvm-cov nextest --text --fail-under-lines 100 --profile ci + cargo llvm-cov nextest --text --fail-under-lines 100 --profile ci --cargo-profile dev + cargo clean working-directory: tests/fixtures/crates/bin_crate - name: Test nightly-specific options, old Cargo compatibility, trybuild compatibility run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 79f02a5c..bcc23991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,9 @@ Note: In this file, do not use the hard wrap in the middle of a sentence for com ## [Unreleased] -- Update minimal version of `cargo-config2` to 0.1.38 to improve support for target names that contain ".". ([#446](https://github.com/taiki-e/cargo-llvm-cov/pull/446)) +- Support Cargo `build-dir` that added in Cargo 1.91. ([#452](https://github.com/taiki-e/cargo-llvm-cov/pull/452)) + +- Update minimal version of `cargo-config2` to 0.1.38 to improve support for target names that contain ".". ([#446](https://github.com/taiki-e/cargo-llvm-cov/issues/446)) ## [0.6.19] - 2025-09-07 diff --git a/README.md b/README.md index 7d0d7404..d6b39980 100644 --- a/README.md +++ b/README.md @@ -676,6 +676,7 @@ You may need to click the "Watch" label in the bottom bar of VS Code to display You can override these environment variables to change cargo-llvm-cov's behavior on your system: - `CARGO_LLVM_COV_TARGET_DIR` -- Location of where to place all generated artifacts, relative to the current working directory. Default to `/llvm-cov-target`. +- `CARGO_LLVM_COV_BUILD_DIR` -- Location of where intermediate build artifacts will be stored, relative to the current working directory. Default to `/llvm-cov-target`. - `CARGO_LLVM_COV_SETUP` -- Control behavior if `llvm-tools-preview` component is not installed. See [#219] for more. - `LLVM_COV` -- Override the path to `llvm-cov`. You may need to specify both this and `LLVM_PROFDATA` environment variables if you are using [`--include-ffi` flag](#get-coverage-of-cc-code-linked-to-rust-librarybinary) or if you are using a toolchain installed without via rustup. `llvm-cov` version must be compatible with the LLVM version used in rustc. - `LLVM_PROFDATA` -- Override the path to `llvm-profdata`. See `LLVM_COV` environment variable for more. diff --git a/src/cargo.rs b/src/cargo.rs index a7e6302c..757799e0 100644 --- a/src/cargo.rs +++ b/src/cargo.rs @@ -7,7 +7,7 @@ use camino::{Utf8Path, Utf8PathBuf}; use cargo_config2::Config; use crate::{ - cli::{Args, ManifestOptions, Subcommand}, + cli::{ManifestOptions, Subcommand}, context::Context, env, metadata::Metadata, @@ -21,6 +21,7 @@ pub(crate) struct Workspace { pub(crate) current_manifest: Utf8PathBuf, pub(crate) target_dir: Utf8PathBuf, + pub(crate) build_dir: Option, pub(crate) output_dir: Utf8PathBuf, pub(crate) doctests_dir: Utf8PathBuf, pub(crate) profdata_file: Utf8PathBuf, @@ -92,18 +93,32 @@ impl Workspace { .is_ok_and(|s| s.contains("doctest-in-workspace")); } - let target_dir = - if let Some(path) = env::var("CARGO_LLVM_COV_TARGET_DIR")?.map(Utf8PathBuf::from) { - let mut base: Utf8PathBuf = env::current_dir()?.try_into()?; - base.push(path); + let (target_dir, build_dir) = if let Some(mut target_dir) = + env::var("CARGO_LLVM_COV_TARGET_DIR")?.map(Utf8PathBuf::from) + { + let mut base: Utf8PathBuf = env::current_dir()?.try_into()?; + target_dir = base.join(target_dir); + let build_dir = if let Some(build_dir) = + env::var("CARGO_LLVM_COV_BUILD_DIR")?.map(Utf8PathBuf::from) + { + base.push(build_dir); base - } else if show_env { - metadata.target_directory.clone() } else { - // If we change RUSTFLAGS, all dependencies will be recompiled. Therefore, - // use a subdirectory of the target directory as the actual target directory. - metadata.target_directory.join("llvm-cov-target") + target_dir.clone() }; + (target_dir, build_dir) + } else if show_env { + (metadata.target_directory.clone(), metadata.build_directory().to_owned()) + } else { + // If we change RUSTFLAGS, all dependencies will be recompiled. Therefore, + // use a subdirectory of the target directory as the actual target directory. + ( + metadata.target_directory.join("llvm-cov-target"), + metadata.build_directory().join("llvm-cov-target"), + ) + }; + // The scope of --target-dir's effect depends on whether build-dir is specified in the config. + let build_dir = config.build.build_dir.as_ref().and(Some(build_dir)); let output_dir = metadata.target_directory.join("llvm-cov"); let doctests_dir = target_dir.join("doctestbins"); @@ -116,6 +131,7 @@ impl Workspace { metadata, current_manifest, target_dir, + build_dir, output_dir, doctests_dir, profdata_file, @@ -222,7 +238,16 @@ pub(crate) fn test_or_run_args(cx: &Context, cmd: &mut ProcessBuilder) { cmd.arg("--manifest-path"); cmd.arg(&cx.ws.current_manifest); - add_target_dir(&cx.args, cmd, &cx.ws.target_dir); + // https://github.com/taiki-e/cargo-llvm-cov/issues/265 + if matches!(cx.args.subcommand, Subcommand::Nextest { archive_file: true }) { + cmd.arg("--extract-to"); + } else { + cmd.arg("--target-dir"); + } + cmd.arg(cx.ws.target_dir.as_str()); + if let Some(build_dir) = &cx.ws.build_dir { + cmd.env("CARGO_BUILD_BUILD_DIR", build_dir.as_str()); + } for cargo_arg in &cx.args.cargo_args { cmd.arg(cargo_arg); @@ -257,6 +282,9 @@ pub(crate) fn clean_args(cx: &Context, cmd: &mut ProcessBuilder) { cmd.arg("--target-dir"); cmd.arg(cx.ws.target_dir.as_str()); + if let Some(build_dir) = &cx.ws.build_dir { + cmd.env("CARGO_BUILD_BUILD_DIR", build_dir.as_str()); + } cx.args.manifest.cargo_args(cmd); @@ -265,13 +293,3 @@ pub(crate) fn clean_args(cx: &Context, cmd: &mut ProcessBuilder) { cmd.arg(format!("-{}", "v".repeat(cx.args.verbose as usize - 1))); } } - -// https://github.com/taiki-e/cargo-llvm-cov/issues/265 -fn add_target_dir(args: &Args, cmd: &mut ProcessBuilder, target_dir: &Utf8Path) { - if matches!(args.subcommand, Subcommand::Nextest { archive_file: true }) { - cmd.arg("--extract-to"); - } else { - cmd.arg("--target-dir"); - } - cmd.arg(target_dir.as_str()); -} diff --git a/src/clean.rs b/src/clean.rs index 17a99db5..4d62c00e 100644 --- a/src/clean.rs +++ b/src/clean.rs @@ -29,6 +29,9 @@ pub(crate) fn run(args: &mut Args) -> Result<()> { for dir in &[&ws.target_dir, &ws.output_dir] { rm_rf(dir, args.verbose != 0)?; } + if let Some(dir) = &ws.build_dir { + rm_rf(dir, args.verbose != 0)?; + } return Ok(()); } @@ -92,6 +95,9 @@ fn clean_ws( for args in args_set { let mut cmd = ws.cargo(verbose); cmd.args(["clean", "--target-dir", ws.target_dir.as_str()]).args(&package_args); + if let Some(build_dir) = &ws.build_dir { + cmd.env("CARGO_BUILD_BUILD_DIR", build_dir.as_str()); + } cmd.args(args); if verbose > 0 { cmd.arg(format!("-{}", "v".repeat(verbose as usize))); diff --git a/src/context.rs b/src/context.rs index e19beaf5..99170d37 100644 --- a/src/context.rs +++ b/src/context.rs @@ -127,6 +127,14 @@ impl Context { if args.subcommand.call_cargo_nextest() { "cargo-nextest" } else { "cargo" } ); } + if ws.config.build.build_dir.is_some() + && matches!( + args.subcommand, + Subcommand::Nextest { archive_file: true } | Subcommand::NextestArchive + ) + { + warn!("nextest archive may not work with Cargo build-dir"); + } let (llvm_cov, llvm_profdata): (PathBuf, PathBuf) = match ( env::var_os("LLVM_COV").map(PathBuf::from), diff --git a/src/main.rs b/src/main.rs index 43be866a..104e1e1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,6 +82,7 @@ fn try_main() -> Result<()> { }; set_env(cx, writer, IsNextest(true))?; // Include envs for nextest. writer.set("CARGO_LLVM_COV_TARGET_DIR", cx.ws.metadata.target_directory.as_str())?; + writer.set("CARGO_LLVM_COV_BUILD_DIR", cx.ws.metadata.build_directory().as_str())?; writer.writer.flush()?; } Subcommand::Report { .. } => { @@ -791,8 +792,10 @@ fn object_files(cx: &Context) -> Result> { // This is not the ideal way, but the way unstable book says it is cannot support them. // https://doc.rust-lang.org/nightly/rustc/instrument-coverage.html#tips-for-listing-the-binaries-automatically let mut target_dir = cx.ws.target_dir.clone(); + let mut build_dir = cx.ws.build_dir.clone(); let mut auto_detect_profile = false; if cx.args.subcommand.read_nextest_archive() { + // TODO: build-dir #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] struct BinariesMetadata { @@ -852,9 +855,12 @@ fn object_files(cx: &Context) -> Result> { } } if !auto_detect_profile { - // https://doc.rust-lang.org/nightly/cargo/guide/build-cache.html + // https://doc.rust-lang.org/nightly/cargo/reference/build-cache.html if let Some(target) = &cx.args.target { target_dir.push(target); + if let Some(build_dir) = &mut build_dir { + build_dir.push(target); + } } // https://doc.rust-lang.org/nightly/cargo/reference/profiles.html#custom-profiles let profile = match cx.args.cargo_profile.as_deref() { @@ -864,6 +870,9 @@ fn object_files(cx: &Context) -> Result> { Some(p) => p, }; target_dir.push(profile); + if let Some(build_dir) = &mut build_dir { + build_dir.push(profile); + } } for f in walk_target_dir(cx, &target_dir) { let f = f.path(); @@ -876,6 +885,22 @@ fn object_files(cx: &Context) -> Result> { } } searched_dir.push_str(target_dir.as_str()); + if let Some(build_dir) = &build_dir { + if target_dir != *build_dir { + for f in walk_target_dir(cx, build_dir) { + let f = f.path(); + if is_object(cx, f) { + if let Some(file_stem) = fs::file_stem_recursive(f).unwrap().to_str() { + if re.is_match(file_stem) { + files.push(make_relative(cx, f).to_owned().into_os_string()); + } + } + } + } + searched_dir.push(','); + searched_dir.push_str(build_dir.as_str()); + } + } if cx.args.doctests { for f in glob::glob( Utf8Path::new(&glob::Pattern::escape(cx.ws.doctests_dir.as_str())) @@ -931,10 +956,10 @@ fn object_files(cx: &Context) -> Result> { files.sort_unstable(); if files.is_empty() { - warn!( + bail!( "not found object files (searched directories: {searched_dir}); this may occur if \ show-env subcommand is used incorrectly (see docs or other warnings), or unsupported \ - commands are used", + commands or configs are used", ); } Ok(files) @@ -1313,6 +1338,11 @@ fn ignore_filename_regex(cx: &Context, object_files: &[OsString]) -> Result; @@ -33,6 +33,8 @@ pub(crate) struct Metadata { /// The absolute path to the root of the workspace. pub(crate) workspace_root: Utf8PathBuf, pub(crate) target_directory: Utf8PathBuf, + /// This is always `None` if running with a version of Cargo older than 1.91. + build_directory: Option, } impl Metadata { @@ -67,8 +69,18 @@ impl Metadata { workspace_members, workspace_root: map.remove_string("workspace_root")?, target_directory: map.remove_string("target_directory")?, + // This field was added in Rust 1.91. + build_directory: if map.contains_key("build_directory") { + Some(map.remove_string("build_directory")?) + } else { + None + }, }) } + + pub(crate) fn build_directory(&self) -> &Utf8Path { + self.build_directory.as_deref().unwrap_or(&self.target_directory) + } } pub(crate) struct Package { @@ -110,11 +122,7 @@ impl Target { // #[allow(clippy::option_option)] // fn allow_null(value: Value, f: impl FnOnce(Value) -> Option) -> Option> { -// if value.is_null() { -// Some(None) -// } else { -// f(value).map(Some) -// } +// if value.is_null() { Some(None) } else { f(value).map(Some) } // } fn into_string>(value: Value) -> Option { diff --git a/tests/fixtures/coverage-reports/build_dir/build_dir.codecov.json b/tests/fixtures/coverage-reports/build_dir/build_dir.codecov.json new file mode 100644 index 00000000..6ea47cdf --- /dev/null +++ b/tests/fixtures/coverage-reports/build_dir/build_dir.codecov.json @@ -0,0 +1 @@ +{"coverage":{"src/lib.rs":{"1":"1/1","2":"2/3","3":"1/1","6":"1/1","10":"2/2","11":"1/1"}}} \ No newline at end of file diff --git a/tests/fixtures/coverage-reports/build_dir/build_dir.hide-instantiations.txt b/tests/fixtures/coverage-reports/build_dir/build_dir.hide-instantiations.txt new file mode 100644 index 00000000..1982bc28 --- /dev/null +++ b/tests/fixtures/coverage-reports/build_dir/build_dir.hide-instantiations.txt @@ -0,0 +1,12 @@ + 1| 1|fn func(x: i32) -> bool { + 2| 1| if x < 0 { true } else { false } + ^0 + 3| 1|} + 4| | + 5| |#[test] + 6| 1|fn test() { + 7| | #[cfg(a)] + 8| | assert!(!func(1)); + 9| | #[cfg(not(a))] + 10| 1| assert!(func(-1)); + 11| 1|} \ No newline at end of file diff --git a/tests/fixtures/coverage-reports/build_dir/build_dir.json b/tests/fixtures/coverage-reports/build_dir/build_dir.json new file mode 100644 index 00000000..31c6d0d3 --- /dev/null +++ b/tests/fixtures/coverage-reports/build_dir/build_dir.json @@ -0,0 +1,83 @@ +{ + "data": [ + { + "files": [ + { + "filename": "src/lib.rs", + "summary": { + "branches": { + "count": 0, + "covered": 0, + "notcovered": 0, + "percent": 0.0 + }, + "mcdc": { + "count": 0, + "covered": 0, + "notcovered": 0, + "percent": 0.0 + }, + "functions": { + "count": 2, + "covered": 2, + "percent": 100.0 + }, + "instantiations": { + "count": 2, + "covered": 2, + "percent": 100.0 + }, + "lines": { + "count": 6, + "covered": 6, + "percent": 100.0 + }, + "regions": { + "count": 9, + "covered": 8, + "notcovered": 1, + "percent": 88.88888888888889 + } + } + } + ], + "totals": { + "branches": { + "count": 0, + "covered": 0, + "notcovered": 0, + "percent": 0 + }, + "functions": { + "count": 2, + "covered": 2, + "percent": 100 + }, + "instantiations": { + "count": 2, + "covered": 2, + "percent": 100 + }, + "lines": { + "count": 6, + "covered": 6, + "percent": 100 + }, + "mcdc": { + "count": 0, + "covered": 0, + "notcovered": 0, + "percent": 0 + }, + "regions": { + "count": 9, + "covered": 8, + "notcovered": 1, + "percent": 88.88888888888889 + } + } + } + ], + "type": "llvm.coverage.json.export", + "version": "3.0.1" +} \ No newline at end of file diff --git a/tests/fixtures/coverage-reports/build_dir/build_dir.lcov.info b/tests/fixtures/coverage-reports/build_dir/build_dir.lcov.info new file mode 100644 index 00000000..f4871027 --- /dev/null +++ b/tests/fixtures/coverage-reports/build_dir/build_dir.lcov.info @@ -0,0 +1,8 @@ +SF:src/lib.rs +FNF:2 +FNH:2 +BRF:0 +BRH:0 +LF:6 +LH:6 +end_of_record \ No newline at end of file diff --git a/tests/fixtures/coverage-reports/build_dir/build_dir.summary.txt b/tests/fixtures/coverage-reports/build_dir/build_dir.summary.txt new file mode 100644 index 00000000..51aa6940 --- /dev/null +++ b/tests/fixtures/coverage-reports/build_dir/build_dir.summary.txt @@ -0,0 +1,5 @@ +Filename Regions Missed Regions Cover Functions Missed Functions Executed Lines Missed Lines Cover Branches Missed Branches Cover +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +src/lib.rs 9 1 88.89% 2 0 100.00% 6 0 100.00% 0 0 - +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +TOTAL 9 1 88.89% 2 0 100.00% 6 0 100.00% 0 0 - \ No newline at end of file diff --git a/tests/fixtures/coverage-reports/build_dir/build_dir.txt b/tests/fixtures/coverage-reports/build_dir/build_dir.txt new file mode 100644 index 00000000..1982bc28 --- /dev/null +++ b/tests/fixtures/coverage-reports/build_dir/build_dir.txt @@ -0,0 +1,12 @@ + 1| 1|fn func(x: i32) -> bool { + 2| 1| if x < 0 { true } else { false } + ^0 + 3| 1|} + 4| | + 5| |#[test] + 6| 1|fn test() { + 7| | #[cfg(a)] + 8| | assert!(!func(1)); + 9| | #[cfg(not(a))] + 10| 1| assert!(func(-1)); + 11| 1|} \ No newline at end of file diff --git a/tests/fixtures/crates/build_dir/.cargo/config.toml b/tests/fixtures/crates/build_dir/.cargo/config.toml new file mode 100644 index 00000000..c5961193 --- /dev/null +++ b/tests/fixtures/crates/build_dir/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +build-dir = "{cargo-cache-home}/build/{workspace-path-hash}" diff --git a/tests/fixtures/crates/build_dir/.config/nextest.toml b/tests/fixtures/crates/build_dir/.config/nextest.toml new file mode 100644 index 00000000..b4945c7a --- /dev/null +++ b/tests/fixtures/crates/build_dir/.config/nextest.toml @@ -0,0 +1 @@ +[profile.ci] diff --git a/tests/fixtures/crates/build_dir/Cargo.toml b/tests/fixtures/crates/build_dir/Cargo.toml new file mode 100644 index 00000000..695ae015 --- /dev/null +++ b/tests/fixtures/crates/build_dir/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "build_dir" +version = "0.0.0" + +[workspace] diff --git a/tests/fixtures/crates/build_dir/src/lib.rs b/tests/fixtures/crates/build_dir/src/lib.rs new file mode 100644 index 00000000..f56d1824 --- /dev/null +++ b/tests/fixtures/crates/build_dir/src/lib.rs @@ -0,0 +1,11 @@ +fn func(x: i32) -> bool { + if x < 0 { true } else { false } +} + +#[test] +fn test() { + #[cfg(a)] + assert!(!func(1)); + #[cfg(not(a))] + assert!(func(-1)); +} diff --git a/tests/test.rs b/tests/test.rs index 317f48e8..efa8f8ac 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -123,6 +123,13 @@ fn cargo_config() { run("cargo_config_toml", "cargo_config_toml", &[], &[]); } +// build-dir requires Cargo 1.91. +#[rustversion::attr(before(1.91), ignore)] +#[test] +fn build_dir() { + run("build_dir", "build_dir", &[], &[]); +} + // feature(coverage_attribute) requires nightly #[rustversion::attr(not(nightly), ignore)] #[test]