diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 6bf25b5309..53d28cb33f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -35,6 +35,13 @@ jobs: with: cache-targets: true # cache build artifacts cache-bin: true # cache the ~/.cargo/bin directory + - name: Verify bin_tests contribution to coverage + run: | + cargo llvm-cov nextest --no-report --package bin_tests + echo "=== Profraw files created ===" + find target/llvm-cov-target -name "*.profraw" -ls + cargo llvm-cov report --package bin_tests --summary-only + cargo clean - name: Generate code coverage (including doc tests) run: | cargo llvm-cov --all-features --workspace --no-report nextest diff --git a/bin_tests/src/lib.rs b/bin_tests/src/lib.rs index 4aec1f6ef0..66e4edc325 100644 --- a/bin_tests/src/lib.rs +++ b/bin_tests/src/lib.rs @@ -60,6 +60,19 @@ fn inner_build_artifact(c: &ArtifactsBuild) -> anyhow::Result { }; build_cmd.arg(&c.name); + // Explicitly pass RUSTFLAGS if present to ensure instrumentation + // This is important for coverage collection when tests spawn separate binaries + if let Ok(rustflags) = env::var("RUSTFLAGS") { + build_cmd.env("RUSTFLAGS", rustflags); + } + + // Pass CARGO_TARGET_DIR to ensure we build in the same target directory + // that cargo-llvm-cov is using (e.g., target/llvm-cov-target/) + // Without this, cargo build would use target/debug/ and miss instrumentation + if let Ok(target_dir) = env::var("CARGO_TARGET_DIR") { + build_cmd.env("CARGO_TARGET_DIR", target_dir); + } + let output = build_cmd.output().unwrap(); if !output.status.success() { anyhow::bail!( @@ -178,3 +191,66 @@ macro_rules! timeit { res }}; } + +/// Propagates `LLVM_PROFILE_FILE` to a spawned process for coverage collection. +/// +/// This function is essential for integration tests that spawn separate processes to ensure +/// those child processes contribute their coverage data. It propagates **only** the +/// `LLVM_PROFILE_FILE` environment variable, which contains a pattern with `%p` (process ID) +/// that the LLVM profiling runtime expands at runtime to ensure each process writes to a +/// unique coverage file. +/// +/// # How It Works +/// +/// 1. **Parent propagates the pattern string:** ``` +/// LLVM_PROFILE_FILE="target/llvm-cov-target/profraw/cargo-test-%p-%m.profraw" ``` Note: `%p` +/// and `%m` are NOT expanded yet - they're literal characters in the string. +/// +/// 2. **Each process expands the pattern at runtime:** +/// - Parent (PID 1000): `%p` → `1000` → writes to `cargo-test-1000-abc.profraw` +/// - Child (PID 1001): `%p` → `1001` → writes to `cargo-test-1001-def.profraw` +/// - Result: Each process writes to a unique file! +/// +/// 3. **Report merges all files:** +/// - `cargo llvm-cov report` finds all `.profraw` files in the target directory +/// - Merges them into a unified coverage report +/// +/// # Why Only `LLVM_PROFILE_FILE`? +/// +/// Other `cargo-llvm-cov` variables like `CARGO_LLVM_COV` and `CARGO_LLVM_COV_TARGET_DIR` +/// are for `cargo` commands during build time, not for runtime binaries. Spawned test +/// binaries only need `LLVM_PROFILE_FILE` to write their coverage data. +/// +/// # Arguments +/// * `cmd` - The Command to add environment variables to (before spawning) +/// +/// # Example +/// ```no_run +/// use bin_tests::propagate_coverage_env; +/// use std::process::Command; +/// +/// let mut cmd = Command::new("path/to/test_binary"); +/// cmd.arg("--test-arg"); +/// +/// // Propagate coverage - spawned process will write coverage if instrumented +/// propagate_coverage_env(&mut cmd); +/// +/// let child = cmd.spawn().expect("Failed to spawn"); +/// ``` +pub fn propagate_coverage_env(cmd: &mut process::Command) { + // LLVM_PROFILE_FILE tells the instrumented binary where to write coverage data. + // + // This variable contains patterns like %p (process ID) and %m (module signature). + // The LLVM profiling runtime inside each instrumented binary expands these patterns + // at runtime, ensuring each spawned process writes to a unique file: + // + // Pattern: "cargo-test-%p-%m.profraw" + // PID 1000: "cargo-test-1000-abc123.profraw" + // PID 1001: "cargo-test-1001-def456.profraw" + // + // This is the ONLY variable needed for spawned binaries to contribute coverage. + // CARGO_LLVM_COV* variables are for cargo commands, not runtime binaries. + if let Ok(profile_file) = env::var("LLVM_PROFILE_FILE") { + cmd.env("LLVM_PROFILE_FILE", profile_file); + } +} diff --git a/bin_tests/src/test_runner.rs b/bin_tests/src/test_runner.rs index 8a179551c1..cff6a2c347 100644 --- a/bin_tests/src/test_runner.rs +++ b/bin_tests/src/test_runner.rs @@ -163,6 +163,9 @@ where .arg(config.mode.as_str()) .arg(config.crash_type.as_str()); + // Pass coverage-related environment variables to spawned processes + crate::propagate_coverage_env(&mut cmd); + for (key, val) in &config.env_vars { cmd.env(key, val); } @@ -245,6 +248,10 @@ where let fixtures = TestFixtures::new()?; let mut cmd = process::Command::new(binary_path); + + // Pass coverage-related environment variables to spawned processes + crate::propagate_coverage_env(&mut cmd); + command_builder(&mut cmd, &fixtures); let mut p = cmd.spawn().context("Failed to spawn test process")?;