use super::{Feature, FeatureSet};
use crate::config::Config;
use anyhow::Result;
use cargo_metadata::Package;
use derive_more::{AsMut, AsRef, Deref, DerefMut};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeSet, HashSet},
ops::Deref as OpsDeref,
};
#[derive(AsMut, AsRef, Clone, Debug, Default, Deref, DerefMut, Deserialize, Serialize)]
#[serde(transparent)]
pub(crate) struct Matrix(BTreeSet<FeatureSet>);
impl Matrix {
pub(crate) fn new(package: &Package, config: &Config, channel: &str) -> Result<Self> {
let deny = config.always_deny(channel)?;
let skip = config.skip(channel)?;
let include = config.always_include(channel)?;
let mutually_exclusive = config.mutually_exclusive(channel)?;
Ok(Self::extract_seed(package, config, channel)?
.into_iter()
.powerset()
.map(FeatureSet::from_iter)
.map(|mut set| {
set.extend(include.clone());
set
})
.filter(|set| set.is_disjoint(&deny))
.filter(|set| !skip.iter().any(|skip| skip == set))
.filter(|set| {
mutually_exclusive
.iter()
.all(|group| set.intersection(group).count() < 2)
})
.collect())
}
fn extract_seed(package: &Package, config: &Config, channel: &str) -> Result<FeatureSet> {
Ok(if let Some(seed) = config.seed(channel)? {
seed.clone()
} else {
let implicit_features = Self::find_implicits(package);
let deny = config.always_deny(channel)?;
let include = config.always_include(channel)?;
let mut set: FeatureSet = package
.features
.keys()
.map(Into::into)
.filter(|feature: &Feature| **feature != "default")
.filter(|feature| !implicit_features.contains(feature))
.filter(|package| !deny.iter().contains(package))
.filter(|package| !include.iter().contains(package))
.filter(|feature| {
config.include_hidden(channel).unwrap_or_default() || !feature.starts_with("__")
})
.collect();
if config.include_all_optional(channel).unwrap_or_default() {
set.extend(
package
.dependencies
.iter()
.filter(|dependency| dependency.optional)
.map(|dependency| {
dependency
.rename
.as_deref()
.unwrap_or(&dependency.name)
.to_string()
})
.map(Feature),
);
}
set.extend(config.include_optional(channel)?);
set
})
}
fn find_implicits(package: &Package) -> HashSet<Feature> {
let mut implicit_features = HashSet::<Feature>::new();
let mut optional_dep: HashSet<Feature> = HashSet::new();
for (feature, implied_features) in &package.features {
for implied_dep in implied_features
.iter()
.filter_map(|v| v.strip_prefix("dep:"))
{
if implied_features.len() == 1 && implied_dep == feature {
let _ = implicit_features.insert(feature.clone().into());
} else {
let _ = optional_dep.insert(implied_dep.into());
}
}
}
for x in &optional_dep {
let _ = implicit_features.remove(x);
}
implicit_features
}
}
impl FromIterator<FeatureSet> for Matrix {
fn from_iter<T: IntoIterator<Item = FeatureSet>>(iter: T) -> Self {
Matrix(iter.into_iter().collect())
}
}
impl IntoIterator for Matrix {
type Item = FeatureSet;
type IntoIter = <<Self as OpsDeref>::Target as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use figment::{
Figment,
providers::{Format, Json},
};
fn fixture_package(relative_path: &str) -> Package {
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(relative_path);
let metadata = cargo_metadata::MetadataCommand::new()
.manifest_path(&manifest)
.exec()
.unwrap();
metadata.packages.into_iter().next().unwrap()
}
fn config_from_package(package: &Package) -> Config {
let figment = if let Some(meta) = package.metadata.get("cargo-matrix") {
Figment::from(Config::default()).merge(Figment::from(Json::string(&meta.to_string())))
} else {
Figment::from(Config::default())
};
Config::from(figment).unwrap()
}
fn feature_set(features: &[&str]) -> FeatureSet {
features.iter().map(|&s| Feature::from(s)).collect()
}
#[test]
fn mutually_exclusive_filters_pairs_from_same_group() {
let package = fixture_package("tests/testdata/mutually_exclusive/Cargo.toml");
let config = config_from_package(&package);
let matrix = Matrix::new(&package, &config, "default").unwrap();
assert!(!matrix.contains(&feature_set(&["feat-a", "feat-b"])));
assert!(!matrix.contains(&feature_set(&["feat-a", "feat-c"])));
assert!(!matrix.contains(&feature_set(&["feat-b", "feat-c"])));
assert!(!matrix.contains(&feature_set(&["feat-a", "feat-b", "feat-c"])));
assert!(!matrix.contains(&feature_set(&["feat-x", "feat-y"])));
assert!(!matrix.contains(&feature_set(&["feat-a", "feat-x", "feat-y"])));
}
#[test]
fn mutually_exclusive_allows_one_per_group() {
let package = fixture_package("tests/testdata/mutually_exclusive/Cargo.toml");
let config = config_from_package(&package);
let matrix = Matrix::new(&package, &config, "default").unwrap();
assert!(matrix.contains(&feature_set(&["feat-a", "feat-x"])));
assert!(matrix.contains(&feature_set(&["feat-b", "feat-y"])));
assert!(matrix.contains(&feature_set(&["feat-a"])));
assert!(matrix.contains(&feature_set(&["feat-b"])));
assert!(matrix.contains(&feature_set(&["feat-c"])));
assert!(matrix.contains(&feature_set(&["feat-x"])));
assert!(matrix.contains(&feature_set(&["feat-y"])));
}
#[test]
fn mutually_exclusive_allows_features_outside_groups() {
let package = fixture_package("tests/testdata/mutually_exclusive/Cargo.toml");
let config = config_from_package(&package);
let matrix = Matrix::new(&package, &config, "default").unwrap();
assert!(matrix.contains(&feature_set(&["feat-z"])));
assert!(matrix.contains(&feature_set(&["feat-a", "feat-z"])));
assert!(matrix.contains(&feature_set(&["feat-x", "feat-z"])));
assert!(matrix.contains(&feature_set(&["feat-a", "feat-x", "feat-z"])));
}
#[test]
fn empty_mutually_exclusive_filters_nothing() {
let package = fixture_package("tests/testdata/mutually_exclusive/Cargo.toml");
let config = Config::from(Figment::from(Config::default())).unwrap();
let matrix = Matrix::new(&package, &config, "default").unwrap();
assert!(matrix.contains(&feature_set(&["feat-a", "feat-b"])));
assert!(matrix.contains(&feature_set(&["feat-x", "feat-y"])));
}
#[test]
fn extract_seed_uses_config_seed_when_set() {
let package = fixture_package("tests/testdata/deny/Cargo.toml");
let config = Config::from(Figment::from(Config::default()).merge(Figment::from(
Json::string(r#"{"channel": [{"name": "default", "seed": ["feat-a", "feat-b"]}]}"#),
)))
.unwrap();
let seed = Matrix::extract_seed(&package, &config, "default").unwrap();
assert_eq!(seed, feature_set(&["feat-a", "feat-b"]));
}
#[test]
fn extract_seed_excludes_hidden_features_by_default() {
let package = fixture_package("tests/testdata/deny/Cargo.toml");
let config = config_from_package(&package);
let seed = Matrix::extract_seed(&package, &config, "default").unwrap();
assert!(!seed.contains(&Feature::from("__vergen_test")));
}
#[test]
fn extract_seed_includes_hidden_features_when_enabled() {
let package = fixture_package("tests/testdata/deny/Cargo.toml");
let config = Config::from(Figment::from(Config::default()).merge(Figment::from(
Json::string(r#"{"channel": [{"name": "default", "include_hidden": true}]}"#),
)))
.unwrap();
let seed = Matrix::extract_seed(&package, &config, "default").unwrap();
assert!(seed.contains(&Feature::from("__vergen_test")));
}
#[test]
fn extract_seed_excludes_implicit_dep_features() {
let package = fixture_package("tests/testdata/deny/Cargo.toml");
let config = Config::from(Figment::from(Config::default())).unwrap();
let seed = Matrix::extract_seed(&package, &config, "default").unwrap();
assert!(!seed.contains(&Feature::from("rand")));
assert!(!seed.contains(&Feature::from("temp-env")));
}
#[test]
fn extract_seed_include_all_optional_adds_optional_deps() {
let package = fixture_package("tests/testdata/deny/Cargo.toml");
let config = Config::from(Figment::from(Config::default()).merge(Figment::from(
Json::string(r#"{"channel": [{"name": "default", "include_all_optional": true}]}"#),
)))
.unwrap();
let seed = Matrix::extract_seed(&package, &config, "default").unwrap();
assert!(seed.contains(&Feature::from("rand")));
assert!(seed.contains(&Feature::from("temp-env")));
}
#[test]
fn find_implicits_identifies_implicit_dep_features() {
let package = fixture_package("tests/testdata/deny/Cargo.toml");
let implicits = Matrix::find_implicits(&package);
assert!(implicits.contains(&Feature::from("rand")));
assert!(implicits.contains(&Feature::from("temp-env")));
}
#[test]
fn find_implicits_empty_when_no_optional_deps() {
let package = fixture_package("tests/testdata/include/Cargo.toml");
let implicits = Matrix::find_implicits(&package);
assert!(implicits.is_empty());
}
#[test]
fn new_applies_always_deny_filter() {
let package = fixture_package("tests/testdata/deny/Cargo.toml");
let config = config_from_package(&package);
let matrix = Matrix::new(&package, &config, "default").unwrap();
for set in matrix.iter() {
assert!(!set.contains(&Feature::from("feat-d")));
assert!(!set.contains(&Feature::from("unstable")));
}
}
#[test]
fn new_applies_skip_filter() {
let package = fixture_package("tests/testdata/deny/Cargo.toml");
let config = config_from_package(&package);
let matrix = Matrix::new(&package, &config, "default").unwrap();
assert!(!matrix.contains(&feature_set(&["feat-c", "temp-env"])));
}
#[test]
fn new_produces_single_empty_set_for_no_features() {
let package = fixture_package("tests/testdata/include/Cargo.toml");
let config = config_from_package(&package);
let matrix = Matrix::new(&package, &config, "default").unwrap();
assert_eq!(matrix.len(), 1);
assert!(matrix.contains(&FeatureSet::default()));
}
}