use assert_fs::TempDir;
use assert_fs::prelude::*;
use cargo_feature_combinations::config::{Config, FlagConfig, WorkspaceConfig};
use cargo_feature_combinations::matrix::build_matrix_rows;
use cargo_feature_combinations::plan::execution::{PlanBuildContext, build_execution_plans};
use cargo_feature_combinations::plan::targets::{
SelectedPackage, TargetPlanRequest, build_target_plans,
};
use cargo_feature_combinations::target::TargetEnvironment;
use cargo_feature_combinations::workspace::Workspace as _;
use cargo_feature_combinations::{CfgEvaluator, Package as _};
use color_eyre::eyre;
mod common;
use common::{HostEnv, PairEval, metadata, single_crate};
fn matrix_rows_for_command(
meta: &cargo_metadata::Metadata,
command: Option<&str>,
env: &impl TargetEnvironment,
evaluator: &mut impl CfgEvaluator,
) -> eyre::Result<Vec<serde_json::Value>> {
let ws_config: WorkspaceConfig = meta.workspace_config()?;
let packages = meta.candidate_packages_for_fc()?;
let configs: Vec<Config> = packages
.iter()
.map(|p| p.config())
.collect::<eyre::Result<Vec<_>>>()?;
let selected: Vec<SelectedPackage<'_>> = packages
.iter()
.zip(&configs)
.map(|(package, config)| SelectedPackage {
package,
config,
ignore_configured_targets: false,
target_decision_explicit: false,
})
.collect();
let base_exclude = meta.base_workspace_exclude_packages()?;
let target_plans = build_target_plans(
&selected,
&ws_config,
&base_exclude,
TargetPlanRequest {
raw_command: command,
resolved_command: command,
..Default::default()
},
env,
evaluator,
)?;
let context = PlanBuildContext {
workspace_config: &ws_config,
raw_command: command,
resolved_command: command,
cli_driver: None,
default_diagnostics_allowed: false,
matrix: true,
};
let plan_set =
build_execution_plans(&target_plans, FlagConfig::default(), &context, evaluator)?;
Ok(build_matrix_rows(&plan_set))
}
fn any_row_has_feature(rows: &[serde_json::Value], feature: &str) -> bool {
rows.iter().any(|r| {
r.get("features")
.and_then(serde_json::Value::as_str)
.is_some_and(|f| f.split(',').any(|x| x == feature))
})
}
#[test]
fn subcommand_override_excludes_feature_only_for_that_command() -> eyre::Result<()> {
let temp = single_crate(
r#"
[package]
name = "solo"
version = "0.1.0"
edition = "2021"
[features]
gpu = []
cpu = []
[package.metadata.cargo-fc.subcommands.test]
exclude_features = { add = ["gpu"] }
"#,
)?;
let meta = metadata(&temp)?;
let env = HostEnv("host-triple");
let mut eval = PairEval::default();
let test_rows = matrix_rows_for_command(&meta, Some("test"), &env, &mut eval)?;
assert!(
!any_row_has_feature(&test_rows, "gpu"),
"feature `gpu` should be excluded for `cargo fc test`"
);
assert!(
any_row_has_feature(&test_rows, "cpu"),
"feature `cpu` should still be present for `cargo fc test`"
);
let mut eval = PairEval::default();
let build_rows = matrix_rows_for_command(&meta, Some("build"), &env, &mut eval)?;
assert!(
any_row_has_feature(&build_rows, "gpu"),
"feature `gpu` should remain for `cargo fc build`"
);
let mut eval = PairEval::default();
let no_command_rows = matrix_rows_for_command(&meta, None, &env, &mut eval)?;
assert!(any_row_has_feature(&no_command_rows, "gpu"));
Ok(())
}
#[test]
fn subcommand_only_features_restricts_matrix_for_that_command() -> eyre::Result<()> {
let temp = single_crate(
r#"
[package]
name = "solo"
version = "0.1.0"
edition = "2021"
[features]
core = []
extra = []
[package.metadata.cargo-fc.subcommands.test]
only_features = ["core"]
"#,
)?;
let meta = metadata(&temp)?;
let env = HostEnv("host-triple");
let mut eval = PairEval::default();
let test_rows = matrix_rows_for_command(&meta, Some("test"), &env, &mut eval)?;
assert!(!any_row_has_feature(&test_rows, "extra"));
assert_eq!(test_rows.len(), 2);
let mut eval = PairEval::default();
let build_rows = matrix_rows_for_command(&meta, Some("build"), &env, &mut eval)?;
assert!(any_row_has_feature(&build_rows, "extra"));
Ok(())
}
#[test]
fn target_and_subcommand_feature_overrides_compose() -> eyre::Result<()> {
let temp = single_crate(
r#"
[package]
name = "solo"
version = "0.1.0"
edition = "2021"
[features]
a = []
b = []
c = []
[package.metadata.cargo-fc]
targets = ["t-linux"]
[package.metadata.cargo-fc.target.'cfg(target_os = "linux")']
exclude_features = { add = ["a"] }
[package.metadata.cargo-fc.subcommands.test]
exclude_features = { add = ["b"] }
"#,
)?;
let meta = metadata(&temp)?;
let env = HostEnv("host-triple");
let mut eval = PairEval::matching("t-linux", "cfg(target_os = \"linux\")");
let test_rows = matrix_rows_for_command(&meta, Some("test"), &env, &mut eval)?;
assert!(!any_row_has_feature(&test_rows, "a"), "target excludes `a`");
assert!(
!any_row_has_feature(&test_rows, "b"),
"subcommand excludes `b`"
);
assert!(any_row_has_feature(&test_rows, "c"), "`c` is untouched");
let mut eval = PairEval::matching("t-linux", "cfg(target_os = \"linux\")");
let build_rows = matrix_rows_for_command(&meta, Some("build"), &env, &mut eval)?;
assert!(!any_row_has_feature(&build_rows, "a"));
assert!(any_row_has_feature(&build_rows, "b"));
Ok(())
}
#[test]
fn target_subcommand_override_narrows_to_target_and_command() -> eyre::Result<()> {
let temp = single_crate(
r#"
[package]
name = "solo"
version = "0.1.0"
edition = "2021"
[features]
a = []
b = []
[package.metadata.cargo-fc]
targets = ["t-linux"]
[package.metadata.cargo-fc.target.'cfg(target_os = "linux")'.subcommands.test]
exclude_features = { add = ["a"] }
"#,
)?;
let meta = metadata(&temp)?;
let env = HostEnv("host-triple");
let mut eval = PairEval::matching("t-linux", "cfg(target_os = \"linux\")");
let test_rows = matrix_rows_for_command(&meta, Some("test"), &env, &mut eval)?;
assert!(!any_row_has_feature(&test_rows, "a"));
let mut eval = PairEval::matching("t-linux", "cfg(target_os = \"linux\")");
let build_rows = matrix_rows_for_command(&meta, Some("build"), &env, &mut eval)?;
assert!(any_row_has_feature(&build_rows, "a"));
Ok(())
}
#[test]
fn subcommand_alias_resolves_to_builtin_override() -> eyre::Result<()> {
let temp = single_crate(
r#"
[package]
name = "solo"
version = "0.1.0"
edition = "2021"
[features]
gpu = []
[package.metadata.cargo-fc.subcommands.test]
exclude_features = { add = ["gpu"] }
"#,
)?;
let meta = metadata(&temp)?;
let env = HostEnv("host-triple");
let mut eval = PairEval::default();
let rows = matrix_rows_for_command(&meta, Some("t"), &env, &mut eval)?;
assert!(
!any_row_has_feature(&rows, "gpu"),
"the `t` alias should resolve to the `test` subcommand override"
);
Ok(())
}
fn row_package_names(rows: &[serde_json::Value]) -> std::collections::HashSet<String> {
rows.iter()
.filter_map(|r| r.get("name").and_then(serde_json::Value::as_str))
.map(ToString::to_string)
.collect()
}
#[test]
fn workspace_subcommand_excludes_package_for_that_command() -> eyre::Result<()> {
let temp = TempDir::new()?;
temp.child("Cargo.toml").write_str(
r#"
[workspace]
members = ["keep", "skip"]
resolver = "2"
[workspace.metadata.cargo-fc.subcommands.test]
exclude_packages = { add = ["skip"] }
"#,
)?;
for name in ["keep", "skip"] {
temp.child(format!("{name}/Cargo.toml"))
.write_str(&format!(
r#"
[package]
name = "{name}"
version = "0.1.0"
edition = "2021"
"#
))?;
temp.child(format!("{name}/src/lib.rs"))
.write_str("pub fn x() {}\n")?;
}
let meta = metadata(&temp)?;
let env = HostEnv("host-triple");
let mut eval = PairEval::default();
let test_names = row_package_names(&matrix_rows_for_command(
&meta,
Some("test"),
&env,
&mut eval,
)?);
assert!(test_names.contains("keep"));
assert!(!test_names.contains("skip"), "skip is excluded for test");
let mut eval = PairEval::default();
let build_names = row_package_names(&matrix_rows_for_command(
&meta,
Some("build"),
&env,
&mut eval,
)?);
assert!(build_names.contains("keep"));
assert!(build_names.contains("skip"), "skip participates for build");
Ok(())
}