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#[derive(Debug, Clone, Default)]
15pub struct ResolvedFeatures {
16 pub exclude_features: HashSet<String>,
18 pub include_features: HashSet<String>,
20 pub only_features: HashSet<String>,
22 pub isolated_feature_sets: Vec<HashSet<String>>,
24 pub exclude_feature_sets: Vec<HashSet<String>>,
26 pub include_feature_sets: Vec<HashSet<String>>,
28 pub allow_feature_sets: Vec<HashSet<String>>,
30 pub skip_optional_dependencies: bool,
32 pub no_empty_feature_set: bool,
34 pub matrix: serde_json::Map<String, serde_json::Value>,
36 pub max_combinations: Option<u128>,
38}
39
40impl ResolvedFeatures {
41 #[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#[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
450pub 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
476pub(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}