use super::{CONFIG_PATH_ENV_VAR, TestPlan, TestRun, nextest};
use crate::common::cargo_cmd;
use crate::manifest::TestRunner;
use anyhow::{Context, bail};
use std::collections::BTreeSet;
use std::process::Command;
pub(super) fn run(plan: &TestPlan, runner: TestRunner) -> anyhow::Result<()> {
match runner {
TestRunner::Cargo => run_cargo_llvm_cov(plan),
TestRunner::Nextest => run_nextest_llvm_cov(plan),
}
}
fn run_cargo_llvm_cov(plan: &TestPlan) -> anyhow::Result<()> {
run_llvm_cov_clean(plan, &[])?;
for run in &plan.runs {
let mut cmd = llvm_cov_test_command(plan, run)?;
let status = cmd.status().context("failed to run llvm-cov test runner")?;
if !status.success() {
bail!("llvm-cov test runner exited with {status}");
}
}
run_llvm_cov_report(plan, &[])
}
fn run_nextest_llvm_cov(plan: &TestPlan) -> anyhow::Result<()> {
let coverage_env = llvm_cov_env(plan)?;
run_llvm_cov_clean(plan, &coverage_env)?;
for run in &plan.runs {
nextest::run_nextest(plan, run, &coverage_env)?;
}
run_llvm_cov_report(plan, &coverage_env)
}
fn llvm_cov_test_command(plan: &TestPlan, run: &TestRun) -> anyhow::Result<Command> {
let mut cmd = cargo_cmd()?;
cmd.arg("llvm-cov");
cmd.args(["--no-report", "--no-clean"]);
let mut args = Vec::new();
run.append_cargo_args(&mut args);
cmd.args(args);
cmd.current_dir(&plan.workspace_root);
cmd.env(CONFIG_PATH_ENV_VAR, &plan.config_path);
Ok(cmd)
}
fn llvm_cov_env(plan: &TestPlan) -> anyhow::Result<Vec<(String, String)>> {
let output = cargo_cmd()?
.args(["llvm-cov", "show-env", "--sh"])
.current_dir(&plan.workspace_root)
.output()
.context("failed to run llvm-cov show-env")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("llvm-cov show-env exited with {}: {stderr}", output.status);
}
let stdout = String::from_utf8(output.stdout).context("llvm-cov show-env was not UTF-8")?;
parse_llvm_cov_env(&stdout)
}
fn run_llvm_cov_clean(plan: &TestPlan, coverage_env: &[(String, String)]) -> anyhow::Result<()> {
let status = cargo_cmd()?
.args(["llvm-cov", "clean", "--workspace"])
.current_dir(&plan.workspace_root)
.envs(coverage_env.iter().map(|(key, value)| (key, value)))
.status()
.context("failed to run llvm-cov clean")?;
if !status.success() {
bail!("llvm-cov clean exited with {status}");
}
Ok(())
}
fn run_llvm_cov_report(plan: &TestPlan, coverage_env: &[(String, String)]) -> anyhow::Result<()> {
let mut cmd = llvm_cov_report_command(plan, coverage_env)?;
let status = cmd.status().context("failed to run llvm-cov report")?;
if !status.success() {
bail!("llvm-cov report exited with {status}");
}
Ok(())
}
fn llvm_cov_report_command(
plan: &TestPlan,
coverage_env: &[(String, String)],
) -> anyhow::Result<Command> {
let mut cmd = cargo_cmd()?;
let mut args = vec!["llvm-cov".to_owned(), "report".to_owned()];
append_report_package_args(&mut args, &plan.runs);
cmd.args(args);
cmd.current_dir(&plan.workspace_root);
cmd.envs(coverage_env.iter().map(|(key, value)| (key, value)));
Ok(cmd)
}
fn append_report_package_args(args: &mut Vec<String>, runs: &[TestRun]) {
if runs.iter().any(|run| run.package.is_none()) {
return;
}
let packages = runs
.iter()
.filter_map(|run| run.package.as_deref())
.collect::<BTreeSet<_>>();
for package in packages {
args.push("-p".to_owned());
args.push(package.to_owned());
}
}
fn parse_llvm_cov_env(output: &str) -> anyhow::Result<Vec<(String, String)>> {
output
.lines()
.filter(|line| !line.trim().is_empty())
.map(parse_llvm_cov_env_line)
.collect()
}
fn parse_llvm_cov_env_line(line: &str) -> anyhow::Result<(String, String)> {
let parts = shell_words::split(line)
.with_context(|| format!("failed to parse llvm-cov show-env line `{line}`"))?;
let assignment = match parts.as_slice() {
[assignment] => assignment.as_str(),
[export, assignment] if export == "export" => assignment.as_str(),
_ => bail!("unexpected llvm-cov show-env line `{line}`"),
};
let (key, value) = assignment
.split_once('=')
.with_context(|| format!("llvm-cov show-env line `{line}` is not KEY=VALUE"))?;
if key.is_empty() {
bail!("llvm-cov show-env line `{line}` has an empty key");
}
Ok((key.to_owned(), value.to_owned()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::FeatureSelection;
use std::path::PathBuf;
#[test]
fn llvm_cov_test_uses_package_and_feature_args_without_reporting() {
let plan = TestPlan {
workspace_root: PathBuf::from("/workspace"),
config_path: PathBuf::from("/workspace/config/app-dev.yml"),
runs: vec![],
};
let run = TestRun {
package: Some("cf-module".to_owned()),
features: FeatureSelection::Features(vec!["sqlite".to_owned(), "otel".to_owned()]),
};
let command =
llvm_cov_test_command(&plan, &run).expect("CARGO env var should exist under tests");
let args = command
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect::<Vec<_>>();
assert_eq!(
args,
vec![
"llvm-cov",
"--no-report",
"--no-clean",
"-p",
"cf-module",
"--no-default-features",
"--features",
"sqlite,otel"
]
);
assert_eq!(
command.get_current_dir(),
Some(plan.workspace_root.as_path())
);
assert_eq!(
command
.get_envs()
.find(|(key, _)| key == &CONFIG_PATH_ENV_VAR),
Some((
CONFIG_PATH_ENV_VAR.as_ref(),
Some(plan.config_path.as_os_str())
))
);
}
#[test]
fn llvm_cov_report_uses_selected_packages_without_feature_args() {
let plan = TestPlan {
workspace_root: PathBuf::from("/workspace"),
config_path: PathBuf::from("/workspace/config/app-dev.yml"),
runs: vec![
TestRun {
package: Some("cf-module-b".to_owned()),
features: FeatureSelection::AllFeatures,
},
TestRun {
package: Some("cf-module-a".to_owned()),
features: FeatureSelection::Features(vec!["sqlite".to_owned()]),
},
TestRun {
package: Some("cf-module-a".to_owned()),
features: FeatureSelection::NoDefaultFeatures,
},
],
};
let coverage_env = vec![(
"LLVM_PROFILE_FILE".to_owned(),
"target/%p-%m.profraw".to_owned(),
)];
let command = llvm_cov_report_command(&plan, &coverage_env)
.expect("CARGO env var should exist under tests");
let args = command
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect::<Vec<_>>();
assert_eq!(
args,
vec![
"llvm-cov",
"report",
"-p",
"cf-module-a",
"-p",
"cf-module-b"
]
);
assert_eq!(
command.get_current_dir(),
Some(plan.workspace_root.as_path())
);
let env_value = command
.get_envs()
.find(|(key, _)| key == &"LLVM_PROFILE_FILE")
.and_then(|(_, value)| value)
.map(|value| value.to_string_lossy().into_owned());
assert_eq!(env_value.as_deref(), Some("target/%p-%m.profraw"));
}
#[test]
fn parses_exported_llvm_cov_env() {
let env = parse_llvm_cov_env(
"export LLVM_PROFILE_FILE='target/llvm cov/%p-%m.profraw'\n\
export RUSTFLAGS=-Cinstrument-coverage\n",
)
.expect("show-env output should parse");
assert_eq!(
env,
vec![
(
"LLVM_PROFILE_FILE".to_owned(),
"target/llvm cov/%p-%m.profraw".to_owned()
),
("RUSTFLAGS".to_owned(), "-Cinstrument-coverage".to_owned()),
]
);
}
}