use assert_fs::TempDir;
use assert_fs::prelude::*;
use cargo_feature_combinations::config::{FlagConfig, ResolvedFlags, WorkspaceConfig};
use cargo_feature_combinations::implication::prune_with_resolved_features;
use cargo_feature_combinations::plan::execution::{PlanBuildContext, build_execution_plans};
use cargo_feature_combinations::plan::targets::build_target_plans;
use cargo_feature_combinations::target::{TargetEnvironment, TargetTriple};
use cargo_feature_combinations::{CfgEvaluator, Package as _, ResolvedFeatures, resolve_config};
use color_eyre::eyre::{self, OptionExt};
use similar_asserts::assert_eq as sim_assert_eq;
use std::collections::HashSet;
fn dummy_crate(features_toml: &str, settings: &str) -> eyre::Result<TempDir> {
let temp = TempDir::new()?;
let cargotoml = temp.child("Cargo.toml");
cargotoml.write_str(&indoc::formatdoc!(
r#"
[package]
name = "testpruning"
version = "0.1.0"
edition = "2024"
{features_toml}
[package.metadata.cargo-feature-combinations]
{settings}
"#,
))?;
temp.child("src/lib.rs").write_str("pub fn main() {}\n")?;
Ok(temp)
}
fn dummy_crate_with_dep(features_toml: &str, settings: &str) -> eyre::Result<TempDir> {
let temp = TempDir::new()?;
let dep_dir = temp.child("optDep");
dep_dir
.child("Cargo.toml")
.write_str("[package]\nname = \"optDep\"\nversion = \"0.1.0\"\nedition = \"2024\"\n")?;
dep_dir
.child("src/lib.rs")
.write_str("pub fn dummy() {}\n")?;
let cargotoml = temp.child("Cargo.toml");
cargotoml.write_str(&indoc::formatdoc!(
r#"
[package]
name = "testpruning"
version = "0.1.0"
edition = "2024"
{features_toml}
[dependencies]
optDep = {{ path = "optDep", optional = true }}
[package.metadata.cargo-feature-combinations]
{settings}
"#,
))?;
temp.child("src/lib.rs").write_str("pub fn main() {}\n")?;
Ok(temp)
}
struct PruneTestResult {
kept: Vec<Vec<String>>,
pruned: Vec<(Vec<String>, Vec<String>)>,
}
struct TestEnv;
impl TargetEnvironment for TestEnv {
fn cargo_build_target(&self) -> Option<String> {
None
}
fn host_target(&self) -> eyre::Result<TargetTriple> {
Ok(TargetTriple("host".to_string()))
}
}
fn run_prune_test(features_toml: &str, settings: &str) -> eyre::Result<PruneTestResult> {
let temp = dummy_crate(features_toml, settings)?;
run_prune_in_dir(&temp)
}
fn run_prune_test_with_dep(features_toml: &str, settings: &str) -> eyre::Result<PruneTestResult> {
let temp = dummy_crate_with_dep(features_toml, settings)?;
run_prune_in_dir(&temp)
}
fn run_prune_in_dir(temp: &TempDir) -> eyre::Result<PruneTestResult> {
let metadata = cargo_metadata::MetadataCommand::new()
.current_dir(temp.path())
.no_deps()
.exec()?;
let pkg = metadata
.packages
.iter()
.find(|p| p.name == "testpruning")
.ok_or_eyre("test package should exist")?;
let config = pkg.config()?;
let resolved = ResolvedFeatures::from_config(&config);
let combos = pkg.feature_combinations(&resolved)?;
let flags = ResolvedFlags::from_config(config.base.settings.flags);
let result =
prune_with_resolved_features(combos, &pkg.features, &resolved, flags.no_prune_implied);
let mut kept: Vec<Vec<String>> = result
.keep
.into_iter()
.map(|c| c.into_iter().cloned().collect())
.collect();
kept.sort();
let mut pruned: Vec<(Vec<String>, Vec<String>)> = result
.pruned
.into_iter()
.map(|p| (p.features, p.equivalent_to))
.collect();
pruned.sort();
Ok(PruneTestResult { kept, pruned })
}
fn vs(strs: &[&str]) -> Vec<String> {
strs.iter().copied().map(String::from).collect()
}
#[test]
fn simple_implication() -> eyre::Result<()> {
let result = run_prune_test(
indoc::indoc! {r#"
[features]
A = []
B = ["A"]
C = []
"#},
"",
)?;
sim_assert_eq!(
result.kept,
vec![
vs(&[]),
vs(&["A"]),
vs(&["A", "C"]),
vs(&["B"]),
vs(&["B", "C"]),
vs(&["C"]),
]
);
sim_assert_eq!(result.pruned.len(), 2);
assert!(
result
.pruned
.iter()
.any(|(f, e)| f == &vs(&["A", "B"]) && e == &vs(&["B"]))
);
assert!(
result
.pruned
.iter()
.any(|(f, e)| f == &vs(&["A", "B", "C"]) && e == &vs(&["B", "C"]))
);
Ok(())
}
#[test]
fn transitive_chain() -> eyre::Result<()> {
let result = run_prune_test(
indoc::indoc! {r#"
[features]
A = []
B = ["A"]
C = ["B"]
"#},
"",
)?;
sim_assert_eq!(
result.kept,
vec![vs(&[]), vs(&["A"]), vs(&["B"]), vs(&["C"])]
);
sim_assert_eq!(result.pruned.len(), 4);
Ok(())
}
#[test]
fn include_features_no_false_pruning() -> eyre::Result<()> {
let result = run_prune_test(
indoc::indoc! {r#"
[features]
A = []
B = ["A"]
"#},
indoc::indoc! {r#"
include_features = ["A"]
"#},
)?;
sim_assert_eq!(result.kept, vec![vs(&["A"]), vs(&["A", "B"])]);
sim_assert_eq!(result.pruned.len(), 0);
Ok(())
}
#[test]
fn disabled_via_config() -> eyre::Result<()> {
let result = run_prune_test(
indoc::indoc! {r#"
[features]
A = []
B = ["A"]
"#},
indoc::indoc! {r"
prune_implied = false
"},
)?;
sim_assert_eq!(result.kept.len(), 4);
sim_assert_eq!(result.pruned.len(), 0);
Ok(())
}
#[test]
fn disabled_via_no_prune_implied_config() -> eyre::Result<()> {
let result = run_prune_test(
indoc::indoc! {r#"
[features]
A = []
B = ["A"]
"#},
indoc::indoc! {r"
no_prune_implied = true
"},
)?;
sim_assert_eq!(result.kept.len(), 4);
sim_assert_eq!(result.pruned.len(), 0);
Ok(())
}
#[test]
fn allow_feature_sets_bypasses_pruning() -> eyre::Result<()> {
let result = run_prune_test(
indoc::indoc! {r#"
[features]
A = []
B = ["A"]
"#},
indoc::indoc! {r#"
allow_feature_sets = [["A"], ["A", "B"], ["B"]]
"#},
)?;
sim_assert_eq!(result.kept, vec![vs(&["A"]), vs(&["A", "B"]), vs(&["B"])]);
sim_assert_eq!(result.pruned.len(), 0);
Ok(())
}
#[test]
fn dep_syntax_not_treated_as_implication() -> eyre::Result<()> {
let result = run_prune_test_with_dep(
indoc::indoc! {r#"
[features]
A = []
B = ["dep:optDep", "A"]
"#},
"",
)?;
sim_assert_eq!(result.kept, vec![vs(&[]), vs(&["A"]), vs(&["B"])]);
sim_assert_eq!(result.pruned, vec![(vs(&["A", "B"]), vs(&["B"]))]);
Ok(())
}
#[test]
fn diamond_graph() -> eyre::Result<()> {
let result = run_prune_test(
indoc::indoc! {r#"
[features]
A = []
B = ["A"]
C = ["A"]
D = ["B", "C"]
"#},
"",
)?;
sim_assert_eq!(
result.kept,
vec![
vs(&[]),
vs(&["A"]),
vs(&["B"]),
vs(&["B", "C"]),
vs(&["C"]),
vs(&["D"]),
]
);
sim_assert_eq!(result.pruned.len(), 10);
Ok(())
}
#[test]
fn no_implications_no_pruning() -> eyre::Result<()> {
let result = run_prune_test(
indoc::indoc! {"
[features]
A = []
B = []
C = []
"},
"",
)?;
sim_assert_eq!(result.kept.len(), 8); sim_assert_eq!(result.pruned.len(), 0);
Ok(())
}
#[test]
fn isolated_feature_sets_with_pruning() -> eyre::Result<()> {
let result = run_prune_test(
indoc::indoc! {r#"
[features]
A = []
B = ["A"]
C = []
D = []
"#},
indoc::indoc! {r#"
isolated_feature_sets = [["A", "B"], ["C", "D"]]
"#},
)?;
assert!(
!result.kept.contains(&vs(&["A", "B"])),
"[A, B] should be pruned"
);
assert!(
result.kept.contains(&vs(&["B"])),
"[B] should be kept as the representative"
);
assert!(result.kept.contains(&vs(&["C", "D"])));
sim_assert_eq!(result.pruned.len(), 1);
Ok(())
}
#[test]
fn exclude_feature_sets_with_pruning() -> eyre::Result<()> {
let result = run_prune_test(
indoc::indoc! {r#"
[features]
A = []
B = ["A"]
C = []
"#},
indoc::indoc! {r#"
exclude_feature_sets = [["B", "C"]]
"#},
)?;
assert!(!result.kept.contains(&vs(&["A", "B"])));
assert!(!result.kept.contains(&vs(&["B", "C"])));
assert!(!result.kept.contains(&vs(&["A", "B", "C"])));
assert!(result.kept.contains(&vs(&["B"])));
Ok(())
}
#[test]
fn no_empty_feature_set_with_pruning() -> eyre::Result<()> {
let result = run_prune_test(
indoc::indoc! {r#"
[features]
A = []
B = ["A"]
"#},
indoc::indoc! {r"
no_empty_feature_set = true
"},
)?;
sim_assert_eq!(result.kept, vec![vs(&["A"]), vs(&["B"])]);
sim_assert_eq!(result.pruned.len(), 1);
Ok(())
}
#[derive(Default)]
struct StubEval {
matches: std::collections::HashSet<String>,
}
impl CfgEvaluator for StubEval {
fn matches(&mut self, cfg_expr: &str, _target: &TargetTriple) -> eyre::Result<bool> {
Ok(self.matches.contains(cfg_expr))
}
}
fn resolve_and_prune(temp: &TempDir, matching_cfgs: &[&str]) -> eyre::Result<PruneTestResult> {
let metadata = cargo_metadata::MetadataCommand::new()
.current_dir(temp.path())
.no_deps()
.exec()?;
let pkg = metadata
.packages
.iter()
.find(|p| p.name == "testpruning")
.ok_or_eyre("test package should exist")?;
let base = pkg.config()?;
let mut eval = StubEval::default();
for &cfg in matching_cfgs {
eval.matches.insert(cfg.to_string());
}
let config = resolve_config(&base, &TargetTriple("x".to_string()), &mut eval)?;
let combos = pkg.feature_combinations(&config)?;
let result = prune_with_resolved_features(combos, &pkg.features, &config, false);
let mut kept: Vec<Vec<String>> = result
.keep
.into_iter()
.map(|c| c.into_iter().cloned().collect())
.collect();
kept.sort();
let mut pruned: Vec<(Vec<String>, Vec<String>)> = result
.pruned
.into_iter()
.map(|p| (p.features, p.equivalent_to))
.collect();
pruned.sort();
Ok(PruneTestResult { kept, pruned })
}
fn execute_and_prune(temp: &TempDir, matching_cfgs: &[&str]) -> eyre::Result<PruneTestResult> {
let metadata = cargo_metadata::MetadataCommand::new()
.current_dir(temp.path())
.no_deps()
.exec()?;
let pkg = metadata
.packages
.iter()
.find(|p| p.name == "testpruning")
.ok_or_eyre("test package should exist")?;
let config = pkg.config()?;
let selected = vec![cargo_feature_combinations::plan::targets::SelectedPackage {
package: pkg,
config: &config,
ignore_configured_targets: false,
target_decision_explicit: true,
}];
let ws = WorkspaceConfig::default();
let env = TestEnv;
let mut planning_eval = StubEval::default();
let target_plans = build_target_plans(
&selected,
&ws,
&HashSet::new(),
cargo_feature_combinations::plan::targets::TargetPlanRequest {
expansion: cargo_feature_combinations::plan::targets::TargetExpansion::Explicit {
triple: "x",
force: false,
},
raw_command: None,
resolved_command: None,
},
&env,
&mut planning_eval,
)?;
let mut execution_eval = StubEval::default();
for &cfg in matching_cfgs {
execution_eval.matches.insert(cfg.to_string());
}
let context = PlanBuildContext {
workspace_config: &ws,
raw_command: None,
resolved_command: None,
cli_driver: None,
cli_env_set: &[],
cli_env_remove: &[],
default_diagnostics_allowed: true,
host: None,
matrix: false,
};
let plan_set = build_execution_plans(
&target_plans,
FlagConfig::default(),
&context,
&mut execution_eval,
)?;
let package_plan = plan_set
.plans
.first()
.and_then(|plan| plan.package_plans.first())
.ok_or_eyre("expected one package execution plan")?;
let mut kept = package_plan.combinations.clone();
kept.sort();
let mut pruned = package_plan
.pruned
.iter()
.map(|entry| (entry.features.clone(), entry.equivalent_to.clone()))
.collect::<Vec<_>>();
pruned.sort();
Ok(PruneTestResult { kept, pruned })
}
#[test]
fn target_override_changes_matrix_then_pruning_applies() -> eyre::Result<()> {
let temp = TempDir::new()?;
temp.child("Cargo.toml").write_str(indoc::indoc! {r#"
[package]
name = "testpruning"
version = "0.1.0"
edition = "2024"
[features]
A = []
B = ["A"]
C = []
[package.metadata.cargo-feature-combinations.target.'cfg(target_os = "linux")']
exclude_features = { add = ["C"] }
"#})?;
temp.child("src/lib.rs").write_str("")?;
let linux = resolve_and_prune(&temp, &["cfg(target_os = \"linux\")"])?;
sim_assert_eq!(linux.kept, vec![vs(&[]), vs(&["A"]), vs(&["B"])]);
sim_assert_eq!(linux.pruned, vec![(vs(&["A", "B"]), vs(&["B"]))]);
let other = resolve_and_prune(&temp, &[])?;
sim_assert_eq!(
other.kept,
vec![
vs(&[]),
vs(&["A"]),
vs(&["A", "C"]),
vs(&["B"]),
vs(&["B", "C"]),
vs(&["C"]),
]
);
sim_assert_eq!(other.pruned.len(), 2);
Ok(())
}
#[test]
fn target_override_disables_pruning_for_specific_target() -> eyre::Result<()> {
let temp = TempDir::new()?;
temp.child("Cargo.toml").write_str(indoc::indoc! {r#"
[package]
name = "testpruning"
version = "0.1.0"
edition = "2024"
[features]
A = []
B = ["A"]
[package.metadata.cargo-feature-combinations.target.'cfg(target_os = "macos")']
prune_implied = false
"#})?;
temp.child("src/lib.rs").write_str("")?;
let macos = execute_and_prune(&temp, &["cfg(target_os = \"macos\")"])?;
sim_assert_eq!(macos.kept.len(), 4); sim_assert_eq!(macos.pruned.len(), 0);
let other = execute_and_prune(&temp, &[])?;
sim_assert_eq!(other.kept, vec![vs(&[]), vs(&["A"]), vs(&["B"])]);
sim_assert_eq!(other.pruned.len(), 1);
Ok(())
}
#[test]
fn target_override_include_features_interacts_with_pruning() -> eyre::Result<()> {
let temp = TempDir::new()?;
temp.child("Cargo.toml").write_str(indoc::indoc! {r#"
[package]
name = "testpruning"
version = "0.1.0"
edition = "2024"
[features]
A = []
B = ["A"]
[package.metadata.cargo-feature-combinations.target.'cfg(target_os = "linux")']
include_features = { add = ["A"] }
"#})?;
temp.child("src/lib.rs").write_str("")?;
let linux = resolve_and_prune(&temp, &["cfg(target_os = \"linux\")"])?;
sim_assert_eq!(linux.kept, vec![vs(&["A"]), vs(&["A", "B"])]);
sim_assert_eq!(linux.pruned.len(), 0);
let other = resolve_and_prune(&temp, &[])?;
sim_assert_eq!(other.kept, vec![vs(&[]), vs(&["A"]), vs(&["B"])]);
sim_assert_eq!(other.pruned.len(), 1);
Ok(())
}