Skip to main content

zombienet_configuration/
network.rs

1use std::{
2    cell::RefCell,
3    collections::HashSet,
4    fs,
5    marker::PhantomData,
6    path::{Path, PathBuf},
7    rc::Rc,
8};
9
10use anyhow::anyhow;
11use regex::{Captures, Regex};
12use serde::{Deserialize, Serialize};
13use support::{
14    constants::{NO_ERR_DEF_BUILDER, RELAY_NOT_NONE, THIS_IS_A_BUG, VALIDATION_CHECK, VALID_REGEX},
15    replacer::apply_env_replacements,
16};
17use tracing::trace;
18
19use crate::{
20    custom_process,
21    global_settings::{GlobalSettings, GlobalSettingsBuilder},
22    hrmp_channel::{self, HrmpChannelConfig, HrmpChannelConfigBuilder},
23    parachain::{self, ParachainConfig, ParachainConfigBuilder},
24    relaychain::{self, RelaychainConfig, RelaychainConfigBuilder},
25    shared::{
26        errors::{ConfigError, ValidationError},
27        helpers::{generate_unique_node_name_from_names, merge_errors, merge_errors_vecs},
28        macros::states,
29        node::{GroupNodeConfig, NodeConfig},
30        types::{Arg, AssetLocation, Chain, Command, Image, ValidationContext},
31    },
32    types::ParaId,
33    CustomProcess, CustomProcessBuilder, RegistrationStrategy,
34};
35
36/// A network configuration, composed of a relaychain, parachains and HRMP channels.
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub struct NetworkConfig {
39    #[serde(rename = "settings", default = "GlobalSettings::default")]
40    global_settings: GlobalSettings,
41    relaychain: Option<RelaychainConfig>,
42    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
43    parachains: Vec<ParachainConfig>,
44    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
45    hrmp_channels: Vec<HrmpChannelConfig>,
46    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
47    custom_processes: Vec<CustomProcess>,
48}
49
50impl NetworkConfig {
51    /// The global settings of the network.
52    pub fn global_settings(&self) -> &GlobalSettings {
53        &self.global_settings
54    }
55
56    /// The relay chain of the network.
57    pub fn relaychain(&self) -> &RelaychainConfig {
58        self.relaychain
59            .as_ref()
60            .expect(&format!("{RELAY_NOT_NONE}, {THIS_IS_A_BUG}"))
61    }
62
63    /// The parachains of the network.
64    pub fn parachains(&self) -> Vec<&ParachainConfig> {
65        self.parachains.iter().collect::<Vec<_>>()
66    }
67
68    /// The HRMP channels of the network.
69    pub fn hrmp_channels(&self) -> Vec<&HrmpChannelConfig> {
70        self.hrmp_channels.iter().collect::<Vec<_>>()
71    }
72
73    /// The Custom process to spawn
74    pub fn custom_processes(&self) -> Vec<&CustomProcess> {
75        self.custom_processes.iter().collect::<Vec<_>>()
76    }
77
78    /// Set the parachain.
79    fn set_parachains(&mut self, parachains: Vec<ParachainConfig>) {
80        self.parachains = parachains;
81    }
82
83    /// A helper function to dump the network configuration to a TOML string.
84    pub fn dump_to_toml(&self) -> Result<String, toml::ser::Error> {
85        // This regex is used to replace the "" enclosed u128 value to a raw u128 because u128 is not supported for TOML serialization/deserialization.
86        let re = Regex::new(r#""U128%(?<u128_value>\d+)""#)
87            .expect(&format!("{VALID_REGEX} {THIS_IS_A_BUG}"));
88        let toml_string = toml::to_string_pretty(&self)?;
89
90        Ok(re.replace_all(&toml_string, "$u128_value").to_string())
91    }
92
93    /// A helper function to load a network configuration from a TOML file.
94    pub fn load_from_toml_with_settings(
95        path: &str,
96        settings: &GlobalSettings,
97    ) -> Result<NetworkConfig, anyhow::Error> {
98        let mut network_config = NetworkConfig::load_from_toml(path)?;
99        network_config.global_settings = settings.clone();
100        Ok(network_config)
101    }
102
103    /// A helper function to load a network configuration from a TOML file.
104    pub fn load_from_toml(path: &str) -> Result<NetworkConfig, anyhow::Error> {
105        let file_path = PathBuf::from(path).canonicalize()?;
106        let network_definition_dir = file_path
107            .parent()
108            .ok_or(anyhow!("should have a base dir"))?;
109        trace!(
110            "file_dir for replacer: {:?}",
111            network_definition_dir.as_os_str()
112        );
113        let file_str = fs::read_to_string(path)
114            .map_err(|e| anyhow!("Failed to read configuration file '{}': {}", path, e))?;
115
116        Self::load_from_toml_string_with_base_dir(&file_str, network_definition_dir)
117    }
118
119    /// A helper function to load a network configuration from a TOML string.
120    ///
121    /// Relative paths (e.g. `command = "./bin/polkadot"`) are resolved against the current
122    /// working directory, since there is no configuration file to use as base. Use
123    /// [`NetworkConfig::load_from_toml_string_with_base_dir`] to resolve them against a
124    /// specific directory instead.
125    pub fn load_from_toml_string(toml_data: &str) -> Result<NetworkConfig, anyhow::Error> {
126        let base_dir = std::env::current_dir()?;
127
128        Self::load_from_toml_string_with_base_dir(toml_data, &base_dir)
129    }
130
131    /// A helper function to load a network configuration from a TOML string, resolving
132    /// relative paths (e.g. `command = "./bin/polkadot"`) against `base_dir`.
133    pub fn load_from_toml_string_with_base_dir(
134        toml_text: &str,
135        base_dir: &Path,
136    ) -> Result<NetworkConfig, anyhow::Error> {
137        let re: Regex = Regex::new(r"(?<field_name>(initial_)?balance)\s+=\s+(?<u128_value>\d+)")
138            .expect(&format!("{VALID_REGEX} {THIS_IS_A_BUG}"));
139
140        let re_paths: Regex = Regex::new(r#"(?<field_name>(default_)?(command|db_snapshot|chain_spec_path|genesis_wasm_path|genesis_state_path))\s+=\s+\"(?<value_string>[a-zA-Z0-9\.\/]+)\""#)
141            .expect(&format!("{VALID_REGEX} {THIS_IS_A_BUG}"));
142
143        let path_replacer = |caps: &Captures| {
144            trace!("cmd replacer captures: {:?}", caps);
145            let cmd = maybe_absolute(&caps["value_string"], base_dir);
146            let line = format!("{} = \"{cmd}\"", &caps["field_name"]);
147            trace!("line after replacer: {line}");
148            line
149        };
150
151        let toml_text = re.replace_all(toml_text, "$field_name = \"$u128_value\"");
152        let toml_text = re_paths.replace_all(&toml_text, &path_replacer);
153
154        trace!("toml text to parse: {}", toml_text);
155        // apply replacements from env in toml
156        let toml_text = apply_env_replacements(&toml_text);
157        trace!("toml text after replacements: {}", toml_text);
158        let mut network_config: NetworkConfig = toml::from_str(&toml_text)?;
159        trace!("parsed config {network_config:#?}");
160
161        // All unwraps below are safe, because we ensure that the relaychain is not None at this point
162        if network_config.relaychain.is_none() {
163            Err(anyhow!("Relay chain does not exist."))?
164        }
165
166        // retrieve the defaults relaychain for assigning to nodes if needed
167        let mut relaychain_default_command: Option<Command> =
168            network_config.relaychain().default_command().cloned();
169
170        if relaychain_default_command.is_none() {
171            relaychain_default_command = network_config.relaychain().command().cloned();
172        }
173
174        let relaychain_default_image: Option<Image> =
175            network_config.relaychain().default_image().cloned();
176
177        let relaychain_default_db_snapshot: Option<AssetLocation> =
178            network_config.relaychain().default_db_snapshot().cloned();
179
180        let default_args: Vec<Arg> = network_config
181            .relaychain()
182            .default_args()
183            .into_iter()
184            .cloned()
185            .collect();
186
187        let mut nodes: Vec<NodeConfig> = network_config
188            .relaychain()
189            .nodes()
190            .into_iter()
191            .cloned()
192            .collect();
193
194        let group_nodes: Vec<GroupNodeConfig> = network_config
195            .relaychain()
196            .group_node_configs()
197            .into_iter()
198            .cloned()
199            .collect();
200
201        if let Some(group) = group_nodes.iter().find(|n| n.count == 0) {
202            return Err(anyhow!(
203                "Group node '{}' must have a count greater than 0.",
204                group.base_config.name()
205            ));
206        }
207
208        let mut parachains: Vec<ParachainConfig> =
209            network_config.parachains().into_iter().cloned().collect();
210
211        // Validation checks for relay
212        TryInto::<Chain>::try_into(network_config.relaychain().chain().as_str())?;
213        if relaychain_default_image.is_some() {
214            TryInto::<Image>::try_into(relaychain_default_image.clone().expect(VALIDATION_CHECK))?;
215        }
216        if relaychain_default_command.is_some() {
217            TryInto::<Command>::try_into(
218                relaychain_default_command.clone().expect(VALIDATION_CHECK),
219            )?;
220        }
221
222        // Keep track of node names to ensure uniqueness
223        let mut names = HashSet::new();
224
225        for node in nodes.iter_mut() {
226            if relaychain_default_command.is_some() {
227                // we modify only nodes which don't already have a command
228                if node.command.is_none() {
229                    node.command.clone_from(&relaychain_default_command);
230                }
231            }
232
233            if relaychain_default_image.is_some() && node.image.is_none() {
234                node.image.clone_from(&relaychain_default_image);
235            }
236
237            if relaychain_default_db_snapshot.is_some() && node.db_snapshot.is_none() {
238                node.db_snapshot.clone_from(&relaychain_default_db_snapshot);
239            }
240
241            if !default_args.is_empty() && node.args().is_empty() {
242                node.set_args(default_args.clone());
243            }
244
245            let unique_name = generate_unique_node_name_from_names(node.name(), &mut names);
246            node.name = unique_name;
247        }
248
249        for para in parachains.iter_mut() {
250            // retrieve the defaults parachain for assigning to collators if needed
251            let parachain_default_command: Option<Command> = para.default_command().cloned();
252
253            let parachain_default_image: Option<Image> = para.default_image().cloned();
254
255            let parachain_default_db_snapshot: Option<AssetLocation> =
256                para.default_db_snapshot().cloned();
257
258            let default_args: Vec<Arg> = para.default_args().into_iter().cloned().collect();
259
260            let group_collators: Vec<GroupNodeConfig> = para
261                .group_collators_configs()
262                .into_iter()
263                .cloned()
264                .collect();
265
266            if let Some(group) = group_collators.iter().find(|n| n.count == 0) {
267                return Err(anyhow!(
268                    "Group node '{}' must have a count greater than 0.",
269                    group.base_config.name()
270                ));
271            }
272
273            let mut collators: Vec<NodeConfig> = para.collators.clone();
274
275            for collator in collators.iter_mut() {
276                populate_collator_with_defaults(
277                    collator,
278                    &parachain_default_command,
279                    &parachain_default_image,
280                    &parachain_default_db_snapshot,
281                    &default_args,
282                );
283                let unique_name = generate_unique_node_name_from_names(collator.name(), &mut names);
284                collator.name = unique_name;
285            }
286
287            para.collators = collators;
288
289            if para.collator.is_some() {
290                let mut collator = para.collator.clone().unwrap();
291                populate_collator_with_defaults(
292                    &mut collator,
293                    &parachain_default_command,
294                    &parachain_default_image,
295                    &parachain_default_db_snapshot,
296                    &default_args,
297                );
298                let unique_name = generate_unique_node_name_from_names(collator.name(), &mut names);
299                collator.name = unique_name;
300                para.collator = Some(collator);
301            }
302        }
303
304        network_config
305            .relaychain
306            .as_mut()
307            .expect(&format!("{NO_ERR_DEF_BUILDER}, {THIS_IS_A_BUG}"))
308            .set_nodes(nodes);
309
310        network_config.set_parachains(parachains);
311
312        // Validation checks for parachains
313        network_config.parachains().iter().for_each(|parachain| {
314            if parachain.default_image().is_some() {
315                let _ = TryInto::<Image>::try_into(parachain.default_image().unwrap().as_str());
316            }
317            if parachain.default_command().is_some() {
318                let _ = TryInto::<Command>::try_into(parachain.default_command().unwrap().as_str());
319            }
320        });
321        Ok(network_config)
322    }
323}
324
325fn maybe_absolute(cmd: &str, base_dir: &Path) -> String {
326    if cmd.starts_with(".") {
327        let mut full = PathBuf::from(base_dir);
328        full.push(cmd);
329        trace!("full_path: {:?}", full);
330        std::path::absolute(&full)
331            .expect(&format!(
332                "absolute path shoud be valid: {:?}",
333                full.as_os_str()
334            ))
335            .to_string_lossy()
336            .to_string()
337    } else {
338        cmd.to_string()
339    }
340}
341
342fn populate_collator_with_defaults(
343    collator: &mut NodeConfig,
344    parachain_default_command: &Option<Command>,
345    parachain_default_image: &Option<Image>,
346    parachain_default_db_snapshot: &Option<AssetLocation>,
347    default_args: &[Arg],
348) {
349    if parachain_default_command.is_some() {
350        // we modify only nodes which don't already have a command
351        if collator.command.is_none() {
352            collator.command.clone_from(parachain_default_command);
353        }
354    }
355
356    if parachain_default_image.is_some() && collator.image.is_none() {
357        collator.image.clone_from(parachain_default_image);
358    }
359
360    if parachain_default_db_snapshot.is_some() && collator.db_snapshot.is_none() {
361        collator
362            .db_snapshot
363            .clone_from(parachain_default_db_snapshot);
364    }
365
366    if !default_args.is_empty() && collator.args().is_empty() {
367        collator.set_args(default_args.to_owned());
368    }
369}
370
371states! {
372    Initial,
373    WithRelaychain
374}
375
376/// A network configuration builder, used to build a [`NetworkConfig`] declaratively with fields validation.
377///
378/// # Example:
379///
380/// ```
381/// use zombienet_configuration::NetworkConfigBuilder;
382///
383/// let network_config = NetworkConfigBuilder::new()
384///     .with_relaychain(|relaychain| {
385///         relaychain
386///             .with_chain("polkadot")
387///             .with_random_nominators_count(10)
388///             .with_default_resources(|resources| {
389///                 resources
390///                     .with_limit_cpu("1000m")
391///                     .with_request_memory("1Gi")
392///                     .with_request_cpu(100_000)
393///             })
394///             .with_validator(|node| node.with_name("node").with_command("command"))
395///             .with_validator(|node| node.with_name("node1").with_command("command"))
396///     })
397///     .with_parachain(|parachain| {
398///         parachain
399///             .with_id(1000)
400///             .with_chain("myparachain1")
401///             .with_initial_balance(100_000)
402///             .with_default_image("myimage:version")
403///             .with_collator(|collator| {
404///                 collator
405///                     .with_name("collator1")
406///                     .with_command("command1")
407///                     .validator(true)
408///             })
409///     })
410///     .with_parachain(|parachain| {
411///         parachain
412///             .with_id(2000)
413///             .with_chain("myparachain2")
414///             .with_initial_balance(50_0000)
415///             .with_collator(|collator| {
416///                 collator
417///                     .with_name("collator2")
418///                     .with_command("command2")
419///                     .validator(true)
420///             })
421///     })
422///     .with_hrmp_channel(|hrmp_channel1| {
423///         hrmp_channel1
424///             .with_sender(1)
425///             .with_recipient(2)
426///             .with_max_capacity(200)
427///             .with_max_message_size(500)
428///     })
429///     .with_hrmp_channel(|hrmp_channel2| {
430///         hrmp_channel2
431///             .with_sender(2)
432///             .with_recipient(1)
433///             .with_max_capacity(100)
434///             .with_max_message_size(250)
435///     })
436///     .with_global_settings(|global_settings| {
437///         global_settings
438///             .with_network_spawn_timeout(1200)
439///             .with_node_spawn_timeout(240)
440///     })
441///     .build();
442///
443/// assert!(network_config.is_ok())
444/// ```
445pub struct NetworkConfigBuilder<State> {
446    config: NetworkConfig,
447    validation_context: Rc<RefCell<ValidationContext>>,
448    errors: Vec<anyhow::Error>,
449    _state: PhantomData<State>,
450}
451
452impl Default for NetworkConfigBuilder<Initial> {
453    fn default() -> Self {
454        Self {
455            config: NetworkConfig {
456                global_settings: GlobalSettingsBuilder::new()
457                    .build()
458                    .expect(&format!("{NO_ERR_DEF_BUILDER}, {THIS_IS_A_BUG}")),
459                relaychain: None,
460                parachains: vec![],
461                hrmp_channels: vec![],
462                custom_processes: vec![],
463            },
464            validation_context: Default::default(),
465            errors: vec![],
466            _state: PhantomData,
467        }
468    }
469}
470
471impl<A> NetworkConfigBuilder<A> {
472    fn transition<B>(
473        config: NetworkConfig,
474        validation_context: Rc<RefCell<ValidationContext>>,
475        errors: Vec<anyhow::Error>,
476    ) -> NetworkConfigBuilder<B> {
477        NetworkConfigBuilder {
478            config,
479            errors,
480            validation_context,
481            _state: PhantomData,
482        }
483    }
484}
485
486impl NetworkConfigBuilder<Initial> {
487    pub fn new() -> NetworkConfigBuilder<Initial> {
488        Self::default()
489    }
490
491    /// uses the default options for both the relay chain and the validator nodes
492    /// the only required fields are the name of the validator nodes,
493    /// and the name of the relay chain ("rococo-local", "polkadot", etc.)
494    pub fn with_chain_and_nodes(
495        relay_name: &str,
496        node_names: Vec<String>,
497    ) -> NetworkConfigBuilder<WithRelaychain> {
498        let network_config = NetworkConfigBuilder::new().with_relaychain(|relaychain| {
499            let mut relaychain_with_node =
500                relaychain.with_chain(relay_name).with_validator(|node| {
501                    node.with_name(node_names.first().unwrap_or(&"".to_string()))
502                });
503
504            for node_name in node_names.iter().skip(1) {
505                relaychain_with_node = relaychain_with_node
506                    .with_validator(|node_builder| node_builder.with_name(node_name));
507            }
508            relaychain_with_node
509        });
510
511        Self::transition(
512            network_config.config,
513            network_config.validation_context,
514            network_config.errors,
515        )
516    }
517
518    /// Set the relay chain using a nested [`RelaychainConfigBuilder`].
519    pub fn with_relaychain(
520        self,
521        f: impl FnOnce(
522            RelaychainConfigBuilder<relaychain::Initial>,
523        ) -> RelaychainConfigBuilder<relaychain::WithAtLeastOneNode>,
524    ) -> NetworkConfigBuilder<WithRelaychain> {
525        match f(RelaychainConfigBuilder::new(
526            self.validation_context.clone(),
527        ))
528        .build()
529        {
530            Ok(relaychain) => Self::transition(
531                NetworkConfig {
532                    relaychain: Some(relaychain),
533                    ..self.config
534                },
535                self.validation_context,
536                self.errors,
537            ),
538            Err(errors) => Self::transition(self.config, self.validation_context, errors),
539        }
540    }
541}
542
543impl NetworkConfigBuilder<WithRelaychain> {
544    /// Set the global settings using a nested [`GlobalSettingsBuilder`].
545    pub fn with_global_settings(
546        self,
547        f: impl FnOnce(GlobalSettingsBuilder) -> GlobalSettingsBuilder,
548    ) -> Self {
549        match f(GlobalSettingsBuilder::new()).build() {
550            Ok(global_settings) => Self::transition(
551                NetworkConfig {
552                    global_settings,
553                    ..self.config
554                },
555                self.validation_context,
556                self.errors,
557            ),
558            Err(errors) => Self::transition(
559                self.config,
560                self.validation_context,
561                merge_errors_vecs(self.errors, errors),
562            ),
563        }
564    }
565
566    /// Add a parachain using a nested [`ParachainConfigBuilder`].
567    pub fn with_parachain(
568        self,
569        f: impl FnOnce(
570            ParachainConfigBuilder<parachain::states::Initial, parachain::states::Bootstrap>,
571        ) -> ParachainConfigBuilder<
572            parachain::states::WithAtLeastOneCollator,
573            parachain::states::Bootstrap,
574        >,
575    ) -> Self {
576        match f(ParachainConfigBuilder::new(self.validation_context.clone())).build() {
577            Ok(parachain) => Self::transition(
578                NetworkConfig {
579                    parachains: [self.config.parachains, vec![parachain]].concat(),
580                    ..self.config
581                },
582                self.validation_context,
583                self.errors,
584            ),
585            Err(errors) => Self::transition(
586                self.config,
587                self.validation_context,
588                merge_errors_vecs(self.errors, errors),
589            ),
590        }
591    }
592
593    /// uses default settings for setting for:
594    /// - the parachain,
595    /// - the global settings
596    /// - the hrmp channels
597    ///
598    /// the only required parameters are the names of the collators as a vector,
599    /// and the id of the parachain
600    pub fn with_parachain_id_and_collators(self, id: u32, collator_names: Vec<String>) -> Self {
601        if collator_names.is_empty() {
602            return Self::transition(
603                self.config,
604                self.validation_context,
605                merge_errors(
606                    self.errors,
607                    ConfigError::Parachain(id, ValidationError::CantBeEmpty().into()).into(),
608                ),
609            );
610        }
611
612        self.with_parachain(|parachain| {
613            let mut parachain_config = parachain.with_id(id).with_collator(|collator| {
614                collator
615                    .with_name(collator_names.first().unwrap_or(&"".to_string()))
616                    .validator(true)
617            });
618
619            for collator_name in collator_names.iter().skip(1) {
620                parachain_config = parachain_config
621                    .with_collator(|collator| collator.with_name(collator_name).validator(true));
622            }
623            parachain_config
624        })
625
626        // TODO: if need to set global settings and hrmp channels
627        // we can also do in here
628    }
629
630    /// Add an HRMP channel using a nested [`HrmpChannelConfigBuilder`].
631    pub fn with_hrmp_channel(
632        self,
633        f: impl FnOnce(
634            HrmpChannelConfigBuilder<hrmp_channel::Initial>,
635        ) -> HrmpChannelConfigBuilder<hrmp_channel::WithRecipient>,
636    ) -> Self {
637        let new_hrmp_channel = f(HrmpChannelConfigBuilder::new()).build();
638
639        Self::transition(
640            NetworkConfig {
641                hrmp_channels: [self.config.hrmp_channels, vec![new_hrmp_channel]].concat(),
642                ..self.config
643            },
644            self.validation_context,
645            self.errors,
646        )
647    }
648
649    /// Add a custom process using a nested [`CustomProcessBuilder`].
650    pub fn with_custom_process(
651        self,
652        f: impl FnOnce(
653            CustomProcessBuilder<custom_process::WithOutName, custom_process::WithOutCmd>,
654        )
655            -> CustomProcessBuilder<custom_process::WithName, custom_process::WithCmd>,
656    ) -> Self {
657        match f(CustomProcessBuilder::new()).build() {
658            Ok(custom_process) => Self::transition(
659                NetworkConfig {
660                    custom_processes: [self.config.custom_processes, vec![custom_process]].concat(),
661                    ..self.config
662                },
663                self.validation_context,
664                self.errors,
665            ),
666            Err((name, errors)) => Self::transition(
667                self.config,
668                self.validation_context,
669                merge_errors_vecs(
670                    self.errors,
671                    errors
672                        .into_iter()
673                        .map(|error| ConfigError::Node(name.clone(), error).into())
674                        .collect::<Vec<_>>(),
675                ),
676            ),
677        }
678    }
679
680    /// Seals the builder and returns a [`NetworkConfig`] if there are no validation errors, else returns errors.
681    pub fn build(self) -> Result<NetworkConfig, Vec<anyhow::Error>> {
682        let mut paras_to_register: HashSet<ParaId> = Default::default();
683        let mut num_of_requested_cores = 0;
684
685        let mut errs: Vec<anyhow::Error> = self
686            .config
687            .parachains
688            .iter()
689            .filter_map(|para| {
690                if let Some(cores) = para.num_cores() {
691                    num_of_requested_cores += cores;
692                } else if let Some(RegistrationStrategy::InGenesis) = para.registration_strategy() {
693                    // add 1 by default
694                    num_of_requested_cores += 1;
695                }
696
697                if let Some(RegistrationStrategy::Manual) = para.registration_strategy() {
698                    return None;
699                };
700
701                if paras_to_register.insert(para.id()) {
702                    None
703                } else {
704                    // already in the set
705                    Some(anyhow!(
706                        "ParaId {} already set to be registered, only one should be.",
707                        para.id()
708                    ))
709                }
710            })
711            .collect();
712
713        // ensure we can make this check
714        if self.config.relaychain.is_some() {
715            // check we have num_validators >= num_requested_cores
716            let rc_config = self.config.relaychain();
717            let mut num_validators = rc_config.nodes().iter().fold(0u32, |mut acc, node| {
718                if node.is_validator {
719                    acc += 1;
720                }
721
722                acc
723            });
724
725            let node_groups = rc_config.group_node_configs();
726            for ng in &node_groups {
727                if ng.base_config.is_validator {
728                    num_validators += ng.count as u32;
729                }
730            }
731
732            if num_validators < num_of_requested_cores {
733                errs.push(anyhow!("Number of assigned cores at genesis {num_of_requested_cores} is greater than validators ({num_validators})"));
734            }
735        }
736
737        if !self.errors.is_empty() || !errs.is_empty() {
738            let mut ret_errs = self.errors;
739            ret_errs.append(&mut errs);
740            return Err(ret_errs);
741        }
742
743        Ok(self.config)
744    }
745}
746
747#[cfg(test)]
748mod tests {
749    use std::path::PathBuf;
750
751    use super::*;
752    use crate::parachain::RegistrationStrategy;
753
754    #[test]
755    fn network_config_builder_should_succeeds_and_returns_a_network_config() {
756        let network_config = NetworkConfigBuilder::new()
757            .with_relaychain(|relaychain| {
758                relaychain
759                    .with_chain("polkadot")
760                    .with_random_nominators_count(10)
761                    .with_validator(|node| node.with_name("node").with_command("command"))
762                    .with_validator(|node| node.with_name("node-1").with_command("command"))
763            })
764            .with_parachain(|parachain| {
765                parachain
766                    .with_id(1)
767                    .with_chain("myparachain1")
768                    .with_initial_balance(100_000)
769                    .with_collator(|collator| {
770                        collator
771                            .with_name("collator1")
772                            .with_command("command1")
773                            .validator(true)
774                    })
775            })
776            .with_parachain(|parachain| {
777                parachain
778                    .with_id(2)
779                    .with_chain("myparachain2")
780                    .with_initial_balance(0)
781                    .with_collator(|collator| {
782                        collator
783                            .with_name("collator2")
784                            .with_command("command2")
785                            .validator(true)
786                    })
787            })
788            .with_hrmp_channel(|hrmp_channel1| {
789                hrmp_channel1
790                    .with_sender(1)
791                    .with_recipient(2)
792                    .with_max_capacity(200)
793                    .with_max_message_size(500)
794            })
795            .with_hrmp_channel(|hrmp_channel2| {
796                hrmp_channel2
797                    .with_sender(2)
798                    .with_recipient(1)
799                    .with_max_capacity(100)
800                    .with_max_message_size(250)
801            })
802            .with_global_settings(|global_settings| {
803                global_settings
804                    .with_network_spawn_timeout(1200)
805                    .with_node_spawn_timeout(240)
806            })
807            .build()
808            .unwrap();
809
810        // relaychain
811        assert_eq!(network_config.relaychain().chain().as_str(), "polkadot");
812        assert_eq!(network_config.relaychain().nodes().len(), 2);
813        let &node = network_config.relaychain().nodes().first().unwrap();
814        assert_eq!(node.name(), "node");
815        assert_eq!(node.command().unwrap().as_str(), "command");
816        assert!(node.is_validator());
817        assert_eq!(
818            network_config
819                .relaychain()
820                .random_nominators_count()
821                .unwrap(),
822            10
823        );
824
825        // parachains
826        assert_eq!(network_config.parachains().len(), 2);
827
828        // parachain1
829        let &parachain1 = network_config.parachains().first().unwrap();
830        assert_eq!(parachain1.id(), 1);
831        assert_eq!(parachain1.collators().len(), 1);
832        let &collator = parachain1.collators().first().unwrap();
833        assert_eq!(collator.name(), "collator1");
834        assert_eq!(collator.command().unwrap().as_str(), "command1");
835        assert!(collator.is_validator());
836        assert_eq!(parachain1.initial_balance(), 100_000);
837        assert_eq!(parachain1.unique_id(), "1");
838
839        // parachain2
840        let &parachain2 = network_config.parachains().last().unwrap();
841        assert_eq!(parachain2.id(), 2);
842        assert_eq!(parachain2.collators().len(), 1);
843        let &collator = parachain2.collators().first().unwrap();
844        assert_eq!(collator.name(), "collator2");
845        assert_eq!(collator.command().unwrap().as_str(), "command2");
846        assert!(collator.is_validator());
847        assert_eq!(parachain2.initial_balance(), 0);
848
849        // hrmp_channels
850        assert_eq!(network_config.hrmp_channels().len(), 2);
851
852        // hrmp_channel1
853        let &hrmp_channel1 = network_config.hrmp_channels().first().unwrap();
854        assert_eq!(hrmp_channel1.sender(), 1);
855        assert_eq!(hrmp_channel1.recipient(), 2);
856        assert_eq!(hrmp_channel1.max_capacity(), 200);
857        assert_eq!(hrmp_channel1.max_message_size(), 500);
858
859        // hrmp_channel2
860        let &hrmp_channel2 = network_config.hrmp_channels().last().unwrap();
861        assert_eq!(hrmp_channel2.sender(), 2);
862        assert_eq!(hrmp_channel2.recipient(), 1);
863        assert_eq!(hrmp_channel2.max_capacity(), 100);
864        assert_eq!(hrmp_channel2.max_message_size(), 250);
865
866        // global settings
867        assert_eq!(
868            network_config.global_settings().network_spawn_timeout(),
869            1200
870        );
871        assert_eq!(network_config.global_settings().node_spawn_timeout(), 240);
872        assert!(network_config.global_settings().tear_down_on_failure());
873    }
874
875    #[test]
876    fn network_config_builder_should_fails_and_returns_multiple_errors_if_relaychain_is_invalid() {
877        let errors = NetworkConfigBuilder::new()
878            .with_relaychain(|relaychain| {
879                relaychain
880                    .with_chain("polkadot")
881                    .with_random_nominators_count(10)
882                    .with_default_image("invalid.image")
883                    .with_validator(|node| node.with_name("node").with_command("invalid command"))
884            })
885            .with_parachain(|parachain| {
886                parachain
887                    .with_id(1)
888                    .with_chain("myparachain")
889                    .with_initial_balance(100_000)
890                    .with_collator(|collator| {
891                        collator
892                            .with_name("collator1")
893                            .with_command("command1")
894                            .validator(true)
895                    })
896            })
897            .build()
898            .unwrap_err();
899
900        assert_eq!(errors.len(), 2);
901        assert_eq!(
902            errors.first().unwrap().to_string(),
903            "relaychain.default_image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
904        );
905        assert_eq!(
906            errors.get(1).unwrap().to_string(),
907            "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
908        );
909    }
910
911    #[test]
912    fn network_config_builder_should_fails_and_returns_multiple_errors_if_parachain_is_invalid() {
913        let errors = NetworkConfigBuilder::new()
914            .with_relaychain(|relaychain| {
915                relaychain
916                    .with_chain("polkadot")
917                    .with_random_nominators_count(10)
918                    .with_validator(|node| node.with_name("node").with_command("command"))
919            })
920            .with_parachain(|parachain| {
921                parachain
922                    .with_id(1000)
923                    .with_chain("myparachain")
924                    .with_initial_balance(100_000)
925                    .with_collator(|collator| {
926                        collator
927                            .with_name("collator1")
928                            .with_command("invalid command")
929                            .with_image("invalid.image")
930                            .validator(true)
931                    })
932            })
933            .build()
934            .unwrap_err();
935
936        assert_eq!(errors.len(), 2);
937        assert_eq!(
938            errors.first().unwrap().to_string(),
939            "parachain[1000].collators['collator1'].command: 'invalid command' shouldn't contains whitespace"
940        );
941        assert_eq!(
942            errors.get(1).unwrap().to_string(),
943            "parachain[1000].collators['collator1'].image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
944        );
945    }
946
947    #[test]
948    fn network_config_builder_should_fails_and_returns_multiple_errors_if_multiple_parachains_are_invalid(
949    ) {
950        let errors = NetworkConfigBuilder::new()
951            .with_relaychain(|relaychain| {
952                relaychain
953                    .with_chain("polkadot")
954                    .with_random_nominators_count(10)
955                    .with_validator(|node| node.with_name("node").with_command("command"))
956            })
957            .with_parachain(|parachain| {
958                parachain
959                    .with_id(1000)
960                    .with_chain("myparachain1")
961                    .with_initial_balance(100_000)
962                    .with_collator(|collator| {
963                        collator
964                            .with_name("collator1")
965                            .with_command("invalid command")
966                    })
967            })
968            .with_parachain(|parachain| {
969                parachain
970                    .with_id(2000)
971                    .with_chain("myparachain2")
972                    .with_initial_balance(100_000)
973                    .with_collator(|collator| {
974                        collator
975                            .with_name("collator2")
976                            .validator(true)
977                            .with_resources(|resources| {
978                                resources
979                                    .with_limit_cpu("1000m")
980                                    .with_request_memory("1Gi")
981                                    .with_request_cpu("invalid")
982                            })
983                    })
984            })
985            .build()
986            .unwrap_err();
987
988        assert_eq!(errors.len(), 2);
989        assert_eq!(
990            errors.first().unwrap().to_string(),
991            "parachain[1000].collators['collator1'].command: 'invalid command' shouldn't contains whitespace"
992        );
993        assert_eq!(
994            errors.get(1).unwrap().to_string(),
995            "parachain[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)?$'"
996        );
997    }
998
999    #[test]
1000    fn network_config_builder_should_fails_and_returns_multiple_errors_if_global_settings_is_invalid(
1001    ) {
1002        let errors = NetworkConfigBuilder::new()
1003            .with_relaychain(|relaychain| {
1004                relaychain
1005                    .with_chain("polkadot")
1006                    .with_random_nominators_count(10)
1007                    .with_validator(|node| node.with_name("node").with_command("command"))
1008            })
1009            .with_parachain(|parachain| {
1010                parachain
1011                    .with_id(1000)
1012                    .with_chain("myparachain")
1013                    .with_initial_balance(100_000)
1014                    .with_collator(|collator| {
1015                        collator
1016                            .with_name("collator")
1017                            .with_command("command")
1018                            .validator(true)
1019                    })
1020            })
1021            .with_global_settings(|global_settings| {
1022                global_settings
1023                    .with_local_ip("127.0.0000.1")
1024                    .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
1025            })
1026            .build()
1027            .unwrap_err();
1028
1029        assert_eq!(errors.len(), 2);
1030        assert_eq!(
1031            errors.first().unwrap().to_string(),
1032            "global_settings.local_ip: invalid IP address syntax"
1033        );
1034        assert_eq!(
1035            errors.get(1).unwrap().to_string(),
1036            "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1037        );
1038    }
1039
1040    #[test]
1041    fn network_config_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid(
1042    ) {
1043        let errors = NetworkConfigBuilder::new()
1044            .with_relaychain(|relaychain| {
1045                relaychain
1046                    .with_chain("polkadot")
1047                    .with_random_nominators_count(10)
1048                    .with_validator(|node| node.with_name("node").with_command("invalid command"))
1049            })
1050            .with_parachain(|parachain| {
1051                parachain
1052                    .with_id(1000)
1053                    .with_chain("myparachain")
1054                    .with_initial_balance(100_000)
1055                    .with_collator(|collator| {
1056                        collator
1057                            .with_name("collator")
1058                            .with_command("command")
1059                            .with_image("invalid.image")
1060                    })
1061            })
1062            .with_global_settings(|global_settings| global_settings.with_local_ip("127.0.0000.1"))
1063            .build()
1064            .unwrap_err();
1065
1066        assert_eq!(errors.len(), 3);
1067        assert_eq!(
1068            errors.first().unwrap().to_string(),
1069            "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
1070        );
1071        assert_eq!(
1072            errors.get(1).unwrap().to_string(),
1073            "parachain[1000].collators['collator'].image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1074        );
1075        assert_eq!(
1076            errors.get(2).unwrap().to_string(),
1077            "global_settings.local_ip: invalid IP address syntax"
1078        );
1079    }
1080
1081    #[test]
1082    fn network_config_should_be_dumpable_to_a_toml_config_for_a_small_network() {
1083        let network_config = NetworkConfigBuilder::new()
1084            .with_relaychain(|relaychain| {
1085                relaychain
1086                    .with_chain("rococo-local")
1087                    .with_default_command("polkadot")
1088                    .with_default_image("docker.io/parity/polkadot:latest")
1089                    .with_default_args(vec![("-lparachain", "debug").into()])
1090                    .with_validator(|node| node.with_name("alice"))
1091                    .with_validator(|node| {
1092                        node.with_name("bob")
1093                            .invulnerable(false)
1094                            .bootnode(true)
1095                            .with_args(vec![("--database", "paritydb-experimental").into()])
1096                    })
1097            })
1098            .build()
1099            .unwrap();
1100
1101        let got = network_config.dump_to_toml().unwrap();
1102        let expected = fs::read_to_string("./testing/snapshots/0000-small-network.toml").unwrap();
1103        assert_eq!(got, expected);
1104    }
1105
1106    #[test]
1107    fn network_config_should_be_dumpable_to_a_toml_config_for_a_big_network() {
1108        let network_config = NetworkConfigBuilder::new()
1109            .with_relaychain(|relaychain| {
1110                relaychain
1111                    .with_chain("polkadot")
1112                    .with_default_command("polkadot")
1113                    .with_default_image("docker.io/parity/polkadot:latest")
1114                    .with_default_resources(|resources| {
1115                        resources
1116                            .with_request_cpu(100000)
1117                            .with_request_memory("500M")
1118                            .with_limit_cpu("10Gi")
1119                            .with_limit_memory("4000M")
1120                    })
1121                    .with_validator(|node| {
1122                        node.with_name("alice")
1123                            .with_initial_balance(1_000_000_000)
1124                            .bootnode(true)
1125                            .invulnerable(true)
1126                    })
1127                    .with_validator(|node| node.with_name("bob").invulnerable(true).bootnode(true))
1128            })
1129            .with_parachain(|parachain| {
1130                parachain
1131                    .with_id(1000)
1132                    .with_chain("myparachain")
1133                    .with_chain_spec_path("/path/to/my/chain/spec.json")
1134                    .with_registration_strategy(RegistrationStrategy::UsingExtrinsic)
1135                    .onboard_as_parachain(false)
1136                    .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz")
1137                    .with_collator(|collator| {
1138                        collator
1139                            .with_name("john")
1140                            .bootnode(true)
1141                            .invulnerable(true)
1142                            .with_initial_balance(5_000_000_000)
1143                    })
1144                    .with_fullnode(|collator| {
1145                        collator
1146                            .with_name("charles")
1147                            .bootnode(true)
1148                            .invulnerable(true)
1149                            .with_initial_balance(0)
1150                    })
1151                    .with_collator(|collator| {
1152                        collator
1153                            .with_name("frank")
1154                            .invulnerable(false)
1155                            .bootnode(true)
1156                            .with_initial_balance(1_000_000_000)
1157                    })
1158            })
1159            .with_parachain(|parachain| {
1160                parachain
1161                    .with_id(2000)
1162                    .with_chain("myotherparachain")
1163                    .with_chain_spec_path("/path/to/my/other/chain/spec.json")
1164                    .with_collator(|collator| {
1165                        collator
1166                            .with_name("mike")
1167                            .bootnode(true)
1168                            .invulnerable(true)
1169                            .with_initial_balance(5_000_000_000)
1170                    })
1171                    .with_fullnode(|collator| {
1172                        collator
1173                            .with_name("georges")
1174                            .bootnode(true)
1175                            .invulnerable(true)
1176                            .with_initial_balance(0)
1177                    })
1178                    .with_collator(|collator| {
1179                        collator
1180                            .with_name("victor")
1181                            .invulnerable(false)
1182                            .bootnode(true)
1183                            .with_initial_balance(1_000_000_000)
1184                    })
1185            })
1186            .with_hrmp_channel(|hrmp_channel| {
1187                hrmp_channel
1188                    .with_sender(1000)
1189                    .with_recipient(2000)
1190                    .with_max_capacity(150)
1191                    .with_max_message_size(5000)
1192            })
1193            .with_hrmp_channel(|hrmp_channel| {
1194                hrmp_channel
1195                    .with_sender(2000)
1196                    .with_recipient(1000)
1197                    .with_max_capacity(200)
1198                    .with_max_message_size(8000)
1199            })
1200            .with_custom_process(|c| c.with_name("eth-rpc").with_command("eth-rpc"))
1201            .build()
1202            .unwrap();
1203
1204        let got = network_config.dump_to_toml().unwrap();
1205        let expected = fs::read_to_string("./testing/snapshots/0001-big-network.toml").unwrap();
1206        assert_eq!(got, expected);
1207    }
1208
1209    #[test]
1210    fn network_config_builder_should_be_dumplable_to_a_toml_config_a_overrides_default_correctly() {
1211        let network_config = NetworkConfigBuilder::new()
1212            .with_relaychain(|relaychain| {
1213                relaychain
1214                    .with_chain("polkadot")
1215                    .with_default_command("polkadot")
1216                    .with_default_image("docker.io/parity/polkadot:latest")
1217                    .with_default_args(vec![("-name", "value").into(), "--flag".into()])
1218                    .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz")
1219                    .with_default_resources(|resources| {
1220                        resources
1221                            .with_request_cpu(100000)
1222                            .with_request_memory("500M")
1223                            .with_limit_cpu("10Gi")
1224                            .with_limit_memory("4000M")
1225                    })
1226                    .with_validator(|node| {
1227                        node.with_name("alice")
1228                            .with_initial_balance(1_000_000_000)
1229                            .bootnode(true)
1230                            .invulnerable(true)
1231                    })
1232                    .with_validator(|node| {
1233                        node.with_name("bob")
1234                            .invulnerable(true)
1235                            .bootnode(true)
1236                            .with_image("mycustomimage:latest")
1237                            .with_command("my-custom-command")
1238                            .with_db_snapshot("https://storage.com/path/to/other/db_snapshot.tgz")
1239                            .with_resources(|resources| {
1240                                resources
1241                                    .with_request_cpu(1000)
1242                                    .with_request_memory("250Mi")
1243                                    .with_limit_cpu("5Gi")
1244                                    .with_limit_memory("2Gi")
1245                            })
1246                            .with_args(vec![("-myothername", "value").into()])
1247                    })
1248            })
1249            .with_parachain(|parachain| {
1250                parachain
1251                    .with_id(1000)
1252                    .with_chain("myparachain")
1253                    .with_chain_spec_path("/path/to/my/chain/spec.json")
1254                    .with_default_db_snapshot("https://storage.com/path/to/other_snapshot.tgz")
1255                    .with_default_command("my-default-command")
1256                    .with_default_image("mydefaultimage:latest")
1257                    .with_collator(|collator| {
1258                        collator
1259                            .with_name("john")
1260                            .bootnode(true)
1261                            .invulnerable(true)
1262                            .with_initial_balance(5_000_000_000)
1263                            .with_command("my-non-default-command")
1264                            .with_image("anotherimage:latest")
1265                    })
1266                    .with_fullnode(|collator| {
1267                        collator
1268                            .with_name("charles")
1269                            .bootnode(true)
1270                            .invulnerable(true)
1271                            .with_initial_balance(0)
1272                    })
1273            })
1274            .build()
1275            .unwrap();
1276
1277        let got = network_config.dump_to_toml().unwrap();
1278        let expected =
1279            fs::read_to_string("./testing/snapshots/0002-overridden-defaults.toml").unwrap();
1280        assert_eq!(got, expected);
1281    }
1282
1283    #[test]
1284    fn the_toml_config_with_custom_settings() {
1285        let settings = GlobalSettingsBuilder::new()
1286            .with_base_dir("/tmp/test-demo")
1287            .build()
1288            .unwrap();
1289
1290        let load_from_toml = NetworkConfig::load_from_toml_with_settings(
1291            "./testing/snapshots/0000-small-network.toml",
1292            &settings,
1293        )
1294        .unwrap();
1295
1296        assert_eq!(
1297            Some(PathBuf::from("/tmp/test-demo").as_path()),
1298            load_from_toml.global_settings.base_dir()
1299        );
1300    }
1301
1302    #[test]
1303    fn the_toml_config_should_be_imported_and_match_a_network() {
1304        let load_from_toml =
1305            NetworkConfig::load_from_toml("./testing/snapshots/0000-small-network.toml").unwrap();
1306
1307        let expected = NetworkConfigBuilder::new()
1308            .with_relaychain(|relaychain| {
1309                relaychain
1310                    .with_chain("rococo-local")
1311                    .with_default_command("polkadot")
1312                    .with_default_image("docker.io/parity/polkadot:latest")
1313                    .with_default_args(vec![("-lparachain=debug").into()])
1314                    .with_validator(|node| {
1315                        node.with_name("alice")
1316                            .validator(true)
1317                            .invulnerable(true)
1318                            .bootnode(false)
1319                            .with_initial_balance(2000000000000)
1320                    })
1321                    .with_validator(|node| {
1322                        node.with_name("bob")
1323                            .with_args(vec![("--database", "paritydb-experimental").into()])
1324                            .invulnerable(false)
1325                            .bootnode(true)
1326                            .with_initial_balance(2000000000000)
1327                    })
1328            })
1329            .build()
1330            .unwrap();
1331
1332        // We need to assert parts of the network config separately because the expected one contains the chain default context which
1333        // is used for dumbing to tomp while the
1334        // while loaded
1335        assert_eq!(
1336            expected.relaychain().chain(),
1337            load_from_toml.relaychain().chain()
1338        );
1339        assert_eq!(
1340            expected.relaychain().default_args(),
1341            load_from_toml.relaychain().default_args()
1342        );
1343        assert_eq!(
1344            expected.relaychain().default_command(),
1345            load_from_toml.relaychain().default_command()
1346        );
1347        assert_eq!(
1348            expected.relaychain().default_image(),
1349            load_from_toml.relaychain().default_image()
1350        );
1351
1352        // Check the nodes without the Chain Default Context
1353        expected
1354            .relaychain()
1355            .nodes()
1356            .iter()
1357            .zip(load_from_toml.relaychain().nodes().iter())
1358            .for_each(|(expected_node, loaded_node)| {
1359                assert_eq!(expected_node.name(), loaded_node.name());
1360                assert_eq!(expected_node.command(), loaded_node.command());
1361                assert_eq!(expected_node.args(), loaded_node.args());
1362                assert_eq!(
1363                    expected_node.is_invulnerable(),
1364                    loaded_node.is_invulnerable()
1365                );
1366                assert_eq!(expected_node.is_validator(), loaded_node.is_validator());
1367                assert_eq!(expected_node.is_bootnode(), loaded_node.is_bootnode());
1368                assert_eq!(
1369                    expected_node.initial_balance(),
1370                    loaded_node.initial_balance()
1371                );
1372            });
1373    }
1374
1375    #[test]
1376    fn the_toml_config_without_settings_should_be_imported_and_match_a_network() {
1377        let load_from_toml = NetworkConfig::load_from_toml(
1378            "./testing/snapshots/0004-small-network-without-settings.toml",
1379        )
1380        .unwrap();
1381
1382        let expected = NetworkConfigBuilder::new()
1383            .with_relaychain(|relaychain| {
1384                relaychain
1385                    .with_chain("rococo-local")
1386                    .with_default_command("polkadot")
1387                    .with_validator(|node| node.with_name("alice"))
1388                    .with_validator(|node| node.with_name("bob"))
1389            })
1390            .build()
1391            .unwrap();
1392
1393        assert_eq!(
1394            load_from_toml.global_settings().network_spawn_timeout(),
1395            expected.global_settings().network_spawn_timeout()
1396        )
1397    }
1398
1399    #[test]
1400    fn the_toml_config_should_be_imported_and_match_a_network_with_parachains() {
1401        let load_from_toml =
1402            NetworkConfig::load_from_toml("./testing/snapshots/0001-big-network.toml").unwrap();
1403
1404        let expected = NetworkConfigBuilder::new()
1405            .with_relaychain(|relaychain| {
1406                relaychain
1407                    .with_chain("polkadot")
1408                    .with_default_command("polkadot")
1409                    .with_default_image("docker.io/parity/polkadot:latest")
1410                    .with_default_resources(|resources| {
1411                        resources
1412                            .with_request_cpu(100000)
1413                            .with_request_memory("500M")
1414                            .with_limit_cpu("10Gi")
1415                            .with_limit_memory("4000M")
1416                    })
1417                    .with_validator(|node| {
1418                        node.with_name("alice")
1419                            .with_initial_balance(1_000_000_000)
1420                            .bootnode(true)
1421                            .invulnerable(true)
1422                    })
1423                    .with_validator(|node| node.with_name("bob").invulnerable(true).bootnode(true))
1424            })
1425            .with_parachain(|parachain| {
1426                parachain
1427                    .with_id(1000)
1428                    .with_chain("myparachain")
1429                    .with_chain_spec_path("/path/to/my/chain/spec.json")
1430                    .with_registration_strategy(RegistrationStrategy::UsingExtrinsic)
1431                    .onboard_as_parachain(false)
1432                    .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz")
1433                    .with_collator(|collator| {
1434                        collator
1435                            .with_name("john")
1436                            .bootnode(true)
1437                            .invulnerable(true)
1438                            .with_initial_balance(5_000_000_000)
1439                    })
1440                    .with_fullnode(|collator| {
1441                        collator
1442                            .with_name("charles")
1443                            .bootnode(true)
1444                            .invulnerable(true)
1445                            .with_initial_balance(0)
1446                    })
1447                    .with_collator(|collator| {
1448                        collator
1449                            .with_name("frank")
1450                            .invulnerable(false)
1451                            .bootnode(true)
1452                            .with_initial_balance(1_000_000_000)
1453                    })
1454            })
1455            .with_parachain(|parachain| {
1456                parachain
1457                    .with_id(2000)
1458                    .with_chain("myotherparachain")
1459                    .with_chain_spec_path("/path/to/my/other/chain/spec.json")
1460                    .with_collator(|collator| {
1461                        collator
1462                            .with_name("mike")
1463                            .bootnode(true)
1464                            .invulnerable(true)
1465                            .with_initial_balance(5_000_000_000)
1466                    })
1467                    .with_fullnode(|collator| {
1468                        collator
1469                            .with_name("georges")
1470                            .bootnode(true)
1471                            .invulnerable(true)
1472                            .with_initial_balance(0)
1473                    })
1474                    .with_collator(|collator| {
1475                        collator
1476                            .with_name("victor")
1477                            .invulnerable(false)
1478                            .bootnode(true)
1479                            .with_initial_balance(1_000_000_000)
1480                    })
1481            })
1482            .with_hrmp_channel(|hrmp_channel| {
1483                hrmp_channel
1484                    .with_sender(1000)
1485                    .with_recipient(2000)
1486                    .with_max_capacity(150)
1487                    .with_max_message_size(5000)
1488            })
1489            .with_hrmp_channel(|hrmp_channel| {
1490                hrmp_channel
1491                    .with_sender(2000)
1492                    .with_recipient(1000)
1493                    .with_max_capacity(200)
1494                    .with_max_message_size(8000)
1495            })
1496            .build()
1497            .unwrap();
1498
1499        // Check the relay chain
1500        assert_eq!(
1501            expected.relaychain().default_resources(),
1502            load_from_toml.relaychain().default_resources()
1503        );
1504
1505        // Check the nodes without the Chain Default Context
1506        expected
1507            .relaychain()
1508            .nodes()
1509            .iter()
1510            .zip(load_from_toml.relaychain().nodes().iter())
1511            .for_each(|(expected_node, loaded_node)| {
1512                assert_eq!(expected_node.name(), loaded_node.name());
1513                assert_eq!(expected_node.command(), loaded_node.command());
1514                assert_eq!(expected_node.args(), loaded_node.args());
1515                assert_eq!(expected_node.is_validator(), loaded_node.is_validator());
1516                assert_eq!(expected_node.is_bootnode(), loaded_node.is_bootnode());
1517                assert_eq!(
1518                    expected_node.initial_balance(),
1519                    loaded_node.initial_balance()
1520                );
1521                assert_eq!(
1522                    expected_node.is_invulnerable(),
1523                    loaded_node.is_invulnerable()
1524                );
1525            });
1526
1527        expected
1528            .parachains()
1529            .iter()
1530            .zip(load_from_toml.parachains().iter())
1531            .for_each(|(expected_parachain, loaded_parachain)| {
1532                assert_eq!(expected_parachain.id(), loaded_parachain.id());
1533                assert_eq!(expected_parachain.chain(), loaded_parachain.chain());
1534                assert_eq!(
1535                    expected_parachain.chain_spec_path(),
1536                    loaded_parachain.chain_spec_path()
1537                );
1538                assert_eq!(
1539                    expected_parachain.registration_strategy(),
1540                    loaded_parachain.registration_strategy()
1541                );
1542                assert_eq!(
1543                    expected_parachain.onboard_as_parachain(),
1544                    loaded_parachain.onboard_as_parachain()
1545                );
1546                assert_eq!(
1547                    expected_parachain.default_db_snapshot(),
1548                    loaded_parachain.default_db_snapshot()
1549                );
1550                assert_eq!(
1551                    expected_parachain.default_command(),
1552                    loaded_parachain.default_command()
1553                );
1554                assert_eq!(
1555                    expected_parachain.default_image(),
1556                    loaded_parachain.default_image()
1557                );
1558                assert_eq!(
1559                    expected_parachain.collators().len(),
1560                    loaded_parachain.collators().len()
1561                );
1562                expected_parachain
1563                    .collators()
1564                    .iter()
1565                    .zip(loaded_parachain.collators().iter())
1566                    .for_each(|(expected_collator, loaded_collator)| {
1567                        assert_eq!(expected_collator.name(), loaded_collator.name());
1568                        assert_eq!(expected_collator.command(), loaded_collator.command());
1569                        assert_eq!(expected_collator.image(), loaded_collator.image());
1570                        assert_eq!(
1571                            expected_collator.is_validator(),
1572                            loaded_collator.is_validator()
1573                        );
1574                        assert_eq!(
1575                            expected_collator.is_bootnode(),
1576                            loaded_collator.is_bootnode()
1577                        );
1578                        assert_eq!(
1579                            expected_collator.is_invulnerable(),
1580                            loaded_collator.is_invulnerable()
1581                        );
1582                        assert_eq!(
1583                            expected_collator.initial_balance(),
1584                            loaded_collator.initial_balance()
1585                        );
1586                    });
1587            });
1588
1589        expected
1590            .hrmp_channels()
1591            .iter()
1592            .zip(load_from_toml.hrmp_channels().iter())
1593            .for_each(|(expected_hrmp_channel, loaded_hrmp_channel)| {
1594                assert_eq!(expected_hrmp_channel.sender(), loaded_hrmp_channel.sender());
1595                assert_eq!(
1596                    expected_hrmp_channel.recipient(),
1597                    loaded_hrmp_channel.recipient()
1598                );
1599                assert_eq!(
1600                    expected_hrmp_channel.max_capacity(),
1601                    loaded_hrmp_channel.max_capacity()
1602                );
1603                assert_eq!(
1604                    expected_hrmp_channel.max_message_size(),
1605                    loaded_hrmp_channel.max_message_size()
1606                );
1607            });
1608    }
1609
1610    #[test]
1611    fn the_toml_config_should_be_imported_and_match_a_network_with_overriden_defaults() {
1612        let load_from_toml =
1613            NetworkConfig::load_from_toml("./testing/snapshots/0002-overridden-defaults.toml")
1614                .unwrap();
1615
1616        let expected = NetworkConfigBuilder::new()
1617            .with_relaychain(|relaychain| {
1618                relaychain
1619                    .with_chain("polkadot")
1620                    .with_default_command("polkadot")
1621                    .with_default_image("docker.io/parity/polkadot:latest")
1622                    .with_default_args(vec![("-name", "value").into(), "--flag".into()])
1623                    .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz")
1624                    .with_default_resources(|resources| {
1625                        resources
1626                            .with_request_cpu(100000)
1627                            .with_request_memory("500M")
1628                            .with_limit_cpu("10Gi")
1629                            .with_limit_memory("4000M")
1630                    })
1631                    .with_validator(|node| {
1632                        node.with_name("alice")
1633                            .with_initial_balance(1_000_000_000)
1634                            .bootnode(true)
1635                            .invulnerable(true)
1636                    })
1637                    .with_validator(|node| {
1638                        node.with_name("bob")
1639                            .invulnerable(true)
1640                            .bootnode(true)
1641                            .with_image("mycustomimage:latest")
1642                            .with_command("my-custom-command")
1643                            .with_db_snapshot("https://storage.com/path/to/other/db_snapshot.tgz")
1644                            .with_resources(|resources| {
1645                                resources
1646                                    .with_request_cpu(1000)
1647                                    .with_request_memory("250Mi")
1648                                    .with_limit_cpu("5Gi")
1649                                    .with_limit_memory("2Gi")
1650                            })
1651                            .with_args(vec![("-myothername", "value").into()])
1652                    })
1653            })
1654            .with_parachain(|parachain| {
1655                parachain
1656                    .with_id(1000)
1657                    .with_chain("myparachain")
1658                    .with_chain_spec_path("/path/to/my/chain/spec.json")
1659                    .with_default_db_snapshot("https://storage.com/path/to/other_snapshot.tgz")
1660                    .with_default_command("my-default-command")
1661                    .with_default_image("mydefaultimage:latest")
1662                    .with_collator(|collator| {
1663                        collator
1664                            .with_name("john")
1665                            .bootnode(true)
1666                            .validator(true)
1667                            .invulnerable(true)
1668                            .with_initial_balance(5_000_000_000)
1669                            .with_command("my-non-default-command")
1670                            .with_image("anotherimage:latest")
1671                    })
1672                    .with_fullnode(|collator| {
1673                        collator
1674                            .with_name("charles")
1675                            .bootnode(true)
1676                            .invulnerable(true)
1677                            .with_initial_balance(0)
1678                    })
1679            })
1680            .build()
1681            .unwrap();
1682
1683        expected
1684            .parachains()
1685            .iter()
1686            .zip(load_from_toml.parachains().iter())
1687            .for_each(|(expected_parachain, loaded_parachain)| {
1688                assert_eq!(expected_parachain.id(), loaded_parachain.id());
1689                assert_eq!(expected_parachain.chain(), loaded_parachain.chain());
1690                assert_eq!(
1691                    expected_parachain.chain_spec_path(),
1692                    loaded_parachain.chain_spec_path()
1693                );
1694                assert_eq!(
1695                    expected_parachain.registration_strategy(),
1696                    loaded_parachain.registration_strategy()
1697                );
1698                assert_eq!(
1699                    expected_parachain.onboard_as_parachain(),
1700                    loaded_parachain.onboard_as_parachain()
1701                );
1702                assert_eq!(
1703                    expected_parachain.default_db_snapshot(),
1704                    loaded_parachain.default_db_snapshot()
1705                );
1706                assert_eq!(
1707                    expected_parachain.default_command(),
1708                    loaded_parachain.default_command()
1709                );
1710                assert_eq!(
1711                    expected_parachain.default_image(),
1712                    loaded_parachain.default_image()
1713                );
1714                assert_eq!(
1715                    expected_parachain.collators().len(),
1716                    loaded_parachain.collators().len()
1717                );
1718                expected_parachain
1719                    .collators()
1720                    .iter()
1721                    .zip(loaded_parachain.collators().iter())
1722                    .for_each(|(expected_collator, loaded_collator)| {
1723                        assert_eq!(expected_collator.name(), loaded_collator.name());
1724                        assert_eq!(expected_collator.command(), loaded_collator.command());
1725                        assert_eq!(expected_collator.image(), loaded_collator.image());
1726                        assert_eq!(
1727                            expected_collator.is_validator(),
1728                            loaded_collator.is_validator()
1729                        );
1730                        assert_eq!(
1731                            expected_collator.is_bootnode(),
1732                            loaded_collator.is_bootnode()
1733                        );
1734                        assert_eq!(
1735                            expected_collator.is_invulnerable(),
1736                            loaded_collator.is_invulnerable()
1737                        );
1738                        assert_eq!(
1739                            expected_collator.initial_balance(),
1740                            loaded_collator.initial_balance()
1741                        );
1742                    });
1743            });
1744    }
1745
1746    #[test]
1747    fn with_chain_and_nodes_works() {
1748        let network_config = NetworkConfigBuilder::with_chain_and_nodes(
1749            "rococo-local",
1750            vec!["alice".to_string(), "bob".to_string()],
1751        )
1752        .build()
1753        .unwrap();
1754
1755        // relaychain
1756        assert_eq!(network_config.relaychain().chain().as_str(), "rococo-local");
1757        assert_eq!(network_config.relaychain().nodes().len(), 2);
1758        let mut node_names = network_config.relaychain().nodes().into_iter();
1759        let node1 = node_names.next().unwrap().name();
1760        assert_eq!(node1, "alice");
1761        let node2 = node_names.next().unwrap().name();
1762        assert_eq!(node2, "bob");
1763
1764        // parachains
1765        assert_eq!(network_config.parachains().len(), 0);
1766    }
1767
1768    #[test]
1769    fn with_chain_and_nodes_should_fail_with_empty_relay_name() {
1770        let errors = NetworkConfigBuilder::with_chain_and_nodes("", vec!["alice".to_string()])
1771            .build()
1772            .unwrap_err();
1773
1774        assert_eq!(
1775            errors.first().unwrap().to_string(),
1776            "relaychain.chain: can't be empty"
1777        );
1778    }
1779
1780    #[test]
1781    fn with_chain_and_nodes_should_fail_with_empty_node_list() {
1782        let errors = NetworkConfigBuilder::with_chain_and_nodes("rococo-local", vec![])
1783            .build()
1784            .unwrap_err();
1785
1786        assert_eq!(
1787            errors.first().unwrap().to_string(),
1788            "relaychain.nodes[''].name: can't be empty"
1789        );
1790    }
1791
1792    #[test]
1793    fn with_chain_and_nodes_should_fail_with_empty_node_name() {
1794        let errors = NetworkConfigBuilder::with_chain_and_nodes(
1795            "rococo-local",
1796            vec!["alice".to_string(), "".to_string()],
1797        )
1798        .build()
1799        .unwrap_err();
1800
1801        assert_eq!(
1802            errors.first().unwrap().to_string(),
1803            "relaychain.nodes[''].name: can't be empty"
1804        );
1805    }
1806
1807    #[test]
1808    fn with_parachain_id_and_collators_works() {
1809        let network_config = NetworkConfigBuilder::with_chain_and_nodes(
1810            "rococo-local",
1811            vec!["alice".to_string(), "bob".to_string()],
1812        )
1813        .with_parachain_id_and_collators(
1814            100,
1815            vec!["collator1".to_string(), "collator2".to_string()],
1816        )
1817        .build()
1818        .unwrap();
1819
1820        // relaychain
1821        assert_eq!(network_config.relaychain().chain().as_str(), "rococo-local");
1822        assert_eq!(network_config.relaychain().nodes().len(), 2);
1823        let mut node_names = network_config.relaychain().nodes().into_iter();
1824        let node1 = node_names.next().unwrap().name();
1825        assert_eq!(node1, "alice");
1826        let node2 = node_names.next().unwrap().name();
1827        assert_eq!(node2, "bob");
1828
1829        // parachains
1830        assert_eq!(network_config.parachains().len(), 1);
1831        let &parachain1 = network_config.parachains().first().unwrap();
1832        assert_eq!(parachain1.id(), 100);
1833        assert_eq!(parachain1.collators().len(), 2);
1834        let mut collator_names = parachain1.collators().into_iter();
1835        let collator1 = collator_names.next().unwrap().name();
1836        assert_eq!(collator1, "collator1");
1837        let collator2 = collator_names.next().unwrap().name();
1838        assert_eq!(collator2, "collator2");
1839
1840        assert_eq!(parachain1.initial_balance(), 2_000_000_000_000);
1841    }
1842
1843    #[test]
1844    fn with_parachain_id_and_collators_should_fail_with_empty_collator_list() {
1845        let errors =
1846            NetworkConfigBuilder::with_chain_and_nodes("polkadot", vec!["alice".to_string()])
1847                .with_parachain_id_and_collators(1, vec![])
1848                .build()
1849                .unwrap_err();
1850
1851        assert_eq!(
1852            errors.first().unwrap().to_string(),
1853            "parachain[1].can't be empty"
1854        );
1855    }
1856
1857    #[test]
1858    fn with_parachain_id_and_collators_should_fail_with_empty_collator_name() {
1859        let errors =
1860            NetworkConfigBuilder::with_chain_and_nodes("polkadot", vec!["alice".to_string()])
1861                .with_parachain_id_and_collators(1, vec!["collator1".to_string(), "".to_string()])
1862                .build()
1863                .unwrap_err();
1864
1865        assert_eq!(
1866            errors.first().unwrap().to_string(),
1867            "parachain[1].collators[''].name: can't be empty"
1868        );
1869    }
1870
1871    #[test]
1872    fn wasm_override_in_toml_should_work() {
1873        let load_from_toml = NetworkConfig::load_from_toml(
1874            "./testing/snapshots/0005-small-networl-with-wasm-override.toml",
1875        )
1876        .unwrap();
1877
1878        let expected = NetworkConfigBuilder::new()
1879            .with_relaychain(|relaychain| {
1880                relaychain
1881                    .with_chain("rococo-local")
1882                    .with_default_command("polkadot")
1883                    .with_wasm_override("/some/path/runtime.wasm")
1884                    .with_validator(|node| node.with_name("alice"))
1885                    .with_validator(|node| node.with_name("bob"))
1886            })
1887            .with_parachain(|p| {
1888                p.with_id(1000)
1889                    .with_wasm_override("https://some.com/runtime.wasm")
1890                    .with_collator(|c| c.with_name("john"))
1891            })
1892            .build()
1893            .unwrap();
1894
1895        assert_eq!(
1896            load_from_toml.relaychain().wasm_override(),
1897            expected.relaychain().wasm_override()
1898        );
1899        assert_eq!(
1900            load_from_toml.parachains()[0].wasm_override(),
1901            expected.parachains()[0].wasm_override()
1902        );
1903    }
1904
1905    #[test]
1906    fn multiple_paras_with_same_id_should_work() {
1907        let network_config = NetworkConfigBuilder::new()
1908            .with_relaychain(|relaychain| {
1909                relaychain
1910                    .with_chain("polkadot")
1911                    .with_fullnode(|node| node.with_name("node").with_command("command"))
1912                    .with_validator(|n| n.with_name("v"))
1913                    .with_validator(|n| n.with_name("v1"))
1914            })
1915            .with_parachain(|parachain| {
1916                parachain
1917                    .with_id(1)
1918                    .with_chain("myparachain1")
1919                    .with_collator(|collator| {
1920                        collator.with_name("collator1").with_command("command1")
1921                    })
1922            })
1923            .with_parachain(|parachain| {
1924                parachain
1925                    .with_id(1)
1926                    .with_chain("myparachain1")
1927                    .with_registration_strategy(RegistrationStrategy::Manual)
1928                    .with_collator(|collator| {
1929                        collator.with_name("collator2").with_command("command1")
1930                    })
1931            })
1932            .build()
1933            .unwrap();
1934
1935        let &parachain2 = network_config.parachains().last().unwrap();
1936        assert_eq!(parachain2.unique_id(), "1-1");
1937    }
1938
1939    #[test]
1940    fn multiple_paras_with_same_id_both_for_register_should_fail() {
1941        let errors = NetworkConfigBuilder::new()
1942            .with_relaychain(|relaychain| {
1943                relaychain
1944                    .with_chain("polkadot")
1945                    .with_fullnode(|node| node.with_name("node").with_command("command"))
1946            })
1947            .with_parachain(|parachain| {
1948                parachain
1949                    .with_id(1)
1950                    .with_chain("myparachain1")
1951                    .with_collator(|collator| {
1952                        collator.with_name("collator1").with_command("command1")
1953                    })
1954            })
1955            .with_parachain(|parachain| {
1956                parachain
1957                    .with_id(1)
1958                    .with_chain("myparachain1")
1959                    // .with_registration_strategy(RegistrationStrategy::UsingExtrinsic)
1960                    .with_collator(|collator| {
1961                        collator
1962                            .with_name("collator2")
1963                            .with_command("command1")
1964                    })
1965            })
1966            .build()
1967            .unwrap_err();
1968
1969        assert_eq!(
1970            errors.first().unwrap().to_string(),
1971            "ParaId 1 already set to be registered, only one should be."
1972        );
1973    }
1974
1975    #[test]
1976    fn network_config_should_work_from_toml_without_chain_name() {
1977        let loaded_from_toml =
1978            NetworkConfig::load_from_toml("./testing/snapshots/0006-without-rc-chain-name.toml")
1979                .unwrap();
1980
1981        assert_eq!(
1982            "rococo-local",
1983            loaded_from_toml.relaychain().chain().as_str()
1984        );
1985    }
1986
1987    #[test]
1988    fn network_config_should_work_from_toml_with_duplicate_name_between_collator_and_relay_node() {
1989        let loaded_from_toml = NetworkConfig::load_from_toml(
1990            "./testing/snapshots/0007-small-network_w_parachain_w_duplicate_node_names.toml",
1991        )
1992        .unwrap();
1993
1994        assert_eq!(
1995            loaded_from_toml
1996                .relaychain()
1997                .nodes()
1998                .iter()
1999                .filter(|n| n.name() == "alice")
2000                .count(),
2001            1
2002        );
2003        assert_eq!(
2004            loaded_from_toml
2005                .parachains()
2006                .iter()
2007                .flat_map(|para| para.collators())
2008                .filter(|n| n.name() == "alice-1")
2009                .count(),
2010            1
2011        );
2012    }
2013
2014    #[test]
2015    fn raw_spec_override_in_toml_should_work() {
2016        let load_from_toml = NetworkConfig::load_from_toml(
2017            "./testing/snapshots/0008-small-network-with-raw-spec-override.toml",
2018        )
2019        .unwrap();
2020
2021        let expected = NetworkConfigBuilder::new()
2022            .with_relaychain(|relaychain| {
2023                relaychain
2024                    .with_chain("rococo-local")
2025                    .with_default_command("polkadot")
2026                    .with_raw_spec_override("/some/path/raw_spec_override.json")
2027                    .with_validator(|node| node.with_name("alice"))
2028                    .with_validator(|node| node.with_name("bob"))
2029            })
2030            .with_parachain(|p| {
2031                p.with_id(1000)
2032                    .with_raw_spec_override("https://some.com/raw_spec_override.json")
2033                    .with_collator(|c| c.with_name("john"))
2034            })
2035            .build()
2036            .unwrap();
2037
2038        assert_eq!(
2039            load_from_toml.relaychain().raw_spec_override(),
2040            expected.relaychain().raw_spec_override()
2041        );
2042        assert_eq!(
2043            load_from_toml.parachains()[0].raw_spec_override(),
2044            expected.parachains()[0].raw_spec_override()
2045        );
2046    }
2047
2048    const TOML_WITH_RELATIVE_PATHS: &str = r#"
2049[relaychain]
2050chain = "rococo-local"
2051default_command = "./bin/polkadot"
2052chain_spec_path = "./rc.json"
2053
2054[[relaychain.nodes]]
2055name = "alice"
2056validator = true
2057command = "polkadot"
2058"#;
2059
2060    #[test]
2061    fn toml_string_should_resolve_relative_paths_against_the_given_base_dir() {
2062        let config = NetworkConfig::load_from_toml_string_with_base_dir(
2063            TOML_WITH_RELATIVE_PATHS,
2064            Path::new("/tmp/some-base-dir"),
2065        )
2066        .unwrap();
2067
2068        assert_eq!(
2069            config.relaychain().default_command().unwrap().as_str(),
2070            "/tmp/some-base-dir/bin/polkadot"
2071        );
2072        assert_eq!(
2073            config.relaychain().chain_spec_path().unwrap().to_string(),
2074            "/tmp/some-base-dir/rc.json"
2075        );
2076        // non-relative values are left untouched
2077        assert_eq!(
2078            config.relaychain().nodes()[0].command().unwrap().as_str(),
2079            "polkadot"
2080        );
2081    }
2082
2083    #[test]
2084    fn toml_string_should_resolve_relative_paths_against_the_cwd_by_default() {
2085        let from_cwd = NetworkConfig::load_from_toml_string(TOML_WITH_RELATIVE_PATHS).unwrap();
2086        let expected = NetworkConfig::load_from_toml_string_with_base_dir(
2087            TOML_WITH_RELATIVE_PATHS,
2088            &std::env::current_dir().unwrap(),
2089        )
2090        .unwrap();
2091
2092        assert_eq!(from_cwd, expected);
2093    }
2094
2095    #[test]
2096    fn toml_string_and_toml_file_should_produce_the_same_config() {
2097        let path = "./testing/snapshots/0006-without-rc-chain-name.toml";
2098        let from_file = NetworkConfig::load_from_toml(path).unwrap();
2099
2100        let file_path = PathBuf::from(path).canonicalize().unwrap();
2101        let base_dir = file_path.parent().unwrap();
2102        let from_string = NetworkConfig::load_from_toml_string_with_base_dir(
2103            &fs::read_to_string(path).unwrap(),
2104            base_dir,
2105        )
2106        .unwrap();
2107
2108        assert_eq!(from_file, from_string);
2109
2110        // and the relative paths were actually rewritten, not just rewritten identically
2111        assert_eq!(
2112            from_string
2113                .relaychain()
2114                .chain_spec_path()
2115                .unwrap()
2116                .to_string(),
2117            base_dir.join("rc.json").to_string_lossy()
2118        );
2119        assert_eq!(
2120            from_string.parachains()[0]
2121                .chain_spec_path()
2122                .unwrap()
2123                .to_string(),
2124            base_dir.join("parachain.json").to_string_lossy()
2125        );
2126    }
2127
2128    #[test]
2129    fn demo_denis() {
2130        let network_config = NetworkConfigBuilder::new()
2131            .with_relaychain(|relaychain| {
2132                relaychain
2133                    .with_chain("westend")
2134                    .with_default_command("polkadot")
2135                    .with_default_image("docker.io/parity/polkadot:stable2603-3")
2136                    .with_validator(|n| n.with_name("validator-0"))
2137                    .with_validator(|n| n.with_name("validator-1"))
2138            })
2139            .with_parachain(|parachain| {
2140                parachain
2141                    .with_id(1004)
2142                    .with_chain_spec_path("https://raw.githubusercontent.com/paritytech/polkadot-sdk/104e66f7114ea3322187b4b93255bb9f3f4d5005/cumulus/zombienet/zombienet-sdk/tests/zombie_ci/statement_store/people-westend-local-spec.json")
2143                    .with_collator_group(|g| {
2144                        g.with_count(100)
2145                        .with_base_node(|n|{
2146                            n.with_name("collator")
2147                            .with_command("polkadot-parachain")
2148                            .with_args(vec!["--enable-statement-store".into()])
2149                            .with_image("docker.io/parity/polkadot-parachain:stable2603-3")
2150                        })
2151                })
2152            })
2153            .build()
2154            .unwrap();
2155
2156        let toml_string = network_config.dump_to_toml().unwrap();
2157        println!("{}", toml_string);
2158    }
2159}