use std::{cell::RefCell, collections::HashSet, fs, marker::PhantomData, rc::Rc}; use anyhow::anyhow; use regex::Regex; use serde::{Deserialize, Serialize}; use support::{ constants::{ NO_ERR_DEF_BUILDER, RELAY_NOT_NONE, RW_FAILED, THIS_IS_A_BUG, VALIDATION_CHECK, VALID_REGEX, }, replacer::apply_env_replacements, }; use tracing::trace; use crate::{ global_settings::{GlobalSettings, GlobalSettingsBuilder}, hrmp_channel::{self, HrmpChannelConfig, HrmpChannelConfigBuilder}, teyrchain::{self, TeyrchainConfig, TeyrchainConfigBuilder}, relaychain::{self, RelaychainConfig, RelaychainConfigBuilder}, shared::{ errors::{ConfigError, ValidationError}, helpers::{generate_unique_node_name_from_names, merge_errors, merge_errors_vecs}, macros::states, node::NodeConfig, types::{Arg, AssetLocation, Chain, Command, Image, ValidationContext}, }, types::ParaId, RegistrationStrategy, }; /// A network configuration, composed of a relaychain, teyrchains and HRMP channels. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct NetworkConfig { #[serde(rename = "settings", default = "GlobalSettings::default")] global_settings: GlobalSettings, relaychain: Option, // Pezkuwi SDK: Added "teyrchains" alias for Pezkuwi terminology #[serde(alias = "teyrchains", skip_serializing_if = "std::vec::Vec::is_empty", default)] teyrchains: Vec, #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)] hrmp_channels: Vec, } impl NetworkConfig { /// The global settings of the network. pub fn global_settings(&self) -> &GlobalSettings { &self.global_settings } /// The relay chain of the network. pub fn relaychain(&self) -> &RelaychainConfig { self.relaychain .as_ref() .expect(&format!("{RELAY_NOT_NONE}, {THIS_IS_A_BUG}")) } /// The teyrchains of the network. pub fn teyrchains(&self) -> Vec<&TeyrchainConfig> { self.teyrchains.iter().collect::>() } /// Backward compatibility alias for teyrchains() - for external crates using Polkadot SDK terminology. pub fn parachains(&self) -> Vec<&TeyrchainConfig> { self.teyrchains() } /// The HRMP channels of the network. pub fn hrmp_channels(&self) -> Vec<&HrmpChannelConfig> { self.hrmp_channels.iter().collect::>() } fn set_teyrchains(&mut self, teyrchains: Vec) { self.teyrchains = teyrchains; } /// A helper function to dump the network configuration to a TOML string. pub fn dump_to_toml(&self) -> Result { // This regex is used to replace the "" enclosed u128 value to a raw u128 because u128 is not supported for TOML serialization/deserialization. let re = Regex::new(r#""U128%(?\d+)""#) .expect(&format!("{VALID_REGEX} {THIS_IS_A_BUG}")); let toml_string = toml::to_string_pretty(&self)?; Ok(re.replace_all(&toml_string, "$u128_value").to_string()) } /// A helper function to load a network configuration from a TOML file. pub fn load_from_toml_with_settings( path: &str, settings: &GlobalSettings, ) -> Result { let mut network_config = NetworkConfig::load_from_toml(path)?; network_config.global_settings = settings.clone(); Ok(network_config) } /// A helper function to load a network configuration from a TOML file. pub fn load_from_toml(path: &str) -> Result { let file_str = fs::read_to_string(path).expect(&format!("{RW_FAILED} {THIS_IS_A_BUG}")); let re: Regex = Regex::new(r"(?(initial_)?balance)\s+=\s+(?\d+)") .expect(&format!("{VALID_REGEX} {THIS_IS_A_BUG}")); let toml_text = re.replace_all(&file_str, "$field_name = \"$u128_value\""); trace!("toml text to parse: {}", toml_text); // apply replacements from env in toml let toml_text = apply_env_replacements(&toml_text); trace!("toml text after replacements: {}", toml_text); let mut network_config: NetworkConfig = toml::from_str(&toml_text)?; trace!("parsed config {network_config:#?}"); // All unwraps below are safe, because we ensure that the relaychain is not None at this point if network_config.relaychain.is_none() { Err(anyhow!("Relay chain does not exist."))? } // retrieve the defaults relaychain for assigning to nodes if needed let mut relaychain_default_command: Option = network_config.relaychain().default_command().cloned(); if relaychain_default_command.is_none() { relaychain_default_command = network_config.relaychain().command().cloned(); } let relaychain_default_image: Option = network_config.relaychain().default_image().cloned(); let relaychain_default_db_snapshot: Option = network_config.relaychain().default_db_snapshot().cloned(); let default_args: Vec = network_config .relaychain() .default_args() .into_iter() .cloned() .collect(); let mut nodes: Vec = network_config .relaychain() .nodes() .into_iter() .cloned() .collect(); let mut teyrchains: Vec = network_config.teyrchains().into_iter().cloned().collect(); // Validation checks for relay TryInto::::try_into(network_config.relaychain().chain().as_str())?; if relaychain_default_image.is_some() { TryInto::::try_into(relaychain_default_image.clone().expect(VALIDATION_CHECK))?; } if relaychain_default_command.is_some() { TryInto::::try_into( relaychain_default_command.clone().expect(VALIDATION_CHECK), )?; } // Keep track of node names to ensure uniqueness let mut names = HashSet::new(); for node in nodes.iter_mut() { if relaychain_default_command.is_some() { // we modify only nodes which don't already have a command if node.command.is_none() { node.command.clone_from(&relaychain_default_command); } } if relaychain_default_image.is_some() && node.image.is_none() { node.image.clone_from(&relaychain_default_image); } if relaychain_default_db_snapshot.is_some() && node.db_snapshot.is_none() { node.db_snapshot.clone_from(&relaychain_default_db_snapshot); } if !default_args.is_empty() && node.args().is_empty() { node.set_args(default_args.clone()); } let unique_name = generate_unique_node_name_from_names(node.name(), &mut names); node.name = unique_name; } for para in teyrchains.iter_mut() { // retrieve the defaults teyrchain for assigning to collators if needed let teyrchain_default_command: Option = para.default_command().cloned(); let teyrchain_default_image: Option = para.default_image().cloned(); let teyrchain_default_db_snapshot: Option = para.default_db_snapshot().cloned(); let default_args: Vec = para.default_args().into_iter().cloned().collect(); let mut collators: Vec = para.collators.clone(); for collator in collators.iter_mut() { populate_collator_with_defaults( collator, &teyrchain_default_command, &teyrchain_default_image, &teyrchain_default_db_snapshot, &default_args, ); let unique_name = generate_unique_node_name_from_names(collator.name(), &mut names); collator.name = unique_name; } para.collators = collators; if para.collator.is_some() { let mut collator = para.collator.clone().unwrap(); populate_collator_with_defaults( &mut collator, &teyrchain_default_command, &teyrchain_default_image, &teyrchain_default_db_snapshot, &default_args, ); let unique_name = generate_unique_node_name_from_names(collator.name(), &mut names); collator.name = unique_name; para.collator = Some(collator); } } network_config .relaychain .as_mut() .expect(&format!("{NO_ERR_DEF_BUILDER}, {THIS_IS_A_BUG}")) .set_nodes(nodes); network_config.set_teyrchains(teyrchains); // Validation checks for teyrchains network_config.teyrchains().iter().for_each(|teyrchain| { if teyrchain.default_image().is_some() { let _ = TryInto::::try_into(teyrchain.default_image().unwrap().as_str()); } if teyrchain.default_command().is_some() { let _ = TryInto::::try_into(teyrchain.default_command().unwrap().as_str()); } }); Ok(network_config) } } fn populate_collator_with_defaults( collator: &mut NodeConfig, teyrchain_default_command: &Option, teyrchain_default_image: &Option, teyrchain_default_db_snapshot: &Option, default_args: &[Arg], ) { if teyrchain_default_command.is_some() { // we modify only nodes which don't already have a command if collator.command.is_none() { collator.command.clone_from(teyrchain_default_command); } } if teyrchain_default_image.is_some() && collator.image.is_none() { collator.image.clone_from(teyrchain_default_image); } if teyrchain_default_db_snapshot.is_some() && collator.db_snapshot.is_none() { collator .db_snapshot .clone_from(teyrchain_default_db_snapshot); } if !default_args.is_empty() && collator.args().is_empty() { collator.set_args(default_args.to_owned()); } } states! { Initial, WithRelaychain } /// A network configuration builder, used to build a [`NetworkConfig`] declaratively with fields validation. /// /// # Example: /// /// ``` /// use zombienet_configuration::NetworkConfigBuilder; /// /// let network_config = NetworkConfigBuilder::new() /// .with_relaychain(|relaychain| { /// relaychain /// .with_chain("polkadot") /// .with_random_nominators_count(10) /// .with_default_resources(|resources| { /// resources /// .with_limit_cpu("1000m") /// .with_request_memory("1Gi") /// .with_request_cpu(100_000) /// }) /// .with_node(|node| { /// node.with_name("node") /// .with_command("command") /// .validator(true) /// }) /// }) /// .with_teyrchain(|teyrchain| { /// teyrchain /// .with_id(1000) /// .with_chain("myteyrchain1") /// .with_initial_balance(100_000) /// .with_default_image("myimage:version") /// .with_collator(|collator| { /// collator /// .with_name("collator1") /// .with_command("command1") /// .validator(true) /// }) /// }) /// .with_teyrchain(|teyrchain| { /// teyrchain /// .with_id(2000) /// .with_chain("myteyrchain2") /// .with_initial_balance(50_0000) /// .with_collator(|collator| { /// collator /// .with_name("collator2") /// .with_command("command2") /// .validator(true) /// }) /// }) /// .with_hrmp_channel(|hrmp_channel1| { /// hrmp_channel1 /// .with_sender(1) /// .with_recipient(2) /// .with_max_capacity(200) /// .with_max_message_size(500) /// }) /// .with_hrmp_channel(|hrmp_channel2| { /// hrmp_channel2 /// .with_sender(2) /// .with_recipient(1) /// .with_max_capacity(100) /// .with_max_message_size(250) /// }) /// .with_global_settings(|global_settings| { /// global_settings /// .with_network_spawn_timeout(1200) /// .with_node_spawn_timeout(240) /// }) /// .build(); /// /// assert!(network_config.is_ok()) /// ``` pub struct NetworkConfigBuilder { config: NetworkConfig, validation_context: Rc>, errors: Vec, _state: PhantomData, } impl Default for NetworkConfigBuilder { fn default() -> Self { Self { config: NetworkConfig { global_settings: GlobalSettingsBuilder::new() .build() .expect(&format!("{NO_ERR_DEF_BUILDER}, {THIS_IS_A_BUG}")), relaychain: None, teyrchains: vec![], hrmp_channels: vec![], }, validation_context: Default::default(), errors: vec![], _state: PhantomData, } } } impl NetworkConfigBuilder { fn transition( config: NetworkConfig, validation_context: Rc>, errors: Vec, ) -> NetworkConfigBuilder { NetworkConfigBuilder { config, errors, validation_context, _state: PhantomData, } } } impl NetworkConfigBuilder { pub fn new() -> NetworkConfigBuilder { Self::default() } /// uses the default options for both the relay chain and the nodes /// the only required fields are the name of the nodes, /// and the name of the relay chain ("rococo-local", "polkadot", etc.) pub fn with_chain_and_nodes( relay_name: &str, node_names: Vec, ) -> NetworkConfigBuilder { let network_config = NetworkConfigBuilder::new().with_relaychain(|relaychain| { let mut relaychain_with_node = relaychain .with_chain(relay_name) .with_node(|node| node.with_name(node_names.first().unwrap_or(&"".to_string()))); for node_name in node_names.iter().skip(1) { relaychain_with_node = relaychain_with_node .with_node(|node_builder| node_builder.with_name(node_name)); } relaychain_with_node }); Self::transition( network_config.config, network_config.validation_context, network_config.errors, ) } /// Set the relay chain using a nested [`RelaychainConfigBuilder`]. pub fn with_relaychain( self, f: impl FnOnce( RelaychainConfigBuilder, ) -> RelaychainConfigBuilder, ) -> NetworkConfigBuilder { match f(RelaychainConfigBuilder::new( self.validation_context.clone(), )) .build() { Ok(relaychain) => Self::transition( NetworkConfig { relaychain: Some(relaychain), ..self.config }, self.validation_context, self.errors, ), Err(errors) => Self::transition(self.config, self.validation_context, errors), } } } impl NetworkConfigBuilder { /// Set the global settings using a nested [`GlobalSettingsBuilder`]. pub fn with_global_settings( self, f: impl FnOnce(GlobalSettingsBuilder) -> GlobalSettingsBuilder, ) -> Self { match f(GlobalSettingsBuilder::new()).build() { Ok(global_settings) => Self::transition( NetworkConfig { global_settings, ..self.config }, self.validation_context, self.errors, ), Err(errors) => Self::transition( self.config, self.validation_context, merge_errors_vecs(self.errors, errors), ), } } /// Add a teyrchain using a nested [`TeyrchainConfigBuilder`]. pub fn with_teyrchain( self, f: impl FnOnce( TeyrchainConfigBuilder, ) -> TeyrchainConfigBuilder< teyrchain::states::WithAtLeastOneCollator, teyrchain::states::Bootstrap, >, ) -> Self { match f(TeyrchainConfigBuilder::new(self.validation_context.clone())).build() { Ok(teyrchain) => Self::transition( NetworkConfig { teyrchains: [self.config.teyrchains, vec![teyrchain]].concat(), ..self.config }, self.validation_context, self.errors, ), Err(errors) => Self::transition( self.config, self.validation_context, merge_errors_vecs(self.errors, errors), ), } } /// uses default settings for setting for: /// - the teyrchain, /// - the global settings /// - the hrmp channels /// /// the only required parameters are the names of the collators as a vector, /// and the id of the teyrchain pub fn with_teyrchain_id_and_collators(self, id: u32, collator_names: Vec) -> Self { if collator_names.is_empty() { return Self::transition( self.config, self.validation_context, merge_errors( self.errors, ConfigError::Teyrchain(id, ValidationError::CantBeEmpty().into()).into(), ), ); } self.with_teyrchain(|teyrchain| { let mut teyrchain_config = teyrchain.with_id(id).with_collator(|collator| { collator .with_name(collator_names.first().unwrap_or(&"".to_string())) .validator(true) }); for collator_name in collator_names.iter().skip(1) { teyrchain_config = teyrchain_config .with_collator(|collator| collator.with_name(collator_name).validator(true)); } teyrchain_config }) // TODO: if need to set global settings and hrmp channels // we can also do in here } /// Add an HRMP channel using a nested [`HrmpChannelConfigBuilder`]. pub fn with_hrmp_channel( self, f: impl FnOnce( HrmpChannelConfigBuilder, ) -> HrmpChannelConfigBuilder, ) -> Self { let new_hrmp_channel = f(HrmpChannelConfigBuilder::new()).build(); Self::transition( NetworkConfig { hrmp_channels: [self.config.hrmp_channels, vec![new_hrmp_channel]].concat(), ..self.config }, self.validation_context, self.errors, ) } /// Seals the builder and returns a [`NetworkConfig`] if there are no validation errors, else returns errors. pub fn build(self) -> Result> { let mut paras_to_register: HashSet = Default::default(); let mut errs: Vec = self .config .teyrchains .iter() .filter_map(|para| { if let Some(RegistrationStrategy::Manual) = para.registration_strategy() { return None; }; if paras_to_register.insert(para.id()) { None } else { // already in the set Some(anyhow!( "ParaId {} already set to be registered, only one should be.", para.id() )) } }) .collect(); if !self.errors.is_empty() || !errs.is_empty() { let mut ret_errs = self.errors; ret_errs.append(&mut errs); return Err(ret_errs); } Ok(self.config) } } #[cfg(test)] mod tests { use std::path::PathBuf; use super::*; use crate::teyrchain::RegistrationStrategy; #[test] fn network_config_builder_should_succeeds_and_returns_a_network_config() { let network_config = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("polkadot") .with_random_nominators_count(10) .with_node(|node| { node.with_name("node") .with_command("command") .validator(true) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1) .with_chain("myteyrchain1") .with_initial_balance(100_000) .with_collator(|collator| { collator .with_name("collator1") .with_command("command1") .validator(true) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(2) .with_chain("myteyrchain2") .with_initial_balance(0) .with_collator(|collator| { collator .with_name("collator2") .with_command("command2") .validator(true) }) }) .with_hrmp_channel(|hrmp_channel1| { hrmp_channel1 .with_sender(1) .with_recipient(2) .with_max_capacity(200) .with_max_message_size(500) }) .with_hrmp_channel(|hrmp_channel2| { hrmp_channel2 .with_sender(2) .with_recipient(1) .with_max_capacity(100) .with_max_message_size(250) }) .with_global_settings(|global_settings| { global_settings .with_network_spawn_timeout(1200) .with_node_spawn_timeout(240) }) .build() .unwrap(); // relaychain assert_eq!(network_config.relaychain().chain().as_str(), "polkadot"); assert_eq!(network_config.relaychain().nodes().len(), 1); let &node = network_config.relaychain().nodes().first().unwrap(); assert_eq!(node.name(), "node"); assert_eq!(node.command().unwrap().as_str(), "command"); assert!(node.is_validator()); assert_eq!( network_config .relaychain() .random_nominators_count() .unwrap(), 10 ); // teyrchains assert_eq!(network_config.teyrchains().len(), 2); // teyrchain1 let &teyrchain1 = network_config.teyrchains().first().unwrap(); assert_eq!(teyrchain1.id(), 1); assert_eq!(teyrchain1.collators().len(), 1); let &collator = teyrchain1.collators().first().unwrap(); assert_eq!(collator.name(), "collator1"); assert_eq!(collator.command().unwrap().as_str(), "command1"); assert!(collator.is_validator()); assert_eq!(teyrchain1.initial_balance(), 100_000); assert_eq!(teyrchain1.unique_id(), "1"); // teyrchain2 let &teyrchain2 = network_config.teyrchains().last().unwrap(); assert_eq!(teyrchain2.id(), 2); assert_eq!(teyrchain2.collators().len(), 1); let &collator = teyrchain2.collators().first().unwrap(); assert_eq!(collator.name(), "collator2"); assert_eq!(collator.command().unwrap().as_str(), "command2"); assert!(collator.is_validator()); assert_eq!(teyrchain2.initial_balance(), 0); // hrmp_channels assert_eq!(network_config.hrmp_channels().len(), 2); // hrmp_channel1 let &hrmp_channel1 = network_config.hrmp_channels().first().unwrap(); assert_eq!(hrmp_channel1.sender(), 1); assert_eq!(hrmp_channel1.recipient(), 2); assert_eq!(hrmp_channel1.max_capacity(), 200); assert_eq!(hrmp_channel1.max_message_size(), 500); // hrmp_channel2 let &hrmp_channel2 = network_config.hrmp_channels().last().unwrap(); assert_eq!(hrmp_channel2.sender(), 2); assert_eq!(hrmp_channel2.recipient(), 1); assert_eq!(hrmp_channel2.max_capacity(), 100); assert_eq!(hrmp_channel2.max_message_size(), 250); // global settings assert_eq!( network_config.global_settings().network_spawn_timeout(), 1200 ); assert_eq!(network_config.global_settings().node_spawn_timeout(), 240); } #[test] fn network_config_builder_should_fails_and_returns_multiple_errors_if_relaychain_is_invalid() { let errors = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("polkadot") .with_random_nominators_count(10) .with_default_image("invalid.image") .with_node(|node| { node.with_name("node") .with_command("invalid command") .validator(true) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1) .with_chain("myteyrchain") .with_initial_balance(100_000) .with_collator(|collator| { collator .with_name("collator1") .with_command("command1") .validator(true) }) }) .build() .unwrap_err(); assert_eq!(errors.len(), 2); assert_eq!( errors.first().unwrap().to_string(), "relaychain.default_image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'" ); assert_eq!( errors.get(1).unwrap().to_string(), "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace" ); } #[test] fn network_config_builder_should_fails_and_returns_multiple_errors_if_teyrchain_is_invalid() { let errors = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("polkadot") .with_random_nominators_count(10) .with_node(|node| { node.with_name("node") .with_command("command") .validator(true) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1000) .with_chain("myteyrchain") .with_initial_balance(100_000) .with_collator(|collator| { collator .with_name("collator1") .with_command("invalid command") .with_image("invalid.image") .validator(true) }) }) .build() .unwrap_err(); assert_eq!(errors.len(), 2); assert_eq!( errors.first().unwrap().to_string(), "teyrchain[1000].collators['collator1'].command: 'invalid command' shouldn't contains whitespace" ); assert_eq!( errors.get(1).unwrap().to_string(), "teyrchain[1000].collators['collator1'].image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'" ); } #[test] fn network_config_builder_should_fails_and_returns_multiple_errors_if_multiple_teyrchains_are_invalid( ) { let errors = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("polkadot") .with_random_nominators_count(10) .with_node(|node| { node.with_name("node") .with_command("command") .validator(true) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1000) .with_chain("myteyrchain1") .with_initial_balance(100_000) .with_collator(|collator| { collator .with_name("collator1") .with_command("invalid command") .validator(true) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(2000) .with_chain("myteyrchain2") .with_initial_balance(100_000) .with_collator(|collator| { collator .with_name("collator2") .validator(true) .with_resources(|resources| { resources .with_limit_cpu("1000m") .with_request_memory("1Gi") .with_request_cpu("invalid") }) }) }) .build() .unwrap_err(); assert_eq!(errors.len(), 2); assert_eq!( errors.first().unwrap().to_string(), "teyrchain[1000].collators['collator1'].command: 'invalid command' shouldn't contains whitespace" ); assert_eq!( errors.get(1).unwrap().to_string(), "teyrchain[2000].collators['collator2'].resources.request_cpu: 'invalid' doesn't match regex '^\\d+(.\\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'" ); } #[test] fn network_config_builder_should_fails_and_returns_multiple_errors_if_global_settings_is_invalid( ) { let errors = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("polkadot") .with_random_nominators_count(10) .with_node(|node| { node.with_name("node") .with_command("command") .validator(true) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1000) .with_chain("myteyrchain") .with_initial_balance(100_000) .with_collator(|collator| { collator .with_name("collator") .with_command("command") .validator(true) }) }) .with_global_settings(|global_settings| { global_settings .with_local_ip("127.0.0000.1") .with_bootnodes_addresses(vec!["/ip4//tcp/45421"]) }) .build() .unwrap_err(); assert_eq!(errors.len(), 2); assert_eq!( errors.first().unwrap().to_string(), "global_settings.local_ip: invalid IP address syntax" ); assert_eq!( errors.get(1).unwrap().to_string(), "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax" ); } #[test] fn network_config_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid( ) { let errors = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("polkadot") .with_random_nominators_count(10) .with_node(|node| { node.with_name("node") .with_command("invalid command") .validator(true) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1000) .with_chain("myteyrchain") .with_initial_balance(100_000) .with_collator(|collator| { collator .with_name("collator") .with_command("command") .with_image("invalid.image") .validator(true) }) }) .with_global_settings(|global_settings| global_settings.with_local_ip("127.0.0000.1")) .build() .unwrap_err(); assert_eq!(errors.len(), 3); assert_eq!( errors.first().unwrap().to_string(), "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace" ); assert_eq!( errors.get(1).unwrap().to_string(), "teyrchain[1000].collators['collator'].image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'" ); assert_eq!( errors.get(2).unwrap().to_string(), "global_settings.local_ip: invalid IP address syntax" ); } #[test] fn network_config_should_be_dumpable_to_a_toml_config_for_a_small_network() { let network_config = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("rococo-local") .with_default_command("polkadot") .with_default_image("docker.io/parity/polkadot:latest") .with_default_args(vec![("-lteyrchain", "debug").into()]) .with_node(|node| node.with_name("alice").validator(true)) .with_node(|node| { node.with_name("bob") .validator(true) .invulnerable(false) .bootnode(true) .with_args(vec![("--database", "paritydb-experimental").into()]) }) }) .build() .unwrap(); let got = network_config.dump_to_toml().unwrap(); let expected = fs::read_to_string("./testing/snapshots/0000-small-network.toml").unwrap(); assert_eq!(got, expected); } #[test] fn network_config_should_be_dumpable_to_a_toml_config_for_a_big_network() { let network_config = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("polkadot") .with_default_command("polkadot") .with_default_image("docker.io/parity/polkadot:latest") .with_default_resources(|resources| { resources .with_request_cpu(100000) .with_request_memory("500M") .with_limit_cpu("10Gi") .with_limit_memory("4000M") }) .with_node(|node| { node.with_name("alice") .with_initial_balance(1_000_000_000) .validator(true) .bootnode(true) .invulnerable(true) }) .with_node(|node| { node.with_name("bob") .validator(true) .invulnerable(true) .bootnode(true) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1000) .with_chain("myteyrchain") .with_chain_spec_path("/path/to/my/chain/spec.json") .with_registration_strategy(RegistrationStrategy::UsingExtrinsic) .onboard_as_teyrchain(false) .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz") .with_collator(|collator| { collator .with_name("john") .bootnode(true) .validator(true) .invulnerable(true) .with_initial_balance(5_000_000_000) }) .with_collator(|collator| { collator .with_name("charles") .validator(false) .bootnode(true) .invulnerable(true) .with_initial_balance(0) }) .with_collator(|collator| { collator .with_name("frank") .validator(true) .invulnerable(false) .bootnode(true) .with_initial_balance(1_000_000_000) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(2000) .with_chain("myotherteyrchain") .with_chain_spec_path("/path/to/my/other/chain/spec.json") .with_collator(|collator| { collator .with_name("mike") .bootnode(true) .validator(true) .invulnerable(true) .with_initial_balance(5_000_000_000) }) .with_collator(|collator| { collator .with_name("georges") .validator(false) .bootnode(true) .invulnerable(true) .with_initial_balance(0) }) .with_collator(|collator| { collator .with_name("victor") .validator(true) .invulnerable(false) .bootnode(true) .with_initial_balance(1_000_000_000) }) }) .with_hrmp_channel(|hrmp_channel| { hrmp_channel .with_sender(1000) .with_recipient(2000) .with_max_capacity(150) .with_max_message_size(5000) }) .with_hrmp_channel(|hrmp_channel| { hrmp_channel .with_sender(2000) .with_recipient(1000) .with_max_capacity(200) .with_max_message_size(8000) }) .build() .unwrap(); let got = network_config.dump_to_toml().unwrap(); let expected = fs::read_to_string("./testing/snapshots/0001-big-network.toml").unwrap(); assert_eq!(got, expected); } #[test] fn network_config_builder_should_be_dumplable_to_a_toml_config_a_overrides_default_correctly() { let network_config = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("polkadot") .with_default_command("polkadot") .with_default_image("docker.io/parity/polkadot:latest") .with_default_args(vec![("-name", "value").into(), "--flag".into()]) .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz") .with_default_resources(|resources| { resources .with_request_cpu(100000) .with_request_memory("500M") .with_limit_cpu("10Gi") .with_limit_memory("4000M") }) .with_node(|node| { node.with_name("alice") .with_initial_balance(1_000_000_000) .validator(true) .bootnode(true) .invulnerable(true) }) .with_node(|node| { node.with_name("bob") .validator(true) .invulnerable(true) .bootnode(true) .with_image("mycustomimage:latest") .with_command("my-custom-command") .with_db_snapshot("https://storage.com/path/to/other/db_snapshot.tgz") .with_resources(|resources| { resources .with_request_cpu(1000) .with_request_memory("250Mi") .with_limit_cpu("5Gi") .with_limit_memory("2Gi") }) .with_args(vec![("-myothername", "value").into()]) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1000) .with_chain("myteyrchain") .with_chain_spec_path("/path/to/my/chain/spec.json") .with_default_db_snapshot("https://storage.com/path/to/other_snapshot.tgz") .with_default_command("my-default-command") .with_default_image("mydefaultimage:latest") .with_collator(|collator| { collator .with_name("john") .bootnode(true) .validator(true) .invulnerable(true) .with_initial_balance(5_000_000_000) .with_command("my-non-default-command") .with_image("anotherimage:latest") }) .with_collator(|collator| { collator .with_name("charles") .validator(false) .bootnode(true) .invulnerable(true) .with_initial_balance(0) }) }) .build() .unwrap(); let got = network_config.dump_to_toml().unwrap(); let expected = fs::read_to_string("./testing/snapshots/0002-overridden-defaults.toml").unwrap(); assert_eq!(got, expected); } #[test] fn the_toml_config_with_custom_settings() { let settings = GlobalSettingsBuilder::new() .with_base_dir("/tmp/test-demo") .build() .unwrap(); let load_from_toml = NetworkConfig::load_from_toml_with_settings( "./testing/snapshots/0000-small-network.toml", &settings, ) .unwrap(); assert_eq!( Some(PathBuf::from("/tmp/test-demo").as_path()), load_from_toml.global_settings.base_dir() ); } #[test] fn the_toml_config_should_be_imported_and_match_a_network() { let load_from_toml = NetworkConfig::load_from_toml("./testing/snapshots/0000-small-network.toml").unwrap(); let expected = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("rococo-local") .with_default_command("polkadot") .with_default_image("docker.io/parity/polkadot:latest") .with_default_args(vec![("-lteyrchain=debug").into()]) .with_node(|node| { node.with_name("alice") .validator(true) .invulnerable(true) .validator(true) .bootnode(false) .with_initial_balance(2000000000000) }) .with_node(|node| { node.with_name("bob") .with_args(vec![("--database", "paritydb-experimental").into()]) .validator(true) .invulnerable(false) .bootnode(true) .with_initial_balance(2000000000000) }) }) .build() .unwrap(); // We need to assert parts of the network config separately because the expected one contains the chain default context which // is used for dumbing to tomp while the // while loaded assert_eq!( expected.relaychain().chain(), load_from_toml.relaychain().chain() ); assert_eq!( expected.relaychain().default_args(), load_from_toml.relaychain().default_args() ); assert_eq!( expected.relaychain().default_command(), load_from_toml.relaychain().default_command() ); assert_eq!( expected.relaychain().default_image(), load_from_toml.relaychain().default_image() ); // Check the nodes without the Chain Default Context expected .relaychain() .nodes() .iter() .zip(load_from_toml.relaychain().nodes().iter()) .for_each(|(expected_node, loaded_node)| { assert_eq!(expected_node.name(), loaded_node.name()); assert_eq!(expected_node.command(), loaded_node.command()); assert_eq!(expected_node.args(), loaded_node.args()); assert_eq!( expected_node.is_invulnerable(), loaded_node.is_invulnerable() ); assert_eq!(expected_node.is_validator(), loaded_node.is_validator()); assert_eq!(expected_node.is_bootnode(), loaded_node.is_bootnode()); assert_eq!( expected_node.initial_balance(), loaded_node.initial_balance() ); }); } #[test] fn the_toml_config_without_settings_should_be_imported_and_match_a_network() { let load_from_toml = NetworkConfig::load_from_toml( "./testing/snapshots/0004-small-network-without-settings.toml", ) .unwrap(); let expected = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("rococo-local") .with_default_command("polkadot") .with_node(|node| node.with_name("alice")) .with_node(|node| node.with_name("bob")) }) .build() .unwrap(); assert_eq!( load_from_toml.global_settings().network_spawn_timeout(), expected.global_settings().network_spawn_timeout() ) } #[test] fn the_toml_config_should_be_imported_and_match_a_network_with_teyrchains() { let load_from_toml = NetworkConfig::load_from_toml("./testing/snapshots/0001-big-network.toml").unwrap(); let expected = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("polkadot") .with_default_command("polkadot") .with_default_image("docker.io/parity/polkadot:latest") .with_default_resources(|resources| { resources .with_request_cpu(100000) .with_request_memory("500M") .with_limit_cpu("10Gi") .with_limit_memory("4000M") }) .with_node(|node| { node.with_name("alice") .with_initial_balance(1_000_000_000) .validator(true) .bootnode(true) .invulnerable(true) }) .with_node(|node| { node.with_name("bob") .validator(true) .invulnerable(true) .bootnode(true) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1000) .with_chain("myteyrchain") .with_chain_spec_path("/path/to/my/chain/spec.json") .with_registration_strategy(RegistrationStrategy::UsingExtrinsic) .onboard_as_teyrchain(false) .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz") .with_collator(|collator| { collator .with_name("john") .bootnode(true) .validator(true) .invulnerable(true) .with_initial_balance(5_000_000_000) }) .with_collator(|collator| { collator .with_name("charles") .bootnode(true) .validator(false) .invulnerable(true) .with_initial_balance(0) }) .with_collator(|collator| { collator .with_name("frank") .validator(true) .invulnerable(false) .bootnode(true) .with_initial_balance(1_000_000_000) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(2000) .with_chain("myotherteyrchain") .with_chain_spec_path("/path/to/my/other/chain/spec.json") .with_collator(|collator| { collator .with_name("mike") .bootnode(true) .validator(true) .invulnerable(true) .with_initial_balance(5_000_000_000) }) .with_collator(|collator| { collator .with_name("georges") .bootnode(true) .validator(false) .invulnerable(true) .with_initial_balance(0) }) .with_collator(|collator| { collator .with_name("victor") .validator(true) .invulnerable(false) .bootnode(true) .with_initial_balance(1_000_000_000) }) }) .with_hrmp_channel(|hrmp_channel| { hrmp_channel .with_sender(1000) .with_recipient(2000) .with_max_capacity(150) .with_max_message_size(5000) }) .with_hrmp_channel(|hrmp_channel| { hrmp_channel .with_sender(2000) .with_recipient(1000) .with_max_capacity(200) .with_max_message_size(8000) }) .build() .unwrap(); // Check the relay chain assert_eq!( expected.relaychain().default_resources(), load_from_toml.relaychain().default_resources() ); // Check the nodes without the Chain Default Context expected .relaychain() .nodes() .iter() .zip(load_from_toml.relaychain().nodes().iter()) .for_each(|(expected_node, loaded_node)| { assert_eq!(expected_node.name(), loaded_node.name()); assert_eq!(expected_node.command(), loaded_node.command()); assert_eq!(expected_node.args(), loaded_node.args()); assert_eq!(expected_node.is_validator(), loaded_node.is_validator()); assert_eq!(expected_node.is_bootnode(), loaded_node.is_bootnode()); assert_eq!( expected_node.initial_balance(), loaded_node.initial_balance() ); assert_eq!( expected_node.is_invulnerable(), loaded_node.is_invulnerable() ); }); expected .teyrchains() .iter() .zip(load_from_toml.teyrchains().iter()) .for_each(|(expected_teyrchain, loaded_teyrchain)| { assert_eq!(expected_teyrchain.id(), loaded_teyrchain.id()); assert_eq!(expected_teyrchain.chain(), loaded_teyrchain.chain()); assert_eq!( expected_teyrchain.chain_spec_path(), loaded_teyrchain.chain_spec_path() ); assert_eq!( expected_teyrchain.registration_strategy(), loaded_teyrchain.registration_strategy() ); assert_eq!( expected_teyrchain.onboard_as_teyrchain(), loaded_teyrchain.onboard_as_teyrchain() ); assert_eq!( expected_teyrchain.default_db_snapshot(), loaded_teyrchain.default_db_snapshot() ); assert_eq!( expected_teyrchain.default_command(), loaded_teyrchain.default_command() ); assert_eq!( expected_teyrchain.default_image(), loaded_teyrchain.default_image() ); assert_eq!( expected_teyrchain.collators().len(), loaded_teyrchain.collators().len() ); expected_teyrchain .collators() .iter() .zip(loaded_teyrchain.collators().iter()) .for_each(|(expected_collator, loaded_collator)| { assert_eq!(expected_collator.name(), loaded_collator.name()); assert_eq!(expected_collator.command(), loaded_collator.command()); assert_eq!(expected_collator.image(), loaded_collator.image()); assert_eq!( expected_collator.is_validator(), loaded_collator.is_validator() ); assert_eq!( expected_collator.is_bootnode(), loaded_collator.is_bootnode() ); assert_eq!( expected_collator.is_invulnerable(), loaded_collator.is_invulnerable() ); assert_eq!( expected_collator.initial_balance(), loaded_collator.initial_balance() ); }); }); expected .hrmp_channels() .iter() .zip(load_from_toml.hrmp_channels().iter()) .for_each(|(expected_hrmp_channel, loaded_hrmp_channel)| { assert_eq!(expected_hrmp_channel.sender(), loaded_hrmp_channel.sender()); assert_eq!( expected_hrmp_channel.recipient(), loaded_hrmp_channel.recipient() ); assert_eq!( expected_hrmp_channel.max_capacity(), loaded_hrmp_channel.max_capacity() ); assert_eq!( expected_hrmp_channel.max_message_size(), loaded_hrmp_channel.max_message_size() ); }); } #[test] fn the_toml_config_should_be_imported_and_match_a_network_with_overriden_defaults() { let load_from_toml = NetworkConfig::load_from_toml("./testing/snapshots/0002-overridden-defaults.toml") .unwrap(); let expected = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("polkadot") .with_default_command("polkadot") .with_default_image("docker.io/parity/polkadot:latest") .with_default_args(vec![("-name", "value").into(), "--flag".into()]) .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz") .with_default_resources(|resources| { resources .with_request_cpu(100000) .with_request_memory("500M") .with_limit_cpu("10Gi") .with_limit_memory("4000M") }) .with_node(|node| { node.with_name("alice") .with_initial_balance(1_000_000_000) .validator(true) .bootnode(true) .invulnerable(true) }) .with_node(|node| { node.with_name("bob") .validator(true) .invulnerable(true) .bootnode(true) .with_image("mycustomimage:latest") .with_command("my-custom-command") .with_db_snapshot("https://storage.com/path/to/other/db_snapshot.tgz") .with_resources(|resources| { resources .with_request_cpu(1000) .with_request_memory("250Mi") .with_limit_cpu("5Gi") .with_limit_memory("2Gi") }) .with_args(vec![("-myothername", "value").into()]) }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1000) .with_chain("myteyrchain") .with_chain_spec_path("/path/to/my/chain/spec.json") .with_default_db_snapshot("https://storage.com/path/to/other_snapshot.tgz") .with_default_command("my-default-command") .with_default_image("mydefaultimage:latest") .with_collator(|collator| { collator .with_name("john") .bootnode(true) .validator(true) .invulnerable(true) .with_initial_balance(5_000_000_000) .with_command("my-non-default-command") .with_image("anotherimage:latest") }) .with_collator(|collator| { collator .with_name("charles") .bootnode(true) .validator(false) .invulnerable(true) .with_initial_balance(0) }) }) .build() .unwrap(); expected .teyrchains() .iter() .zip(load_from_toml.teyrchains().iter()) .for_each(|(expected_teyrchain, loaded_teyrchain)| { assert_eq!(expected_teyrchain.id(), loaded_teyrchain.id()); assert_eq!(expected_teyrchain.chain(), loaded_teyrchain.chain()); assert_eq!( expected_teyrchain.chain_spec_path(), loaded_teyrchain.chain_spec_path() ); assert_eq!( expected_teyrchain.registration_strategy(), loaded_teyrchain.registration_strategy() ); assert_eq!( expected_teyrchain.onboard_as_teyrchain(), loaded_teyrchain.onboard_as_teyrchain() ); assert_eq!( expected_teyrchain.default_db_snapshot(), loaded_teyrchain.default_db_snapshot() ); assert_eq!( expected_teyrchain.default_command(), loaded_teyrchain.default_command() ); assert_eq!( expected_teyrchain.default_image(), loaded_teyrchain.default_image() ); assert_eq!( expected_teyrchain.collators().len(), loaded_teyrchain.collators().len() ); expected_teyrchain .collators() .iter() .zip(loaded_teyrchain.collators().iter()) .for_each(|(expected_collator, loaded_collator)| { assert_eq!(expected_collator.name(), loaded_collator.name()); assert_eq!(expected_collator.command(), loaded_collator.command()); assert_eq!(expected_collator.image(), loaded_collator.image()); assert_eq!( expected_collator.is_validator(), loaded_collator.is_validator() ); assert_eq!( expected_collator.is_bootnode(), loaded_collator.is_bootnode() ); assert_eq!( expected_collator.is_invulnerable(), loaded_collator.is_invulnerable() ); assert_eq!( expected_collator.initial_balance(), loaded_collator.initial_balance() ); }); }); } #[test] fn with_chain_and_nodes_works() { let network_config = NetworkConfigBuilder::with_chain_and_nodes( "rococo-local", vec!["alice".to_string(), "bob".to_string()], ) .build() .unwrap(); // relaychain assert_eq!(network_config.relaychain().chain().as_str(), "rococo-local"); assert_eq!(network_config.relaychain().nodes().len(), 2); let mut node_names = network_config.relaychain().nodes().into_iter(); let node1 = node_names.next().unwrap().name(); assert_eq!(node1, "alice"); let node2 = node_names.next().unwrap().name(); assert_eq!(node2, "bob"); // teyrchains assert_eq!(network_config.teyrchains().len(), 0); } #[test] fn with_chain_and_nodes_should_fail_with_empty_relay_name() { let errors = NetworkConfigBuilder::with_chain_and_nodes("", vec!["alice".to_string()]) .build() .unwrap_err(); assert_eq!( errors.first().unwrap().to_string(), "relaychain.chain: can't be empty" ); } #[test] fn with_chain_and_nodes_should_fail_with_empty_node_list() { let errors = NetworkConfigBuilder::with_chain_and_nodes("rococo-local", vec![]) .build() .unwrap_err(); assert_eq!( errors.first().unwrap().to_string(), "relaychain.nodes[''].name: can't be empty" ); } #[test] fn with_chain_and_nodes_should_fail_with_empty_node_name() { let errors = NetworkConfigBuilder::with_chain_and_nodes( "rococo-local", vec!["alice".to_string(), "".to_string()], ) .build() .unwrap_err(); assert_eq!( errors.first().unwrap().to_string(), "relaychain.nodes[''].name: can't be empty" ); } #[test] fn with_teyrchain_id_and_collators_works() { let network_config = NetworkConfigBuilder::with_chain_and_nodes( "rococo-local", vec!["alice".to_string(), "bob".to_string()], ) .with_teyrchain_id_and_collators( 100, vec!["collator1".to_string(), "collator2".to_string()], ) .build() .unwrap(); // relaychain assert_eq!(network_config.relaychain().chain().as_str(), "rococo-local"); assert_eq!(network_config.relaychain().nodes().len(), 2); let mut node_names = network_config.relaychain().nodes().into_iter(); let node1 = node_names.next().unwrap().name(); assert_eq!(node1, "alice"); let node2 = node_names.next().unwrap().name(); assert_eq!(node2, "bob"); // teyrchains assert_eq!(network_config.teyrchains().len(), 1); let &teyrchain1 = network_config.teyrchains().first().unwrap(); assert_eq!(teyrchain1.id(), 100); assert_eq!(teyrchain1.collators().len(), 2); let mut collator_names = teyrchain1.collators().into_iter(); let collator1 = collator_names.next().unwrap().name(); assert_eq!(collator1, "collator1"); let collator2 = collator_names.next().unwrap().name(); assert_eq!(collator2, "collator2"); assert_eq!(teyrchain1.initial_balance(), 2_000_000_000_000); } #[test] fn with_teyrchain_id_and_collators_should_fail_with_empty_collator_list() { let errors = NetworkConfigBuilder::with_chain_and_nodes("polkadot", vec!["alice".to_string()]) .with_teyrchain_id_and_collators(1, vec![]) .build() .unwrap_err(); assert_eq!( errors.first().unwrap().to_string(), "teyrchain[1].can't be empty" ); } #[test] fn with_teyrchain_id_and_collators_should_fail_with_empty_collator_name() { let errors = NetworkConfigBuilder::with_chain_and_nodes("polkadot", vec!["alice".to_string()]) .with_teyrchain_id_and_collators(1, vec!["collator1".to_string(), "".to_string()]) .build() .unwrap_err(); assert_eq!( errors.first().unwrap().to_string(), "teyrchain[1].collators[''].name: can't be empty" ); } #[test] fn wasm_override_in_toml_should_work() { let load_from_toml = NetworkConfig::load_from_toml( "./testing/snapshots/0005-small-networl-with-wasm-override.toml", ) .unwrap(); let expected = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("rococo-local") .with_default_command("polkadot") .with_wasm_override("/some/path/runtime.wasm") .with_node(|node| node.with_name("alice")) .with_node(|node| node.with_name("bob")) }) .with_teyrchain(|p| { p.with_id(1000) .with_wasm_override("https://some.com/runtime.wasm") .with_collator(|c| c.with_name("john")) }) .build() .unwrap(); assert_eq!( load_from_toml.relaychain().wasm_override(), expected.relaychain().wasm_override() ); assert_eq!( load_from_toml.teyrchains()[0].wasm_override(), expected.teyrchains()[0].wasm_override() ); } #[test] fn multiple_paras_with_same_id_should_work() { let network_config = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("polkadot") .with_node(|node| node.with_name("node").with_command("command")) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1) .with_chain("myteyrchain1") .with_collator(|collator| { collator.with_name("collator1").with_command("command1") }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1) .with_chain("myteyrchain1") .with_registration_strategy(RegistrationStrategy::Manual) .with_collator(|collator| { collator.with_name("collator2").with_command("command1") }) }) .build() .unwrap(); let &teyrchain2 = network_config.teyrchains().last().unwrap(); assert_eq!(teyrchain2.unique_id(), "1-1"); } #[test] fn multiple_paras_with_same_id_both_for_register_should_fail() { let errors = NetworkConfigBuilder::new() .with_relaychain(|relaychain| { relaychain .with_chain("polkadot") .with_node(|node| node.with_name("node").with_command("command")) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1) .with_chain("myteyrchain1") .with_collator(|collator| { collator.with_name("collator1").with_command("command1") }) }) .with_teyrchain(|teyrchain| { teyrchain .with_id(1) .with_chain("myteyrchain1") // .with_registration_strategy(RegistrationStrategy::UsingExtrinsic) .with_collator(|collator| { collator .with_name("collator2") .with_command("command1") }) }) .build() .unwrap_err(); assert_eq!( errors.first().unwrap().to_string(), "ParaId 1 already set to be registered, only one should be." ); } #[test] fn network_config_should_work_from_toml_without_chain_name() { let loaded_from_toml = NetworkConfig::load_from_toml("./testing/snapshots/0006-without-rc-chain-name.toml") .unwrap(); assert_eq!( "rococo-local", loaded_from_toml.relaychain().chain().as_str() ); } #[test] fn network_config_should_work_from_toml_with_duplicate_name_between_collator_and_relay_node() { let loaded_from_toml = NetworkConfig::load_from_toml( "./testing/snapshots/0007-small-network_w_teyrchain_w_duplicate_node_names.toml", ) .unwrap(); assert_eq!( loaded_from_toml .relaychain() .nodes() .iter() .filter(|n| n.name() == "alice") .count(), 1 ); assert_eq!( loaded_from_toml .teyrchains() .iter() .flat_map(|para| para.collators()) .filter(|n| n.name() == "alice-1") .count(), 1 ); } }