use serde::{Deserialize, Deserializer, Serialize, de};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ConsumerScope {
#[default]
Open,
Workspace,
}
impl ConsumerScope {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Open => "open",
Self::Workspace => "workspace",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct UnifyConfig {
#[serde(default = "default_include_paths")]
pub include_paths: bool,
#[serde(default)]
pub include_renamed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transitive_pinning: Option<TransitivePinning>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub include: Vec<String>,
#[serde(default = "default_max_backups")]
pub max_backups: usize,
#[serde(default)]
pub msrv_policy: MsrvPolicy,
#[serde(default)]
pub consumer_scope: ConsumerScope,
#[serde(default)]
pub preserve_features: Vec<String>,
#[serde(default = "default_true")]
pub strict_version_compat: bool,
#[serde(default)]
pub exact_pin_handling: ExactPinHandling,
#[serde(default)]
pub major_version_conflict: MajorVersionConflict,
#[serde(default = "default_skip_undeclared_patterns")]
pub skip_undeclared_patterns: Vec<String>,
}
impl Default for UnifyConfig {
fn default() -> Self {
Self {
include_paths: default_include_paths(),
include_renamed: false,
transitive_pinning: None,
exclude: Vec::new(),
include: Vec::new(),
max_backups: default_max_backups(),
msrv_policy: MsrvPolicy::default(),
consumer_scope: ConsumerScope::default(),
preserve_features: Vec::new(),
strict_version_compat: true,
exact_pin_handling: ExactPinHandling::default(),
major_version_conflict: MajorVersionConflict::default(),
skip_undeclared_patterns: default_skip_undeclared_patterns(),
}
}
}
impl UnifyConfig {
pub fn should_exclude(&self, dep_name: &str) -> bool {
self.exclude.iter().any(|e| e == dep_name)
}
pub fn should_include(&self, dep_name: &str) -> bool {
self.include.iter().any(|i| i == dep_name)
}
pub fn should_preserve_feature(&self, feature_name: &str) -> bool {
self.preserve_features.iter().any(|pattern| {
if pattern.contains('*') || pattern.contains('?') || pattern.contains('[') {
glob::Pattern::new(pattern)
.map(|p| p.matches(feature_name))
.unwrap_or(false)
} else {
pattern == feature_name
}
})
}
pub fn should_skip_undeclared_feature(&self, feature_name: &str) -> bool {
self.skip_undeclared_patterns.iter().any(|pattern| {
if pattern.contains('*') || pattern.contains('?') || pattern.contains('[') {
glob::Pattern::new(pattern)
.map(|p| p.matches(feature_name))
.unwrap_or(false)
} else {
pattern == feature_name
}
})
}
pub fn validate(&self, workspace_root: &std::path::Path) -> Result<(), crate::error::ConfigError> {
if let Some(TransitivePinning {
host: TransitiveFeatureHost::Path(p),
}) = &self.transitive_pinning
{
if p.contains("..") {
return Err(crate::error::ConfigError::InvalidValue {
field: "unify.transitive_pinning.host".to_string(),
message: format!("path '{}' contains '..' traversal, which is not allowed", p),
});
}
if std::path::Path::new(p).is_absolute() {
return Err(crate::error::ConfigError::InvalidValue {
field: "unify.transitive_pinning.host".to_string(),
message: format!("path '{}' is absolute, must be relative to workspace root", p),
});
}
let full_path = workspace_root.join(p);
if !full_path.exists() {
return Err(crate::error::ConfigError::InvalidValue {
field: "unify.transitive_pinning.host".to_string(),
message: format!("path '{}' does not exist", p),
});
}
let cargo_toml = full_path.join("Cargo.toml");
if !cargo_toml.exists() {
return Err(crate::error::ConfigError::InvalidValue {
field: "unify.transitive_pinning.host".to_string(),
message: format!("path '{}' does not contain a Cargo.toml", p),
});
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MsrvSource {
Deps,
Workspace,
#[default]
Max,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ExactPinHandling {
Skip,
Preserve,
#[default]
Warn,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MajorVersionConflict {
#[default]
Warn,
Bump,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum TransitiveFeatureHost {
#[default]
Root,
Path(String),
}
impl Serialize for TransitiveFeatureHost {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
TransitiveFeatureHost::Root => serializer.serialize_str("root"),
TransitiveFeatureHost::Path(path) => serializer.serialize_str(path),
}
}
}
impl<'de> Deserialize<'de> for TransitiveFeatureHost {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct TransitiveFeatureHostVisitor;
impl serde::de::Visitor<'_> for TransitiveFeatureHostVisitor {
type Value = TransitiveFeatureHost;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("'root' or a path string")
}
fn visit_str<E>(self, value: &str) -> Result<TransitiveFeatureHost, E>
where
E: serde::de::Error,
{
match value {
"root" => Ok(TransitiveFeatureHost::Root),
path => Ok(TransitiveFeatureHost::Path(path.to_string())),
}
}
}
deserializer.deserialize_any(TransitiveFeatureHostVisitor)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TransitivePinning {
#[serde(default)]
pub host: TransitiveFeatureHost,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "lowercase", deny_unknown_fields)]
pub enum MsrvPolicy {
Disabled,
Compute {
#[serde(default)]
source: MsrvSource,
#[serde(default)]
inherit: bool,
},
}
impl Default for MsrvPolicy {
fn default() -> Self {
Self::Compute {
source: MsrvSource::default(),
inherit: false,
}
}
}
impl MsrvPolicy {
pub const fn source(self) -> Option<MsrvSource> {
match self {
Self::Disabled => None,
Self::Compute { source, .. } => Some(source),
}
}
pub const fn inherits(self) -> bool {
matches!(self, Self::Compute { inherit: true, .. })
}
}
#[derive(Deserialize)]
#[serde(default)]
struct UnifyConfigInput {
include_paths: bool,
include_renamed: bool,
transitive_pinning: Option<TransitivePinning>,
pin_transitives: Option<bool>,
transitive_host: Option<TransitiveFeatureHost>,
exclude: Vec<String>,
include: Vec<String>,
max_backups: usize,
msrv_policy: Option<MsrvPolicy>,
msrv: Option<bool>,
enforce_msrv_inheritance: Option<bool>,
msrv_source: Option<MsrvSource>,
consumer_scope: ConsumerScope,
preserve_features: Vec<String>,
strict_version_compat: bool,
exact_pin_handling: ExactPinHandling,
major_version_conflict: MajorVersionConflict,
skip_undeclared_patterns: Vec<String>,
}
impl Default for UnifyConfigInput {
fn default() -> Self {
let config = UnifyConfig::default();
Self {
include_paths: config.include_paths,
include_renamed: config.include_renamed,
transitive_pinning: None,
pin_transitives: None,
transitive_host: None,
exclude: config.exclude,
include: config.include,
max_backups: config.max_backups,
msrv_policy: None,
msrv: None,
enforce_msrv_inheritance: None,
msrv_source: None,
consumer_scope: config.consumer_scope,
preserve_features: config.preserve_features,
strict_version_compat: config.strict_version_compat,
exact_pin_handling: config.exact_pin_handling,
major_version_conflict: config.major_version_conflict,
skip_undeclared_patterns: config.skip_undeclared_patterns,
}
}
}
impl<'de> Deserialize<'de> for UnifyConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let input = UnifyConfigInput::deserialize(deserializer)?;
let has_legacy_transitive = input.pin_transitives.is_some() || input.transitive_host.is_some();
if input.transitive_pinning.is_some() && has_legacy_transitive {
return Err(de::Error::custom(
"unify.transitive_pinning cannot be combined with deprecated pin_transitives or transitive_host; run `cargo rail config migrate`",
));
}
let transitive_pinning = input.transitive_pinning.or_else(|| {
input.pin_transitives.unwrap_or(false).then(|| TransitivePinning {
host: input.transitive_host.unwrap_or_default(),
})
});
let has_legacy_msrv =
input.msrv.is_some() || input.enforce_msrv_inheritance.is_some() || input.msrv_source.is_some();
if input.msrv_policy.is_some() && has_legacy_msrv {
return Err(de::Error::custom(
"unify.msrv_policy cannot be combined with deprecated msrv, msrv_source, or enforce_msrv_inheritance; run `cargo rail config migrate`",
));
}
let msrv_policy = if let Some(policy) = input.msrv_policy {
policy
} else {
let enabled = input.msrv.unwrap_or(true);
let inherit = input.enforce_msrv_inheritance.unwrap_or(false);
if !enabled && inherit {
return Err(de::Error::custom(
"deprecated enforce_msrv_inheritance = true cannot be combined with msrv = false",
));
}
if enabled {
MsrvPolicy::Compute {
source: input.msrv_source.unwrap_or_default(),
inherit,
}
} else {
MsrvPolicy::Disabled
}
};
Ok(Self {
include_paths: input.include_paths,
include_renamed: input.include_renamed,
transitive_pinning,
exclude: input.exclude,
include: input.include,
max_backups: input.max_backups,
msrv_policy,
consumer_scope: input.consumer_scope,
preserve_features: input.preserve_features,
strict_version_compat: input.strict_version_compat,
exact_pin_handling: input.exact_pin_handling,
major_version_conflict: input.major_version_conflict,
skip_undeclared_patterns: input.skip_undeclared_patterns,
})
}
}
fn default_max_backups() -> usize {
3
}
fn default_include_paths() -> bool {
true
}
pub(crate) fn default_true() -> bool {
true
}
fn default_skip_undeclared_patterns() -> Vec<String> {
const PATTERNS: &[&str] = &["default", "std", "alloc", "*_backend", "*_impl"];
PATTERNS.iter().map(|&s| String::from(s)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unify_config_defaults() {
let config = UnifyConfig::default();
assert!(config.include_paths); assert!(!config.include_renamed); assert!(config.transitive_pinning.is_none());
assert!(config.exclude.is_empty());
assert!(config.include.is_empty());
assert_eq!(config.msrv_policy.source(), Some(MsrvSource::Max));
assert_eq!(config.consumer_scope, ConsumerScope::Open);
}
#[test]
fn test_unify_config_should_exclude() {
let config = UnifyConfig {
exclude: vec!["tokio".to_string(), "serde".to_string()],
..Default::default()
};
assert!(config.should_exclude("tokio"));
assert!(config.should_exclude("serde"));
assert!(!config.should_exclude("regex"));
}
#[test]
fn test_unify_config_should_include() {
let config = UnifyConfig {
include: vec!["special-dep".to_string()],
..Default::default()
};
assert!(config.should_include("special-dep"));
assert!(!config.should_include("normal-dep"));
}
#[test]
fn test_transitive_feature_host_in_full_config() {
let toml = r#"
include_paths = true
include_renamed = false
pin_transitives = true
transitive_host = "root"
exclude = []
include = []
"#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert_eq!(
config.transitive_pinning,
Some(TransitivePinning {
host: TransitiveFeatureHost::Root
})
);
assert!(config.include_paths);
}
#[test]
fn test_unify_config_default_transitive_host() {
let config = UnifyConfig::default();
assert!(config.transitive_pinning.is_none());
}
#[test]
fn typed_unify_policies_parse_without_invalid_combinations() {
let config: UnifyConfig = toml_edit::de::from_str(
r#"
transitive_pinning = { host = "crates/host" }
msrv_policy = { mode = "compute", source = "workspace", inherit = true }
"#,
)
.unwrap();
assert_eq!(
config.transitive_pinning,
Some(TransitivePinning {
host: TransitiveFeatureHost::Path("crates/host".to_string())
})
);
assert_eq!(
config.msrv_policy,
MsrvPolicy::Compute {
source: MsrvSource::Workspace,
inherit: true
}
);
}
#[test]
fn invalid_legacy_msrv_combination_cannot_be_constructed() {
let error = toml_edit::de::from_str::<UnifyConfig>("msrv = false\nenforce_msrv_inheritance = true\n").unwrap_err();
assert!(error.to_string().contains("cannot be combined"));
}
#[test]
fn test_consumer_scope_requires_explicit_workspace_contract() {
let default = UnifyConfig::default();
assert_eq!(default.consumer_scope, ConsumerScope::Open);
let config: UnifyConfig = toml_edit::de::from_str("consumer_scope = \"workspace\"").unwrap();
assert_eq!(config.consumer_scope, ConsumerScope::Workspace);
let invalid = toml_edit::de::from_str::<UnifyConfig>("consumer_scope = \"private\"");
assert!(
invalid.is_err(),
"unknown trust boundaries must fail configuration parsing"
);
}
#[test]
fn test_strict_version_compat_default() {
let config = UnifyConfig::default();
assert!(config.strict_version_compat); }
#[test]
fn test_exact_pin_handling_default() {
let config = UnifyConfig::default();
assert_eq!(config.exact_pin_handling, ExactPinHandling::Warn);
}
#[test]
fn test_exact_pin_handling_parsing() {
let toml = r#"exact_pin_handling = "skip""#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert_eq!(config.exact_pin_handling, ExactPinHandling::Skip);
let toml = r#"exact_pin_handling = "preserve""#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert_eq!(config.exact_pin_handling, ExactPinHandling::Preserve);
let toml = r#"exact_pin_handling = "warn""#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert_eq!(config.exact_pin_handling, ExactPinHandling::Warn);
}
#[test]
fn test_new_config_options_parsing() {
let toml = r#"
strict_version_compat = false
exact_pin_handling = "preserve"
"#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert!(!config.strict_version_compat);
assert_eq!(config.exact_pin_handling, ExactPinHandling::Preserve);
}
#[test]
fn test_major_version_conflict_default() {
let config = UnifyConfig::default();
assert_eq!(config.major_version_conflict, MajorVersionConflict::Warn);
}
#[test]
fn test_major_version_conflict_parsing() {
let toml = r#"major_version_conflict = "warn""#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert_eq!(config.major_version_conflict, MajorVersionConflict::Warn);
let toml = r#"major_version_conflict = "bump""#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert_eq!(config.major_version_conflict, MajorVersionConflict::Bump);
}
#[test]
fn test_major_version_conflict_with_other_options() {
let toml = r#"
strict_version_compat = false
exact_pin_handling = "preserve"
major_version_conflict = "bump"
"#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert!(!config.strict_version_compat);
assert_eq!(config.exact_pin_handling, ExactPinHandling::Preserve);
assert_eq!(config.major_version_conflict, MajorVersionConflict::Bump);
}
#[test]
fn test_transitive_host_validate_root() {
let config = UnifyConfig {
transitive_pinning: Some(TransitivePinning {
host: TransitiveFeatureHost::Root,
}),
..Default::default()
};
let workspace = std::env::current_dir().unwrap();
assert!(config.validate(&workspace).is_ok());
}
#[test]
fn test_transitive_host_validate_valid_path() {
let config = UnifyConfig::default();
let workspace = std::env::current_dir().unwrap();
assert!(config.validate(&workspace).is_ok());
}
#[test]
fn test_transitive_host_validate_nonexistent_path() {
let config = UnifyConfig {
transitive_pinning: Some(TransitivePinning {
host: TransitiveFeatureHost::Path("nonexistent/path".to_string()),
}),
..Default::default()
};
let workspace = std::env::current_dir().unwrap();
let result = config.validate(&workspace);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, crate::error::ConfigError::InvalidValue { .. }));
}
#[test]
fn test_transitive_host_validate_path_traversal() {
let config = UnifyConfig {
transitive_pinning: Some(TransitivePinning {
host: TransitiveFeatureHost::Path("../somewhere".to_string()),
}),
..Default::default()
};
let workspace = std::env::current_dir().unwrap();
let result = config.validate(&workspace);
assert!(result.is_err());
let err = result.unwrap_err();
if let crate::error::ConfigError::InvalidValue { message, .. } = err {
assert!(message.contains(".."));
} else {
panic!("Expected InvalidValue error");
}
}
#[test]
fn test_preserve_features_default() {
let config = UnifyConfig::default();
assert!(config.preserve_features.is_empty());
}
#[test]
fn test_preserve_features_parsing() {
let toml = r#"
preserve_features = ["future-api", "unstable-*", "bench*"]
"#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert_eq!(config.preserve_features.len(), 3);
assert!(config.preserve_features.contains(&"future-api".to_string()));
assert!(config.preserve_features.contains(&"unstable-*".to_string()));
assert!(config.preserve_features.contains(&"bench*".to_string()));
}
#[test]
fn test_should_preserve_feature_exact_match() {
let config = UnifyConfig {
preserve_features: vec!["future-api".to_string(), "experimental".to_string()],
..Default::default()
};
assert!(config.should_preserve_feature("future-api"));
assert!(config.should_preserve_feature("experimental"));
assert!(!config.should_preserve_feature("other-feature"));
}
#[test]
fn test_should_preserve_feature_glob_wildcard() {
let config = UnifyConfig {
preserve_features: vec!["unstable-*".to_string()],
..Default::default()
};
assert!(config.should_preserve_feature("unstable-api"));
assert!(config.should_preserve_feature("unstable-feature"));
assert!(config.should_preserve_feature("unstable-"));
assert!(!config.should_preserve_feature("unstable")); assert!(!config.should_preserve_feature("stable-api"));
}
#[test]
fn test_should_preserve_feature_glob_suffix() {
let config = UnifyConfig {
preserve_features: vec!["bench*".to_string()],
..Default::default()
};
assert!(config.should_preserve_feature("bench"));
assert!(config.should_preserve_feature("benchmark"));
assert!(config.should_preserve_feature("benchmarks"));
assert!(!config.should_preserve_feature("prebench"));
}
#[test]
fn test_should_preserve_feature_glob_question_mark() {
let config = UnifyConfig {
preserve_features: vec!["test-?".to_string()],
..Default::default()
};
assert!(config.should_preserve_feature("test-a"));
assert!(config.should_preserve_feature("test-1"));
assert!(!config.should_preserve_feature("test-ab")); assert!(!config.should_preserve_feature("test-")); }
#[test]
fn test_should_preserve_feature_multiple_patterns() {
let config = UnifyConfig {
preserve_features: vec!["future-api".to_string(), "unstable-*".to_string(), "bench*".to_string()],
..Default::default()
};
assert!(config.should_preserve_feature("future-api"));
assert!(config.should_preserve_feature("unstable-feature"));
assert!(config.should_preserve_feature("benchmark"));
assert!(!config.should_preserve_feature("stable-api"));
assert!(!config.should_preserve_feature("other"));
}
#[test]
fn test_should_preserve_feature_empty_list() {
let config = UnifyConfig::default();
assert!(!config.should_preserve_feature("any-feature"));
}
#[test]
fn test_msrv_source_default() {
let config = UnifyConfig::default();
assert_eq!(config.msrv_policy.source(), Some(MsrvSource::Max));
}
#[test]
fn test_msrv_source_parsing_deps() {
let toml = r#"msrv_source = "deps""#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert_eq!(config.msrv_policy.source(), Some(MsrvSource::Deps));
}
#[test]
fn test_msrv_source_parsing_workspace() {
let toml = r#"msrv_source = "workspace""#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert_eq!(config.msrv_policy.source(), Some(MsrvSource::Workspace));
}
#[test]
fn test_msrv_source_parsing_max() {
let toml = r#"msrv_source = "max""#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert_eq!(config.msrv_policy.source(), Some(MsrvSource::Max));
}
#[test]
fn test_msrv_source_with_msrv_enabled() {
let toml = r#"
msrv = true
msrv_source = "workspace"
"#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert_eq!(config.msrv_policy.source(), Some(MsrvSource::Workspace));
}
#[test]
fn test_msrv_source_with_msrv_disabled() {
let toml = r#"
msrv = false
msrv_source = "deps"
"#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert_eq!(config.msrv_policy, MsrvPolicy::Disabled);
}
#[test]
fn test_skip_undeclared_patterns_default() {
let config = UnifyConfig::default();
assert!(!config.skip_undeclared_patterns.is_empty());
assert!(config.skip_undeclared_patterns.contains(&"default".to_string()));
assert!(config.skip_undeclared_patterns.contains(&"std".to_string()));
assert!(config.skip_undeclared_patterns.contains(&"alloc".to_string()));
assert!(config.skip_undeclared_patterns.contains(&"*_backend".to_string()));
assert!(config.skip_undeclared_patterns.contains(&"*_impl".to_string()));
}
#[test]
fn test_skip_undeclared_patterns_parsing() {
let toml = r#"
skip_undeclared_patterns = ["default", "std", "custom-*"]
"#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert_eq!(config.skip_undeclared_patterns.len(), 3);
assert!(config.skip_undeclared_patterns.contains(&"default".to_string()));
assert!(config.skip_undeclared_patterns.contains(&"std".to_string()));
assert!(config.skip_undeclared_patterns.contains(&"custom-*".to_string()));
}
#[test]
fn test_skip_undeclared_patterns_empty() {
let toml = r#"
skip_undeclared_patterns = []
"#;
let config: UnifyConfig = toml_edit::de::from_str(toml).unwrap();
assert!(config.skip_undeclared_patterns.is_empty());
}
#[test]
fn test_should_skip_undeclared_feature_exact_match() {
let config = UnifyConfig {
skip_undeclared_patterns: vec!["default".to_string(), "std".to_string()],
..Default::default()
};
assert!(config.should_skip_undeclared_feature("default"));
assert!(config.should_skip_undeclared_feature("std"));
assert!(!config.should_skip_undeclared_feature("derive"));
}
#[test]
fn test_should_skip_undeclared_feature_glob_suffix() {
let config = UnifyConfig {
skip_undeclared_patterns: vec!["*_backend".to_string()],
..Default::default()
};
assert!(config.should_skip_undeclared_feature("sqlite_backend"));
assert!(config.should_skip_undeclared_feature("postgres_backend"));
assert!(config.should_skip_undeclared_feature("_backend")); assert!(!config.should_skip_undeclared_feature("backend"));
assert!(!config.should_skip_undeclared_feature("backend_"));
}
#[test]
fn test_should_skip_undeclared_feature_glob_prefix() {
let config = UnifyConfig {
skip_undeclared_patterns: vec!["unstable-*".to_string()],
..Default::default()
};
assert!(config.should_skip_undeclared_feature("unstable-api"));
assert!(config.should_skip_undeclared_feature("unstable-internal"));
assert!(config.should_skip_undeclared_feature("unstable-")); assert!(!config.should_skip_undeclared_feature("unstable"));
}
#[test]
fn test_should_skip_undeclared_feature_glob_question_mark() {
let config = UnifyConfig {
skip_undeclared_patterns: vec!["test-?".to_string()],
..Default::default()
};
assert!(config.should_skip_undeclared_feature("test-1"));
assert!(config.should_skip_undeclared_feature("test-a"));
assert!(!config.should_skip_undeclared_feature("test-12"));
assert!(!config.should_skip_undeclared_feature("test-"));
}
#[test]
fn test_should_skip_undeclared_feature_multiple_patterns() {
let config = UnifyConfig {
skip_undeclared_patterns: vec![
"default".to_string(),
"std".to_string(),
"*_backend".to_string(),
"*_impl".to_string(),
],
..Default::default()
};
assert!(config.should_skip_undeclared_feature("default"));
assert!(config.should_skip_undeclared_feature("std"));
assert!(config.should_skip_undeclared_feature("sqlite_backend"));
assert!(config.should_skip_undeclared_feature("sync_impl"));
assert!(!config.should_skip_undeclared_feature("derive"));
assert!(!config.should_skip_undeclared_feature("serde"));
}
#[test]
fn test_should_skip_undeclared_feature_empty_patterns() {
let config = UnifyConfig {
skip_undeclared_patterns: vec![],
..Default::default()
};
assert!(!config.should_skip_undeclared_feature("default"));
assert!(!config.should_skip_undeclared_feature("std"));
assert!(!config.should_skip_undeclared_feature("anything"));
}
}