| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use anyhow::{anyhow, Context, Result}; |
| use serde::Serialize; |
| use std::collections::{btree_map::Entry, BTreeSet}; |
| |
| use assembly_config_schema::{BoardInformation, BuildType, FileEntry, ICUConfig}; |
| use assembly_util::NamedMap; |
| |
| /// The platform's base service level. |
| /// |
| /// This is the basis for the contract with the product as to what the minimal |
| /// set of services that are available in the platform will be. Features can |
| /// be enabled on top of this most-basic level, but some features will require |
| /// a higher basic level of support. |
| /// |
| /// These were initially based on the product definitions that are used to |
| /// provide the basis for all other products: |
| /// |
| /// bringup.gni (Bootstrap) |
| /// +--> minimal.gni (Minimal) |
| /// +--> core.gni |
| /// +--> (everything else) |
| /// |
| /// The `Utility` level is between `Bootstrap` and `Minimal`, adding the `/core` |
| /// realm and those children of `/core` needed by all systems that include |
| /// `/core`. |
| /// |
| /// The standard, default, level is `Minimal`, and is the level that should be |
| /// used by products' main system. |
| /// |
| /// Note: This version of the enum does not contain the |
| /// `assembly_config_schema::FeatureSetLevel::Empty` option, as that is instead |
| /// represented as `Option::None`, with the other values as an |
| /// `Option::Some(value)`. |
| #[derive(Debug, PartialEq)] |
| pub(crate) enum FeatureSupportLevel { |
| /// Bootable, but serial-only. This is only the `/bootstrap` realm. No |
| /// netstack, no storage drivers, etc. this is the smallest bootable system |
| /// created by assembly, and is primarily used for board-level bringup. |
| /// |
| /// https://fuchsia.dev/fuchsia-src/development/build/build_system/bringup |
| Bootstrap, |
| |
| /// This is the smallest configuration that includes the `/core` realm, and |
| /// is best suited for utility-type systems such as recovery. The "main" |
| /// system for a product should not use this, and instead use the default. |
| Utility, |
| |
| /// This is the smallest "full Fuchsia" configuration. This has a netstack, |
| /// can update itself, and has all the subsystems that are required to |
| /// ship a production-level product. |
| /// |
| /// This is the default level unless otherwise specified. |
| Minimal, |
| } |
| impl FeatureSupportLevel { |
| /// Convert a deserialized assembly_config_schema:: FeatureSetLevel into an |
| /// `Option<FeatureSetLevel>`, where the `Empty` case becomes `None`. |
| pub fn from_deserialized( |
| value: &assembly_config_schema::platform_config::FeatureSupportLevel, |
| ) -> Option<Self> { |
| match value { |
| assembly_config_schema::FeatureSupportLevel::Empty => None, |
| assembly_config_schema::FeatureSupportLevel::Bootstrap => { |
| Some(FeatureSupportLevel::Bootstrap) |
| } |
| assembly_config_schema::FeatureSupportLevel::Utility => { |
| Some(FeatureSupportLevel::Utility) |
| } |
| assembly_config_schema::FeatureSupportLevel::Minimal => { |
| Some(FeatureSupportLevel::Minimal) |
| } |
| } |
| } |
| } |
| |
| /// A trait for subsystems to implement to provide the configuration for their |
| /// subsystem's components. |
| pub(crate) trait DefineSubsystemConfiguration<T> { |
| /// Given the feature_set_level and build_type, along with the configuration |
| /// schema for the subsystem, add its configuration to the builder. |
| fn define_configuration( |
| context: &ConfigurationContext<'_>, |
| subsystem_config: &T, |
| builder: &mut dyn ConfigurationBuilder, |
| ) -> anyhow::Result<()>; |
| } |
| |
| /// This provides the context that's passed to each subsystem's configuration |
| /// module. These are the fields from the |
| /// `assembly_config_schema::platform_config::PlatformConfig` struct that are |
| /// available to all subsystems to use to derive their configuration from. |
| pub(crate) struct ConfigurationContext<'a> { |
| pub feature_set_level: &'a FeatureSupportLevel, |
| pub build_type: &'a BuildType, |
| pub board_info: Option<&'a BoardInformation>, |
| /// The desired ICU configuration, used to configure subsystems that are |
| /// ICU-flavor aware. |
| /// |
| /// If not set, the, use the unflavored version of the component. |
| pub _icu_config: &'a Option<ICUConfig>, |
| } |
| |
| impl Default for ConfigurationContext<'_> { |
| /// Use e.g. in tests that initialize only relevant fields. |
| /// |
| /// For example: |
| /// |
| /// ```ignore |
| /// let context = ConfigurationContext { |
| /// feature_set_level: &FeatureSupportLevel::Minimal, |
| /// build_type: &BuildType::Eng, |
| /// ..Default::default() |
| /// }; |
| /// ``` |
| fn default() -> Self { |
| Self { |
| feature_set_level: &FeatureSupportLevel::Minimal, |
| build_type: &BuildType::User, |
| board_info: None, |
| _icu_config: &None, |
| } |
| } |
| } |
| |
| /// A struct for collecting multiple kinds of platform configuration. |
| /// |
| /// Subsystem configuration structs use this builder to add their configuration |
| /// to the assembly. |
| /// |
| /// Usage: |
| /// ``` |
| /// use crate::subsystems::prelude::*; |
| /// let builder = ConfigurationBuilder::default(); |
| /// builder.platform_bundle("wlan")?; |
| /// |
| /// // to set a single field on a single component in a package: |
| /// builder.package("wlancfg").component("wlancfg").field("my_key", "some_value"); |
| /// |
| /// // to set a single field on multiple components in the same package: |
| /// let mut swd_package = builder.package("swd"); |
| /// swd_package.component("meta/foo.cm").field("some_key", "some_value"); |
| /// swd_package.component("meta/bar.cm").field("some_key", "some_value"); |
| /// |
| /// // to set multiple fieds on the same component: |
| /// swd_package.component("meta/mine.cm") |
| /// .field("a", "value1") |
| /// .field("b", "value2"); |
| /// ``` |
| /// |
| pub(crate) trait ConfigurationBuilder { |
| /// Add a platform assembly input bundle that should be included in the |
| /// assembled platform. |
| fn platform_bundle(&mut self, name: &str); |
| |
| /// Add configuration for items in bootfs. |
| fn bootfs(&mut self) -> &mut dyn BootfsConfigBuilder; |
| |
| /// Add configuration for a named package in one of the package sets. |
| fn package(&mut self, name: &str) -> &mut dyn PackageConfigBuilder; |
| |
| /// Create a new domain config package. |
| fn add_domain_config(&mut self, name: &str) -> &mut dyn DomainConfigBuilder; |
| } |
| |
| /// The interface for specifying the configuration to provide for bootfs. |
| pub(crate) trait BootfsConfigBuilder { |
| /// Add configuration to the builder for a component within a package. |
| fn component(&mut self, pkg_path: &str) -> Result<&mut dyn ComponentConfigBuilder>; |
| } |
| |
| /// The interface for specifying the configuration to provide for a package. |
| pub(crate) trait PackageConfigBuilder { |
| /// Add configuration to the builder for a component within a package. |
| fn component(&mut self, pkg_path: &str) -> Result<&mut dyn ComponentConfigBuilder>; |
| |
| /// Add a config data file to the package in the builder. |
| fn config_data(&mut self, file_entry: FileEntry) -> Result<&mut dyn PackageConfigBuilder>; |
| } |
| |
| /// The interface for building a domain config. |
| pub(crate) trait DomainConfigBuilder { |
| /// Add a directory to the domain config which can hold config resources. |
| fn directory(&mut self, name: &str) -> &mut dyn DomainConfigDirectoryBuilder; |
| } |
| |
| /// The interface for specifying the config files to add to a domain config package directory. |
| pub(crate) trait DomainConfigDirectoryBuilder { |
| fn entry(&mut self, file_entry: FileEntry) -> Result<&mut dyn DomainConfigDirectoryBuilder>; |
| } |
| |
| /// The interface for specifying the configuration to provide for a component. |
| pub(crate) trait ComponentConfigBuilder { |
| /// Add a value for a Structured Configuration field for a given component. |
| fn field_value( |
| &mut self, |
| key: &str, |
| value: serde_json::Value, |
| ) -> Result<&mut dyn ComponentConfigBuilder>; |
| } |
| |
| /// An extension trait that allows the field value to be passed without calling |
| /// `.into()` at the call-site. |
| pub(crate) trait ComponentConfigBuilderExt { |
| /// Add a value for a Structured Configuration field for a given component. |
| fn field( |
| &mut self, |
| key: &str, |
| value: impl Into<serde_json::Value>, |
| ) -> Result<&mut dyn ComponentConfigBuilder>; |
| } |
| |
| impl ComponentConfigBuilderExt for &mut dyn ComponentConfigBuilder { |
| fn field( |
| &mut self, |
| key: &str, |
| value: impl Into<serde_json::Value>, |
| ) -> Result<&mut dyn ComponentConfigBuilder> { |
| ComponentConfigBuilder::field_value(*self, key, value.into()) |
| } |
| } |
| |
| /// The in-progress builder, which hides its state. |
| pub(crate) struct ConfigurationBuilderImpl { |
| /// The Assembly Input Bundles to add. |
| bundles: BTreeSet<String>, |
| |
| /// BootFS configuration. |
| bootfs: BootfsConfig, |
| |
| /// Per-package configuration. |
| package_configs: PackageConfigs, |
| |
| /// The domain config packages to add. |
| domain_configs: DomainConfigs, |
| } |
| |
| impl Default for ConfigurationBuilderImpl { |
| fn default() -> Self { |
| Self { |
| bundles: BTreeSet::default(), |
| bootfs: BootfsConfig::default(), |
| package_configs: PackageConfigs::new("package configs"), |
| domain_configs: DomainConfigs::new("domain configs"), |
| } |
| } |
| } |
| |
| impl ConfigurationBuilderImpl { |
| /// Convert the builder into the completed configuration that can be used |
| /// to create the configured platform itself. |
| pub fn build(self) -> CompletedConfiguration { |
| let Self { bundles, bootfs, package_configs, domain_configs } = self; |
| CompletedConfiguration { bundles, bootfs, package_configs, domain_configs } |
| } |
| } |
| |
| /// The struct containing the resultant configuration to apply to the platform. |
| pub struct CompletedConfiguration { |
| /// The list of the Platform Assembly Input Bundles to add. |
| /// |
| /// This is a list of Platform AIBs by name (not path). |
| /// |
| pub bundles: BTreeSet<String>, |
| |
| /// Configuration for items in bootfs. |
| pub bootfs: BootfsConfig, |
| |
| /// Per-package configuration for named packages in the package sets |
| /// |
| /// Which set doesn't matter, as a package can only be in one package set in |
| /// the assembled image. |
| pub package_configs: PackageConfigs, |
| |
| /// The list of domain configs to add. |
| pub domain_configs: DomainConfigs, |
| } |
| |
| /// A map from package names to the configuration to apply to them. |
| pub type PackageConfigs = NamedMap<PackageConfiguration>; |
| |
| /// A map from component manifest path with a namespace to the values for for the component. |
| pub type ComponentConfigs = NamedMap<ComponentConfiguration>; |
| |
| /// A map from package name to domain config. |
| pub type DomainConfigs = NamedMap<DomainConfig>; |
| |
| /// All of the configuration that applies to a single package. |
| /// |
| /// This holds: |
| /// - config_data entries for the package |
| /// - for each component: |
| /// - Structured Config |
| /// |
| #[derive(Clone, Debug, PartialEq, Serialize)] |
| pub struct PackageConfiguration { |
| /// A map from manifest paths within the package namespace to the values for the component. |
| pub components: ComponentConfigs, |
| |
| /// A map of config data entries, keyed by the destination path. |
| pub config_data: NamedMap<FileEntry>, |
| |
| /// The package name. |
| pub name: String, |
| } |
| |
| impl PackageConfiguration { |
| /// Construt a new PackageConfiguration. |
| pub fn new(name: impl AsRef<str>) -> Self { |
| PackageConfiguration { |
| components: ComponentConfigs::new("component configs"), |
| config_data: NamedMap::new("config data"), |
| name: name.as_ref().into(), |
| } |
| } |
| } |
| |
| /// All of the configuration for a single component. |
| /// |
| /// This holds: |
| /// - Structured Config values for this component. |
| #[derive(Clone, Debug, PartialEq, Serialize)] |
| pub struct ComponentConfiguration { |
| /// Structured Config key-value pairs. |
| pub fields: NamedMap<serde_json::Value>, |
| |
| /// The component's manifest path in its package or in bootfs |
| manifest_path: String, |
| } |
| |
| impl Default for ComponentConfiguration { |
| fn default() -> Self { |
| Self { fields: NamedMap::new("structured config fields"), manifest_path: String::default() } |
| } |
| } |
| |
| #[derive(Clone, Debug, PartialEq, Serialize)] |
| pub struct DomainConfig { |
| pub directories: NamedMap<DomainConfigDirectory>, |
| pub name: String, |
| } |
| |
| #[derive(Clone, Debug, PartialEq, Serialize)] |
| pub struct DomainConfigDirectory { |
| pub entries: NamedMap<FileEntry>, |
| } |
| |
| impl ConfigurationBuilder for ConfigurationBuilderImpl { |
| fn platform_bundle(&mut self, name: &str) { |
| self.bundles.insert(name.to_string()); |
| } |
| |
| fn bootfs(&mut self) -> &mut dyn BootfsConfigBuilder { |
| &mut self.bootfs |
| } |
| |
| fn package(&mut self, name: &str) -> &mut dyn PackageConfigBuilder { |
| self.package_configs.entry(name.to_string()).or_insert_with_key(|name| { |
| PackageConfiguration { |
| components: ComponentConfigs::new("component configs"), |
| config_data: NamedMap::new("config data"), |
| name: name.to_owned(), |
| } |
| }) |
| } |
| |
| fn add_domain_config(&mut self, name: &str) -> &mut dyn DomainConfigBuilder { |
| self.domain_configs.entry(name.to_string()).or_insert_with_key(|name| DomainConfig { |
| directories: NamedMap::new("directories"), |
| name: name.to_owned(), |
| }) |
| } |
| } |
| |
| impl DomainConfigBuilder for DomainConfig { |
| fn directory(&mut self, name: &str) -> &mut dyn DomainConfigDirectoryBuilder { |
| self.directories |
| .entry(name.to_string()) |
| .or_insert_with(|| DomainConfigDirectory { entries: NamedMap::new("domain configs") }) |
| } |
| } |
| |
| impl DomainConfigDirectoryBuilder for DomainConfigDirectory { |
| fn entry(&mut self, file_entry: FileEntry) -> Result<&mut dyn DomainConfigDirectoryBuilder> { |
| self.entries |
| .try_insert_unique(file_entry.destination.clone(), file_entry) |
| .context("A config destination can only be set once for a domain config")?; |
| Ok(self) |
| } |
| } |
| |
| impl PackageConfigBuilder for PackageConfiguration { |
| fn component(&mut self, pkg_path: &str) -> Result<&mut dyn ComponentConfigBuilder> { |
| match self.components.entry(pkg_path.to_owned()) { |
| entry @ Entry::Vacant(_) => { |
| Ok(entry.or_insert_with_key(|path_in_package| ComponentConfiguration { |
| fields: NamedMap::new("structured config fields"), |
| manifest_path: path_in_package.to_owned(), |
| })) |
| } |
| Entry::Occupied(_) => { |
| Err(anyhow!("Each component's configuration can only be set once")) |
| .with_context(|| format!("Setting configuration for component: {pkg_path}")) |
| .with_context(|| anyhow!("Setting configuration for package: {}", self.name)) |
| } |
| } |
| } |
| |
| fn config_data(&mut self, file_entry: FileEntry) -> Result<&mut dyn PackageConfigBuilder> { |
| self.config_data |
| .try_insert_unique(file_entry.destination.clone(), file_entry) |
| .context("A config data destination can only be set once for a package")?; |
| Ok(self) |
| } |
| } |
| |
| impl ComponentConfigBuilder for ComponentConfiguration { |
| /// Add a value for a Structured Configuration field for a given component. |
| fn field_value( |
| &mut self, |
| key: &str, |
| value: serde_json::Value, |
| ) -> Result<&mut dyn ComponentConfigBuilder> { |
| self.fields |
| .try_insert_unique(key.to_owned(), value) |
| .context("Each Structured Config field can only be set once for a component")?; |
| Ok(self) |
| } |
| } |
| |
| /// Configuration of components in bootfs. |
| /// |
| /// This is separate from PackageConfig because it may have to place bare files |
| /// in bootfs. |
| #[derive(Clone, Debug, PartialEq)] |
| pub struct BootfsConfig { |
| /// A map from manifest paths within bootfs to the configuration values for |
| /// the component. |
| pub components: ComponentConfigs, |
| } |
| |
| impl Default for BootfsConfig { |
| fn default() -> Self { |
| Self { components: ComponentConfigs::new("component configs") } |
| } |
| } |
| |
| impl BootfsConfigBuilder for BootfsConfig { |
| /// Add configuration to the builder for a component within bootfs. |
| fn component( |
| &mut self, |
| component_manifest_path: &str, |
| ) -> Result<&mut dyn ComponentConfigBuilder> { |
| match self.components.entry(component_manifest_path.to_owned()) { |
| entry @ Entry::Vacant(_) => { |
| Ok(entry.or_insert_with_key(|component_manifest_path| ComponentConfiguration { |
| fields: NamedMap::new("structured config fields"), |
| manifest_path: component_manifest_path.to_owned(), |
| })) |
| } |
| Entry::Occupied(_) => { |
| Err(anyhow!("Each component's configuration can only be set once")) |
| .with_context(|| { |
| format!("Setting configuration for component: {component_manifest_path}") |
| }) |
| .context("Setting configuration in bootfs") |
| } |
| } |
| } |
| } |
| |
| pub(crate) trait BoardInformationExt { |
| /// Returns whether or not this board provides the named feature. |
| fn provides_feature(&self, name: impl AsRef<str>) -> bool; |
| } |
| |
| impl BoardInformationExt for BoardInformation { |
| /// Returns whether or not this board provides the named feature. |
| fn provides_feature(&self, name: impl AsRef<str>) -> bool { |
| // .contains(&str) doesn't work for Vec<String>, so it's neccessary |
| // to use .iter().any(...) instead. |
| let name = name.as_ref(); |
| self.provided_features.iter().any(|f| f == name) |
| } |
| } |
| |
| impl BoardInformationExt for Option<&BoardInformation> { |
| fn provides_feature(&self, name: impl AsRef<str>) -> bool { |
| match self { |
| Some(board_info) => board_info.provides_feature(name), |
| _ => false, |
| } |
| } |
| } |
| |
| /// DefaultByBuildType trait is implemented on each enum value to instantiate an |
| /// unconfigured update checker. Specifically, an Eng build-type will result in an |
| /// unconfigured system-update-checker and the User or UserDebug build-types will |
| /// result in an unconfigured omaha-client |
| /// |
| /// ``` |
| /// impl DefaultByBuildType for PolicyLabels { |
| /// fn default_by_build_type(build_type: &BuildType) -> Self { |
| /// match build_type { |
| /// BuildType::Eng => PolicyLabels::Unrestricted, |
| /// BuildType::UserDebug => PolicyLabels::LocalDynamicConfig, |
| /// BuildType::User => PolicyLabels::BaseComponentsOnly, |
| /// } |
| /// } |
| /// } |
| /// |
| /// let policy = PolicyLabels::default_by_build_type(&BuildType::Eng); |
| /// assert_eq!(policy, PolicyLabels::Unrestricted); |
| /// ``` |
| pub(crate) trait DefaultByBuildType { |
| fn default_by_build_type(build_type: &BuildType) -> Self; |
| } |
| |
| /// A trait which declares that a type T implements a fn which returns an instance of type T by |
| /// default (with respect to the build_type) or because the T struct has already been instantiated |
| pub(crate) trait OptionDefaultByBuildTypeExt<T: DefaultByBuildType> { |
| fn value_or_default_from_build_type(self, build_type: &BuildType) -> T; |
| } |
| |
| /// Returns an unwrapped instance of T if T is provided, else defers to T::default_by_build_type |
| /// Used in situations where the configuration value's default is dependent on the build_type, |
| /// and may not be provided by the product owner. Therefore, None ends up being converted to |
| /// a default which depends on the provided BuildType. |
| /// |
| /// Usage: |
| /// ``` |
| /// let config = SwdConfig { |
| /// policy: None, |
| /// ... |
| /// }; |
| /// let policy = config.policy.value_or_default_from_build_type(&BuildType::Eng); |
| /// assert_eq!(policy, PolicyLabels::Unrestricted); |
| /// ``` |
| /// |
| /// |
| /// |
| impl<T: DefaultByBuildType> OptionDefaultByBuildTypeExt<T> for Option<T> { |
| fn value_or_default_from_build_type(self, build_type: &BuildType) -> T { |
| self.unwrap_or_else(|| T::default_by_build_type(build_type)) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn test_config_builder() { |
| let mut builder = ConfigurationBuilderImpl::default(); |
| |
| // using an inner |
| fn make_config(builder: &mut dyn ConfigurationBuilder) -> Result<()> { |
| builder.bootfs().component("some/bootfs_component")?.field("key", "value")?; |
| |
| builder |
| .package("package_a") |
| .component("meta/component_a1")? |
| .field("key_a1", "value_a1")?; |
| builder |
| .package("package_a") |
| .component("meta/component_a2")? |
| .field("key_a2", "value_a2")?; |
| builder |
| .package("package_b") |
| .component("meta/component_b")? |
| .field("key_b1", "value_b1")? |
| .field("key_b2", "value_b2")?; |
| |
| builder.package("package_a").config_data(FileEntry { |
| destination: "config/one".into(), |
| source: "config_data1".into(), |
| })?; |
| builder.package("package_a").config_data(FileEntry { |
| destination: "config/two".into(), |
| source: "config_data2".into(), |
| })?; |
| builder.package("package_b").config_data(FileEntry { |
| destination: "config/one".into(), |
| source: "config_data1".into(), |
| })?; |
| |
| Ok(()) |
| } |
| assert!(make_config(&mut builder).is_ok()); |
| let config = builder.build(); |
| |
| assert_eq!(config.bootfs.components.len(), 1); |
| |
| assert_eq!( |
| config.bootfs.components.get("some/bootfs_component").unwrap().fields, |
| NamedMap { |
| name: "structured config fields".into(), |
| entries: [("key".into(), "value".into())].into(), |
| }, |
| ); |
| assert_eq!(config.package_configs.len(), 2); |
| assert_eq!( |
| config.package_configs.get("package_a").unwrap(), |
| &PackageConfiguration { |
| name: "package_a".into(), |
| components: NamedMap { |
| name: "component configs".into(), |
| entries: [ |
| ( |
| "meta/component_a1".into(), |
| ComponentConfiguration { |
| manifest_path: "meta/component_a1".into(), |
| fields: NamedMap { |
| name: "structured config fields".into(), |
| entries: [("key_a1".into(), "value_a1".into())].into() |
| }, |
| } |
| ), |
| ( |
| "meta/component_a2".into(), |
| ComponentConfiguration { |
| manifest_path: "meta/component_a2".into(), |
| fields: NamedMap { |
| name: "structured config fields".into(), |
| entries: [("key_a2".into(), "value_a2".into())].into(), |
| }, |
| } |
| ), |
| ] |
| .into(), |
| }, |
| config_data: NamedMap { |
| name: "config data".into(), |
| entries: [ |
| ( |
| "config/one".into(), |
| FileEntry { |
| destination: "config/one".into(), |
| source: "config_data1".into(), |
| } |
| ), |
| ( |
| "config/two".into(), |
| FileEntry { |
| destination: "config/two".into(), |
| source: "config_data2".into(), |
| } |
| ), |
| ] |
| .into(), |
| }, |
| } |
| ); |
| assert_eq!( |
| config.package_configs.get("package_b").unwrap(), |
| &PackageConfiguration { |
| name: "package_b".into(), |
| components: NamedMap { |
| name: "component configs".into(), |
| entries: [( |
| "meta/component_b".into(), |
| ComponentConfiguration { |
| manifest_path: "meta/component_b".into(), |
| fields: NamedMap { |
| name: "structured config fields".into(), |
| entries: [ |
| ("key_b1".into(), "value_b1".into()), |
| ("key_b2".into(), "value_b2".into()) |
| ] |
| .into(), |
| }, |
| } |
| )] |
| .into(), |
| }, |
| config_data: NamedMap { |
| name: "config data".into(), |
| entries: [( |
| "config/one".into(), |
| FileEntry { |
| destination: "config/one".into(), |
| source: "config_data1".into(), |
| } |
| )] |
| .into(), |
| }, |
| } |
| ); |
| } |
| |
| #[test] |
| fn test_multiple_adds_fail() { |
| let mut builder = ConfigurationBuilderImpl::default(); |
| |
| assert!(builder.bootfs().component("foo").is_ok()); |
| assert!(builder.bootfs().component("foo").is_err()); |
| |
| assert!(builder.package("foo").component("bar").is_ok()); |
| assert!(builder.package("foo").component("bar").is_err()); |
| |
| let mut component = builder.package("foo").component("baz").unwrap(); |
| assert!(component.field("key", "value").is_ok()); |
| assert!(component.field("key", "diff_value").is_err()); |
| assert!(component.field("key2", "value2").is_ok()); |
| assert!(component.field("key2", "value2").is_err()); |
| |
| assert!(builder |
| .package("foo") |
| .config_data(FileEntry { destination: "bar".into(), source: "baz".into() }) |
| .is_ok()); |
| assert!(builder |
| .package("foo") |
| .config_data(FileEntry { destination: "cat".into(), source: "baz".into() }) |
| .is_ok()); |
| assert!(builder |
| .package("foo") |
| .config_data(FileEntry { destination: "cat".into(), source: "diz".into() }) |
| .is_err()); |
| } |
| |
| #[test] |
| fn test_error_messages_for_multiple_adds() { |
| // This test validates that the error messages produced by the builder, |
| // when their context() entries are flattened, produces sensical-looking |
| // errors. |
| |
| fn format_result<T>(result: Result<T>) -> String { |
| if let Err(e) = result { |
| format!( |
| "{} Failed{}", |
| e, |
| e.chain() |
| .skip(1) |
| .enumerate() |
| .map(|(i, e)| format!("\n {: >3}. {}", i + 1, e)) |
| .collect::<Vec<String>>() |
| .concat() |
| ) |
| } else { |
| "Not An error".into() |
| } |
| } |
| |
| let mut builder = ConfigurationBuilderImpl::default(); |
| |
| builder.bootfs().component("foo").unwrap(); |
| let result = builder.bootfs().component("foo").context("Configuring Subsystem"); |
| |
| assert_eq!( |
| format_result(result), |
| r"Configuring Subsystem Failed |
| 1. Setting configuration in bootfs |
| 2. Setting configuration for component: foo |
| 3. Each component's configuration can only be set once" |
| ); |
| |
| builder.package("foo").component("bar").unwrap(); |
| let result = builder.package("foo").component("bar").context("Configuring Subsystem"); |
| |
| assert_eq!( |
| format_result(result), |
| r"Configuring Subsystem Failed |
| 1. Setting configuration for package: foo |
| 2. Setting configuration for component: bar |
| 3. Each component's configuration can only be set once" |
| ); |
| |
| let mut component = builder.package("other").component("bar").unwrap(); |
| component.field("key", "value").unwrap(); |
| let result = component.field("key", "value2").context("Configuring Subsystem"); |
| |
| assert_eq!( |
| format_result(result), |
| r#"Configuring Subsystem Failed |
| 1. Each Structured Config field can only be set once for a component |
| 2. duplicate entry in structured config fields: |
| key: 'key' |
| existing value: String("value") |
| new value: String("value2")"# |
| ); |
| } |
| |
| #[test] |
| fn test_provides_feature() { |
| let board_info = BoardInformation { |
| name: "sample".to_owned(), |
| provided_features: vec!["feature_a".into(), "feature_b".into()], |
| }; |
| |
| assert!(board_info.provides_feature("feature_a")); |
| assert!(board_info.provides_feature("feature_b")); |
| assert!(!board_info.provides_feature("feature_c")); |
| } |
| } |