Skip to main content

cargo_feature_combinations/config/
resolve.rs

1use crate::cfg_eval::CfgEvaluator;
2use crate::target::TargetTriple;
3use color_eyre::eyre::{self, WrapErr};
4use std::collections::{BTreeMap, HashSet};
5
6use super::patch::{
7    FeatureSetVecPatch, SetPatchOps, StringSetPatch, TargetListOps, TargetListPatch,
8    combine_set_patches, combine_target_list_patches,
9};
10use super::scope::{Chain, Layer, ScopeView};
11use super::{Config, FeatureMatrixPatch, FlagConfig, ResolvedFlags, WorkspaceConfig};
12
13/// Feature-matrix output read by feature generation.
14#[derive(Debug, Clone, Default)]
15pub struct ResolvedFeatures {
16    /// Features excluded from the powerset.
17    pub exclude_features: HashSet<String>,
18    /// Features included in every generated combination.
19    pub include_features: HashSet<String>,
20    /// Features to consider when generating the powerset.
21    pub only_features: HashSet<String>,
22    /// Feature sets that must be tested independently.
23    pub isolated_feature_sets: Vec<HashSet<String>>,
24    /// Feature-set patterns to exclude.
25    pub exclude_feature_sets: Vec<HashSet<String>>,
26    /// Feature sets to include exactly.
27    pub include_feature_sets: Vec<HashSet<String>>,
28    /// Explicitly allowed feature sets.
29    pub allow_feature_sets: Vec<HashSet<String>>,
30    /// Whether implicit optional-dependency features are excluded.
31    pub skip_optional_dependencies: bool,
32    /// Whether the empty feature set is omitted.
33    pub no_empty_feature_set: bool,
34    /// Arbitrary user-defined matrix metadata.
35    pub matrix: serde_json::Map<String, serde_json::Value>,
36    /// Maximum generated feature combinations before failing.
37    pub max_combinations: Option<u128>,
38}
39
40impl ResolvedFeatures {
41    /// Convert a raw package base config into a resolved feature view.
42    ///
43    /// This is used by tests and callers that intentionally want package-base
44    /// feature generation without target or command layers.
45    #[must_use]
46    pub fn from_config(config: &Config) -> Self {
47        let mut out = Self::default();
48        apply_single_feature_patch(&mut out, &config.base.settings.features);
49        out
50    }
51}
52
53/// Everything one package-target command resolution needs.
54#[derive(Debug, Clone)]
55pub(crate) struct Resolved {
56    pub(crate) flags: ResolvedFlags,
57    pub(crate) ignored_diagnostics_config: bool,
58    pub(crate) driver: Option<String>,
59    pub(crate) targets_enabled: bool,
60    pub(crate) targets_explicit: bool,
61    pub(crate) features: ResolvedFeatures,
62}
63
64#[derive(Debug, Clone, Copy)]
65pub(crate) struct CliOverlay<'a> {
66    pub(crate) flags: FlagConfig,
67    pub(crate) driver: Option<&'a str>,
68}
69
70#[derive(Debug, Clone, Copy)]
71pub(crate) struct ResolvePolicy {
72    pub(crate) default_diagnostics_allowed: bool,
73    pub(crate) default_targets_enabled: bool,
74}
75
76#[derive(Debug, Clone)]
77pub(crate) struct TargetListResolution {
78    pub(crate) targets: Vec<String>,
79    pub(crate) patched: bool,
80    pub(crate) package_touched: bool,
81}
82
83impl Chain<'_> {
84    pub(crate) fn resolve(
85        &self,
86        cli: CliOverlay<'_>,
87        policy: ResolvePolicy,
88    ) -> eyre::Result<Resolved> {
89        let start = replace_start(&self.layers);
90        let layers = self.layers.get(start..).unwrap_or(&[]);
91        let flag_result = resolve_flags(layers, cli.flags, policy.default_diagnostics_allowed)?;
92        let driver = resolve_driver(layers, cli.driver)?;
93        let (targets_enabled, targets_explicit) =
94            resolve_expand_targets(layers, policy.default_targets_enabled)?;
95        let features = resolve_features(layers)?;
96
97        Ok(Resolved {
98            flags: flag_result.flags,
99            ignored_diagnostics_config: flag_result.ignored_diagnostics_config,
100            driver,
101            targets_enabled,
102            targets_explicit,
103            features,
104        })
105    }
106
107    pub(crate) fn exclude_packages(
108        &self,
109        base_exclude: &HashSet<String>,
110    ) -> eyre::Result<HashSet<String>> {
111        let start = replace_start(&self.layers);
112        let mut out = if start == 0 {
113            base_exclude.clone()
114        } else {
115            HashSet::new()
116        };
117        for layer in self.layers.get(start..).unwrap_or(&[]) {
118            if let Some(ops) =
119                combine_string_patches("exclude_packages", layer, |view| view.exclude_packages)?
120            {
121                out = ops.apply_to(&out);
122            }
123        }
124        Ok(out)
125    }
126
127    pub(crate) fn targets_list(
128        &self,
129        workspace_base: &[String],
130    ) -> eyre::Result<TargetListResolution> {
131        let start = replace_start(&self.layers);
132        let mut targets = if start == 0 {
133            workspace_base.to_vec()
134        } else {
135            Vec::new()
136        };
137        let mut patched = start != 0;
138        let mut package_touched = false;
139        for layer in self.layers.get(start..).unwrap_or(&[]) {
140            if let Some(ops) = combine_target_patches("targets", layer, |view| view.targets)? {
141                targets = ops.apply_to(&targets);
142                patched = true;
143                package_touched = package_touched || layer.scope.is_package();
144            }
145        }
146        Ok(TargetListResolution {
147            targets,
148            patched,
149            package_touched,
150        })
151    }
152}
153
154struct ResolvedFlagResult {
155    flags: ResolvedFlags,
156    ignored_diagnostics_config: bool,
157}
158
159fn replace_start(layers: &[Layer<'_>]) -> usize {
160    layers
161        .iter()
162        .rposition(|layer| layer.entries.iter().any(|(_, view)| view.replace))
163        .unwrap_or(0)
164}
165
166fn resolve_flags(
167    layers: &[Layer<'_>],
168    cli_flags: FlagConfig,
169    default_diagnostics_allowed: bool,
170) -> eyre::Result<ResolvedFlagResult> {
171    let mut merged = FlagConfig::default();
172    let mut ignored_diagnostics_config = false;
173    for layer in layers {
174        let mut flags = combine_flags(layer)?;
175        flags.validate()?;
176        if layer.scope.is_command() {
177            if flags.mentions_diagnostics() {
178                ignored_diagnostics_config = false;
179            }
180        } else if !default_diagnostics_allowed {
181            flags = super::flags::gated_plain_diagnostics(flags, &mut ignored_diagnostics_config);
182        }
183        merged.overlay(flags);
184    }
185    merged.overlay(cli_flags);
186    let flags = ResolvedFlags::try_from_config(merged)?;
187    Ok(ResolvedFlagResult {
188        ignored_diagnostics_config: ignored_diagnostics_config && !flags.diagnostics_only,
189        flags,
190    })
191}
192
193fn combine_flags(layer: &Layer<'_>) -> eyre::Result<FlagConfig> {
194    let prefix = layer
195        .scope
196        .is_command()
197        .then(|| format!("subcommands.{}", layer.command.unwrap_or_default()));
198    super::combine_flag_configs(
199        prefix.as_deref(),
200        layer.scope.source_kind(),
201        layer.entries.iter().map(|(expr, view)| (*expr, view.flags)),
202    )
203}
204
205fn resolve_driver(layers: &[Layer<'_>], cli_driver: Option<&str>) -> eyre::Result<Option<String>> {
206    let mut out = None;
207    for layer in layers {
208        if let Some(driver) = super::combine_driver(
209            "driver",
210            layer.scope.source_kind(),
211            &layer.entries,
212            |view| view.driver,
213        )? {
214            out = Some(driver);
215        }
216    }
217    if let Some(driver) = cli_driver {
218        out = Some(driver.to_string());
219    }
220    Ok(out)
221}
222
223fn resolve_expand_targets(
224    layers: &[Layer<'_>],
225    default_enabled: bool,
226) -> eyre::Result<(bool, bool)> {
227    let mut value = None;
228    for layer in layers.iter().filter(|layer| layer.scope.is_command()) {
229        let name = format!(
230            "subcommands.{}.expand_targets",
231            layer.command.unwrap_or_default()
232        );
233        if let Some(enabled) =
234            super::combine_bool(&name, layer.scope.source_kind(), &layer.entries, |view| {
235                view.expand_targets
236            })?
237        {
238            value = Some(enabled);
239        }
240    }
241    Ok((value.unwrap_or(default_enabled), value.is_some()))
242}
243
244fn resolve_features(layers: &[Layer<'_>]) -> eyre::Result<ResolvedFeatures> {
245    let mut out = ResolvedFeatures::default();
246    for layer in layers.iter().filter(|layer| layer.scope.is_package()) {
247        apply_feature_layer(&mut out, layer)?;
248    }
249    Ok(out)
250}
251
252fn apply_feature_layer(out: &mut ResolvedFeatures, layer: &Layer<'_>) -> eyre::Result<()> {
253    let patches: Vec<(&str, &FeatureMatrixPatch)> = layer
254        .entries
255        .iter()
256        .filter_map(|(expr, view)| view.features.map(|features| (*expr, features)))
257        .collect();
258    if patches.is_empty() {
259        return Ok(());
260    }
261
262    apply_feature_patches(out, layer.scope.source_kind(), patches)
263}
264
265fn apply_single_feature_patch(out: &mut ResolvedFeatures, features: &FeatureMatrixPatch) {
266    if let Some(value) = features.skip_optional_dependencies {
267        out.skip_optional_dependencies = value;
268    }
269    if let Some(value) = features.no_empty_feature_set {
270        out.no_empty_feature_set = value;
271    }
272    if let Some(value) = features.max_combinations {
273        out.max_combinations = Some(value);
274    }
275
276    if let Some(patch) = &features.exclude_features {
277        out.exclude_features = SetPatchOps::from_single(patch).apply_to(&out.exclude_features);
278    }
279    if let Some(patch) = &features.include_features {
280        out.include_features = SetPatchOps::from_single(patch).apply_to(&out.include_features);
281    }
282    if let Some(patch) = &features.only_features {
283        out.only_features = SetPatchOps::from_single(patch).apply_to(&out.only_features);
284    }
285
286    if let Some(patch) = &features.isolated_feature_sets {
287        out.isolated_feature_sets =
288            SetPatchOps::from_single(patch).apply_to_feature_sets(&out.isolated_feature_sets);
289    }
290    if let Some(patch) = &features.exclude_feature_sets {
291        out.exclude_feature_sets =
292            SetPatchOps::from_single(patch).apply_to_feature_sets(&out.exclude_feature_sets);
293    }
294    if let Some(patch) = &features.include_feature_sets {
295        out.include_feature_sets =
296            SetPatchOps::from_single(patch).apply_to_feature_sets(&out.include_feature_sets);
297    }
298    if let Some(patch) = &features.allow_feature_sets {
299        out.allow_feature_sets =
300            SetPatchOps::from_single(patch).apply_to_feature_sets(&out.allow_feature_sets);
301    }
302
303    if let Some(matrix) = &features.matrix {
304        merge_matrix(&mut out.matrix, matrix);
305    }
306}
307
308fn apply_feature_patches<'a>(
309    out: &mut ResolvedFeatures,
310    source_kind: &str,
311    patches: impl IntoIterator<Item = (&'a str, &'a FeatureMatrixPatch)>,
312) -> eyre::Result<()> {
313    let patches = patches.into_iter().collect::<Vec<_>>();
314
315    if let Some(value) = super::combine_bool(
316        "skip_optional_dependencies",
317        source_kind,
318        &patches,
319        |features| features.skip_optional_dependencies,
320    )? {
321        out.skip_optional_dependencies = value;
322    }
323    if let Some(value) =
324        super::combine_bool("no_empty_feature_set", source_kind, &patches, |features| {
325            features.no_empty_feature_set
326        })?
327    {
328        out.no_empty_feature_set = value;
329    }
330    if let Some(value) =
331        super::combine_u128("max_combinations", source_kind, &patches, |features| {
332            features.max_combinations
333        })?
334    {
335        out.max_combinations = Some(value);
336    }
337
338    macro_rules! resolve_string_set {
339        ($field:ident) => {
340            if let Some(ops) = combine_set_patches(
341                stringify!($field),
342                source_kind,
343                patches.iter().filter_map(|(expr, features)| {
344                    features.$field.as_ref().map(|patch| (*expr, patch))
345                }),
346            )? {
347                out.$field = ops.apply_to(&out.$field);
348            }
349        };
350    }
351    resolve_string_set!(exclude_features);
352    resolve_string_set!(include_features);
353    resolve_string_set!(only_features);
354
355    macro_rules! resolve_feature_sets {
356        ($field:ident) => {
357            if let Some(ops) = combine_set_patches(
358                stringify!($field),
359                source_kind,
360                patches.iter().filter_map(|(expr, features)| {
361                    features.$field.as_ref().map(|patch| (*expr, patch))
362                }),
363            )? {
364                out.$field = ops.apply_to_feature_sets(&out.$field);
365            }
366        };
367    }
368    resolve_feature_sets!(isolated_feature_sets);
369    resolve_feature_sets!(exclude_feature_sets);
370    resolve_feature_sets!(include_feature_sets);
371
372    let allow_patches: Vec<(&str, &FeatureSetVecPatch)> = patches
373        .iter()
374        .filter_map(|(expr, features)| {
375            features
376                .allow_feature_sets
377                .as_ref()
378                .map(|patch| (*expr, patch))
379        })
380        .collect();
381    if allow_patches.len() > 1 {
382        let exprs = allow_patches
383            .iter()
384            .map(|(expr, _patch)| *expr)
385            .collect::<Vec<_>>();
386        eyre::bail!(
387            "multiple matching {} entries set allow_feature_sets: {}",
388            source_kind,
389            exprs.join(", ")
390        );
391    }
392    if let Some(ops) = combine_set_patches("allow_feature_sets", source_kind, allow_patches)? {
393        out.allow_feature_sets = ops.apply_to_feature_sets(&out.allow_feature_sets);
394    }
395
396    for (_expr, features) in patches {
397        if let Some(matrix) = features.matrix.as_ref() {
398            merge_matrix(&mut out.matrix, matrix);
399        }
400    }
401    Ok(())
402}
403
404fn combine_string_patches(
405    name: &str,
406    layer: &Layer<'_>,
407    get: impl Fn(ScopeView<'_>) -> Option<&StringSetPatch>,
408) -> eyre::Result<Option<SetPatchOps<String>>> {
409    combine_set_patches(
410        name,
411        layer.scope.source_kind(),
412        layer
413            .entries
414            .iter()
415            .filter_map(|(expr, view)| get(*view).map(|patch| (*expr, patch))),
416    )
417}
418
419fn combine_target_patches(
420    name: &str,
421    layer: &Layer<'_>,
422    get: impl Fn(ScopeView<'_>) -> Option<&TargetListPatch>,
423) -> eyre::Result<Option<TargetListOps>> {
424    combine_target_list_patches(
425        name,
426        layer.scope.source_kind(),
427        layer
428            .entries
429            .iter()
430            .filter_map(|(expr, view)| get(*view).map(|patch| (*expr, patch))),
431    )
432}
433
434fn merge_matrix(
435    base: &mut serde_json::Map<String, serde_json::Value>,
436    patch: &serde_json::Map<String, serde_json::Value>,
437) {
438    for (key, value) in patch {
439        match (base.get_mut(key), value) {
440            (Some(serde_json::Value::Object(base_obj)), serde_json::Value::Object(patch_obj)) => {
441                merge_matrix(base_obj, patch_obj);
442            }
443            _ => {
444                base.insert(key.clone(), value.clone());
445            }
446        }
447    }
448}
449
450/// Resolve target-specific feature config with no workspace and no command.
451///
452/// # Errors
453///
454/// Returns an error if cfg evaluation fails or if matching overrides conflict.
455pub fn resolve_config<E: CfgEvaluator>(
456    base: &Config,
457    target: &TargetTriple,
458    evaluator: &mut E,
459) -> eyre::Result<ResolvedFeatures> {
460    let ws = WorkspaceConfig::default();
461    let matched = matching_overrides(&base.targets, target, evaluator)?;
462    Ok(Chain::full(&ws, &[], base, matched, None, None)
463        .resolve(
464            CliOverlay {
465                flags: FlagConfig::default(),
466                driver: None,
467            },
468            ResolvePolicy {
469                default_diagnostics_allowed: true,
470                default_targets_enabled: true,
471            },
472        )?
473        .features)
474}
475
476/// Return the `target.'cfg(...)'` overrides whose cfg expression matches
477/// `target`, preserving map order.
478pub(crate) fn matching_overrides<'a, V, E: CfgEvaluator>(
479    overrides: &'a BTreeMap<String, V>,
480    target: &TargetTriple,
481    evaluator: &mut E,
482) -> eyre::Result<Vec<(&'a str, &'a V)>> {
483    let mut matched = Vec::new();
484    for (expr, ov) in overrides {
485        let is_match = evaluator
486            .matches(expr, target)
487            .wrap_err_with(|| format!("failed to evaluate cfg expression `{expr}`"))?;
488        if is_match {
489            matched.push((expr.as_str(), ov));
490        }
491    }
492    Ok(matched)
493}
494
495#[cfg(test)]
496mod tests {
497    use super::{Chain, CliOverlay, ResolvePolicy, resolve_config};
498    use crate::cfg_eval::CfgEvaluator;
499    use crate::config::{
500        Config, FlagConfig, ScopeConfig, WorkspaceConfig, WorkspaceTargetOverride,
501    };
502    use crate::target::TargetTriple;
503    use color_eyre::eyre;
504    use std::collections::{BTreeMap, HashSet};
505
506    struct MatchAll;
507
508    impl CfgEvaluator for MatchAll {
509        fn matches(&mut self, _cfg_expr: &str, _target: &TargetTriple) -> eyre::Result<bool> {
510            Ok(true)
511        }
512    }
513
514    fn resolve_base(
515        ws: &WorkspaceConfig,
516        pkg: Option<&Config>,
517        raw: Option<&str>,
518        resolved: Option<&str>,
519        cli_flags: FlagConfig,
520        cli_driver: Option<&str>,
521        default_diagnostics_allowed: bool,
522    ) -> eyre::Result<super::Resolved> {
523        Chain::base(ws, pkg, raw, resolved).resolve(
524            CliOverlay {
525                flags: cli_flags,
526                driver: cli_driver,
527            },
528            ResolvePolicy {
529                default_diagnostics_allowed,
530                default_targets_enabled: true,
531            },
532        )
533    }
534
535    #[test]
536    fn broad_diagnostics_config_is_gated_for_unsafe_command() -> eyre::Result<()> {
537        let mut ws = WorkspaceConfig::default();
538        ws.base.settings.flags.diagnostics_only = Some(true);
539
540        let resolved = resolve_base(&ws, None, None, None, FlagConfig::default(), None, false)?;
541
542        assert!(!resolved.flags.diagnostics_only);
543        assert!(resolved.ignored_diagnostics_config);
544        Ok(())
545    }
546
547    #[test]
548    fn broad_diagnostics_true_with_dedupe_false_still_warns() -> eyre::Result<()> {
549        let mut ws = WorkspaceConfig::default();
550        ws.base.settings.flags.diagnostics_only = Some(true);
551        ws.base.settings.flags.dedupe = Some(false);
552
553        let resolved = resolve_base(&ws, None, None, None, FlagConfig::default(), None, false)?;
554
555        assert!(!resolved.flags.diagnostics_only);
556        assert!(!resolved.flags.dedupe);
557        assert!(resolved.ignored_diagnostics_config);
558        Ok(())
559    }
560
561    #[test]
562    fn cli_diagnostics_rescues_gated_broad_config_warning() -> eyre::Result<()> {
563        let mut ws = WorkspaceConfig::default();
564        ws.base.settings.flags.diagnostics_only = Some(true);
565
566        let resolved = resolve_base(
567            &ws,
568            None,
569            None,
570            None,
571            FlagConfig {
572                diagnostics_only: Some(true),
573                ..FlagConfig::default()
574            },
575            None,
576            false,
577        )?;
578
579        assert!(resolved.flags.diagnostics_only);
580        assert!(!resolved.ignored_diagnostics_config);
581        Ok(())
582    }
583
584    #[test]
585    fn command_dedupe_bypasses_diagnostics_gate() -> eyre::Result<()> {
586        let mut ws = WorkspaceConfig::default();
587        ws.base.subcommands.insert(
588            "test".to_string(),
589            ScopeConfig {
590                flags: FlagConfig {
591                    dedupe: Some(true),
592                    ..FlagConfig::default()
593                },
594                ..ScopeConfig::default()
595            },
596        );
597
598        let resolved = resolve_base(
599            &ws,
600            None,
601            Some("test"),
602            Some("test"),
603            FlagConfig::default(),
604            None,
605            false,
606        )?;
607
608        assert!(resolved.flags.diagnostics_only);
609        assert!(resolved.flags.dedupe);
610        assert!(!resolved.ignored_diagnostics_config);
611        Ok(())
612    }
613
614    #[test]
615    fn package_replace_discards_broader_flags_and_driver() -> eyre::Result<()> {
616        let mut ws = WorkspaceConfig::default();
617        ws.base.settings.flags.pedantic = Some(true);
618        ws.base.settings.driver = Some("cargo-zigbuild".to_string());
619        let mut pkg = Config::default();
620        pkg.base.settings.replace = true;
621        pkg.base.settings.flags.verbose = Some(true);
622        pkg.base.settings.driver = Some("cross".to_string());
623
624        let resolved = resolve_base(
625            &ws,
626            Some(&pkg),
627            None,
628            None,
629            FlagConfig::default(),
630            None,
631            true,
632        )?;
633
634        assert!(!resolved.flags.pedantic);
635        assert!(resolved.flags.verbose);
636        assert_eq!(resolved.driver.as_deref(), Some("cross"));
637        Ok(())
638    }
639
640    #[test]
641    fn cli_driver_overrides_resolved_driver() -> eyre::Result<()> {
642        let mut ws = WorkspaceConfig::default();
643        ws.base.settings.driver = Some("cargo-zigbuild".to_string());
644        let mut pkg = Config::default();
645        pkg.base.settings.driver = Some("cross".to_string());
646
647        let resolved = resolve_base(
648            &ws,
649            Some(&pkg),
650            None,
651            None,
652            FlagConfig::default(),
653            Some("cargo"),
654            true,
655        )?;
656
657        assert_eq!(resolved.driver.as_deref(), Some("cargo"));
658        Ok(())
659    }
660
661    #[test]
662    fn target_command_errors_use_selected_command_name_for_short_alias() {
663        let ws = WorkspaceConfig {
664            targets: BTreeMap::from([
665                (
666                    "cfg(a)".to_string(),
667                    WorkspaceTargetOverride {
668                        subcommands: BTreeMap::from([(
669                            "test".to_string(),
670                            ScopeConfig {
671                                flags: FlagConfig {
672                                    pedantic: Some(true),
673                                    ..FlagConfig::default()
674                                },
675                                ..ScopeConfig::default()
676                            },
677                        )]),
678                        ..WorkspaceTargetOverride::default()
679                    },
680                ),
681                (
682                    "cfg(b)".to_string(),
683                    WorkspaceTargetOverride {
684                        subcommands: BTreeMap::from([(
685                            "test".to_string(),
686                            ScopeConfig {
687                                flags: FlagConfig {
688                                    pedantic: Some(false),
689                                    ..FlagConfig::default()
690                                },
691                                ..ScopeConfig::default()
692                            },
693                        )]),
694                        ..WorkspaceTargetOverride::default()
695                    },
696                ),
697            ]),
698            ..WorkspaceConfig::default()
699        };
700        let matched = ws
701            .targets
702            .iter()
703            .map(|(expr, section)| (expr.clone(), section))
704            .collect::<Vec<_>>();
705
706        let err = Chain::workspace(&ws, &matched, Some("t"), Some("test"))
707            .resolve(
708                CliOverlay {
709                    flags: FlagConfig::default(),
710                    driver: None,
711                },
712                ResolvePolicy {
713                    default_diagnostics_allowed: true,
714                    default_targets_enabled: true,
715                },
716            )
717            .expect_err("conflicting target command flags should fail");
718        let message = err.to_string();
719
720        assert!(message.contains("subcommands.test.pedantic"), "{message}");
721        assert!(!message.contains("subcommands.t.pedantic"), "{message}");
722    }
723
724    #[test]
725    fn non_replacing_sibling_add_applies_onto_reset_base() -> eyre::Result<()> {
726        let raw = serde_json::json!({
727            "exclude_features": ["base"],
728            "target": {
729                "cfg(a)": { "replace": true },
730                "cfg(b)": { "exclude_features": { "add": ["sibling"] } },
731            },
732        });
733        crate::config::validate_package_metadata(&raw, "package.metadata.cargo-fc")?;
734        let config = serde_json::from_value(raw)?;
735
736        let resolved = resolve_config(
737            &config,
738            &TargetTriple("x86_64-unknown-linux-gnu".to_string()),
739            &mut MatchAll,
740        )?;
741
742        assert!(resolved.exclude_features.contains("sibling"));
743        assert!(!resolved.exclude_features.contains("base"));
744        Ok(())
745    }
746
747    #[test]
748    fn multiple_matching_replacing_sections_combine_agreeing_overrides() -> eyre::Result<()> {
749        let raw = serde_json::json!({
750            "exclude_features": ["base"],
751            "target": {
752                "cfg(a)": {
753                    "replace": true,
754                    "exclude_features": ["fresh"],
755                },
756                "cfg(b)": {
757                    "replace": true,
758                    "exclude_features": ["fresh"],
759                },
760            },
761        });
762        crate::config::validate_package_metadata(&raw, "package.metadata.cargo-fc")?;
763        let config = serde_json::from_value(raw)?;
764
765        let resolved = resolve_config(
766            &config,
767            &TargetTriple("x86_64-unknown-linux-gnu".to_string()),
768            &mut MatchAll,
769        )?;
770
771        assert_eq!(
772            resolved.exclude_features,
773            HashSet::from(["fresh".to_string()])
774        );
775        Ok(())
776    }
777}