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, 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    jamchain::{self, JamchainConfig, JamchainConfigBuilder},
24    parachain::{self, ParachainConfig, ParachainConfigBuilder},
25    relaychain::{self, RelaychainConfig, RelaychainConfigBuilder},
26    shared::{
27        errors::{ConfigError, ValidationError},
28        helpers::{generate_unique_node_name_from_names, merge_errors, merge_errors_vecs},
29        macros::states,
30        node::{GroupNodeConfig, NodeConfig},
31        types::{Arg, AssetLocation, Chain, Command, Image, ValidationContext},
32    },
33    types::ParaId,
34    CustomProcess, CustomProcessBuilder, RegistrationStrategy,
35};
36
37/// A network configuration, composed of a relaychain (or a JAM chain), parachains and HRMP
38/// channels.
39///
40/// The JAM chain takes the place of the relaychain, so a network has one or the other, never
41/// both.
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub struct NetworkConfig {
44    #[serde(rename = "settings", default = "GlobalSettings::default")]
45    global_settings: GlobalSettings,
46    relaychain: Option<RelaychainConfig>,
47    jamchain: Option<JamchainConfig>,
48    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
49    parachains: Vec<ParachainConfig>,
50    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
51    hrmp_channels: Vec<HrmpChannelConfig>,
52    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
53    custom_processes: Vec<CustomProcess>,
54}
55
56impl NetworkConfig {
57    /// The global settings of the network.
58    pub fn global_settings(&self) -> &GlobalSettings {
59        &self.global_settings
60    }
61
62    /// The relay chain of the network.
63    pub fn relaychain(&self) -> &RelaychainConfig {
64        self.relaychain
65            .as_ref()
66            .expect(&format!("{RELAY_NOT_NONE}, {THIS_IS_A_BUG}"))
67    }
68
69    /// The relay chain of the network, `None` for a JAM network.
70    pub fn try_relaychain(&self) -> Option<&RelaychainConfig> {
71        self.relaychain.as_ref()
72    }
73
74    /// The jam chain of the network.
75    pub fn jamchain(&self) -> Option<&JamchainConfig> {
76        if let Some(jamconfig) = &self.jamchain {
77            Some(jamconfig)
78        } else {
79            None
80        }
81    }
82
83    /// The parachains of the network.
84    pub fn parachains(&self) -> Vec<&ParachainConfig> {
85        self.parachains.iter().collect::<Vec<_>>()
86    }
87
88    /// The HRMP channels of the network.
89    pub fn hrmp_channels(&self) -> Vec<&HrmpChannelConfig> {
90        self.hrmp_channels.iter().collect::<Vec<_>>()
91    }
92
93    /// The Custom process to spawn
94    pub fn custom_processes(&self) -> Vec<&CustomProcess> {
95        self.custom_processes.iter().collect::<Vec<_>>()
96    }
97
98    /// Set the parachain.
99    fn set_parachains(&mut self, parachains: Vec<ParachainConfig>) {
100        self.parachains = parachains;
101    }
102
103    /// A helper function to dump the network configuration to a TOML string.
104    pub fn dump_to_toml(&self) -> Result<String, toml::ser::Error> {
105        // This regex is used to replace the "" enclosed u128 value to a raw u128 because u128 is not supported for TOML serialization/deserialization.
106        let re = Regex::new(r#""U128%(?<u128_value>\d+)""#)
107            .expect(&format!("{VALID_REGEX} {THIS_IS_A_BUG}"));
108        let toml_string = toml::to_string_pretty(&self)?;
109
110        Ok(re.replace_all(&toml_string, "$u128_value").to_string())
111    }
112
113    /// A helper function to load a network configuration from a TOML file.
114    pub fn load_from_toml_with_settings(
115        path: &str,
116        settings: &GlobalSettings,
117    ) -> Result<NetworkConfig, anyhow::Error> {
118        let mut network_config = NetworkConfig::load_from_toml(path)?;
119        network_config.global_settings = settings.clone();
120        Ok(network_config)
121    }
122
123    /// A helper function to load a network configuration from a TOML file.
124    pub fn load_from_toml(path: &str) -> Result<NetworkConfig, anyhow::Error> {
125        let file_path = PathBuf::from(path).canonicalize()?;
126        let network_definition_dir = file_path
127            .parent()
128            .ok_or(anyhow!("should have a base dir"))?;
129        trace!(
130            "file_dir for replacer: {:?}",
131            network_definition_dir.as_os_str()
132        );
133        let file_str = fs::read_to_string(path)
134            .map_err(|e| anyhow!("Failed to read configuration file '{}': {}", path, e))?;
135
136        Self::load_from_toml_string_with_base_dir(&file_str, network_definition_dir)
137    }
138
139    /// A helper function to load a network configuration from a TOML string.
140    ///
141    /// Relative paths (e.g. `command = "./bin/polkadot"`) are resolved against the current
142    /// working directory, since there is no configuration file to use as base. Use
143    /// [`NetworkConfig::load_from_toml_string_with_base_dir`] to resolve them against a
144    /// specific directory instead.
145    pub fn load_from_toml_string(toml_data: &str) -> Result<NetworkConfig, anyhow::Error> {
146        let base_dir = std::env::current_dir()?;
147
148        Self::load_from_toml_string_with_base_dir(toml_data, &base_dir)
149    }
150
151    /// A helper function to load a network configuration from a TOML string, resolving
152    /// relative paths (e.g. `command = "./bin/polkadot"`) against `base_dir`.
153    pub fn load_from_toml_string_with_base_dir(
154        toml_text: &str,
155        base_dir: &Path,
156    ) -> Result<NetworkConfig, anyhow::Error> {
157        let re: Regex = Regex::new(r"(?<field_name>(initial_)?balance)\s+=\s+(?<u128_value>\d+)")
158            .expect(&format!("{VALID_REGEX} {THIS_IS_A_BUG}"));
159
160        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\.\/]+)\""#)
161            .expect(&format!("{VALID_REGEX} {THIS_IS_A_BUG}"));
162
163        let path_replacer = |caps: &Captures| {
164            trace!("cmd replacer captures: {:?}", caps);
165            let cmd = maybe_absolute(&caps["value_string"], base_dir);
166            let line = format!("{} = \"{cmd}\"", &caps["field_name"]);
167            trace!("line after replacer: {line}");
168            line
169        };
170
171        let toml_text = re.replace_all(toml_text, "$field_name = \"$u128_value\"");
172        let toml_text = re_paths.replace_all(&toml_text, &path_replacer);
173
174        trace!("toml text to parse: {}", toml_text);
175        // apply replacements from env in toml
176        let toml_text = apply_env_replacements(&toml_text);
177        trace!("toml text after replacements: {}", toml_text);
178        let mut network_config: NetworkConfig = toml::from_str(&toml_text)?;
179        trace!("parsed config {network_config:#?}");
180
181        // A network is rooted either in a relaychain or in a JAM chain.
182        if network_config.relaychain.is_none() && network_config.jamchain.is_none() {
183            Err(anyhow!("Relay chain or Jam definition does not exist."))?
184        }
185
186        // Keep track of node names to ensure uniqueness, shared between the nodes of the
187        // root chain and the collators.
188        let mut names = HashSet::new();
189
190        if let Some(relaychain) = network_config.relaychain.as_mut() {
191            // retrieve the defaults relaychain for assigning to nodes if needed
192            let mut relaychain_default_command: Option<Command> =
193                relaychain.default_command().cloned();
194
195            if relaychain_default_command.is_none() {
196                relaychain_default_command = relaychain.command().cloned();
197            }
198
199            let relaychain_default_image: Option<Image> = relaychain.default_image().cloned();
200
201            let relaychain_default_db_snapshot: Option<AssetLocation> =
202                relaychain.default_db_snapshot().cloned();
203
204            let default_args: Vec<Arg> = relaychain.default_args().into_iter().cloned().collect();
205
206            let mut nodes: Vec<NodeConfig> = relaychain.nodes().into_iter().cloned().collect();
207
208            let group_nodes: Vec<GroupNodeConfig> = relaychain
209                .group_node_configs()
210                .into_iter()
211                .cloned()
212                .collect();
213
214            if let Some(group) = group_nodes.iter().find(|n| n.count == 0) {
215                return Err(anyhow!(
216                    "Group node '{}' must have a count greater than 0.",
217                    group.base_config.name()
218                ));
219            }
220
221            // Validation checks for relay
222            TryInto::<Chain>::try_into(relaychain.chain().as_str())?;
223            if let Some(default_image) = &relaychain_default_image {
224                TryInto::<Image>::try_into(default_image.clone())?;
225            }
226            if let Some(default_command) = &relaychain_default_command {
227                TryInto::<Command>::try_into(default_command.clone())?;
228            }
229
230            for node in nodes.iter_mut() {
231                if relaychain_default_command.is_some() {
232                    // we modify only nodes which don't already have a command
233                    if node.command.is_none() {
234                        node.command.clone_from(&relaychain_default_command);
235                    }
236                }
237
238                if relaychain_default_image.is_some() && node.image.is_none() {
239                    node.image.clone_from(&relaychain_default_image);
240                }
241
242                if relaychain_default_db_snapshot.is_some() && node.db_snapshot.is_none() {
243                    node.db_snapshot.clone_from(&relaychain_default_db_snapshot);
244                }
245
246                if !default_args.is_empty() && node.args().is_empty() {
247                    node.set_args(default_args.clone());
248                }
249
250                let unique_name = generate_unique_node_name_from_names(node.name(), &mut names);
251                node.name = unique_name;
252            }
253
254            relaychain.set_nodes(nodes);
255        }
256
257        let mut parachains: Vec<ParachainConfig> =
258            network_config.parachains().into_iter().cloned().collect();
259
260        for para in parachains.iter_mut() {
261            // retrieve the defaults parachain for assigning to collators if needed
262            let parachain_default_command: Option<Command> = para.default_command().cloned();
263
264            let parachain_default_image: Option<Image> = para.default_image().cloned();
265
266            let parachain_default_db_snapshot: Option<AssetLocation> =
267                para.default_db_snapshot().cloned();
268
269            let default_args: Vec<Arg> = para.default_args().into_iter().cloned().collect();
270
271            let group_collators: Vec<GroupNodeConfig> = para
272                .group_collators_configs()
273                .into_iter()
274                .cloned()
275                .collect();
276
277            if let Some(group) = group_collators.iter().find(|n| n.count == 0) {
278                return Err(anyhow!(
279                    "Group node '{}' must have a count greater than 0.",
280                    group.base_config.name()
281                ));
282            }
283
284            let mut collators: Vec<NodeConfig> = para.collators.clone();
285
286            for collator in collators.iter_mut() {
287                populate_collator_with_defaults(
288                    collator,
289                    &parachain_default_command,
290                    &parachain_default_image,
291                    &parachain_default_db_snapshot,
292                    &default_args,
293                );
294                let unique_name = generate_unique_node_name_from_names(collator.name(), &mut names);
295                collator.name = unique_name;
296            }
297
298            para.collators = collators;
299
300            if let Some(mut collator) = para.collator.clone() {
301                populate_collator_with_defaults(
302                    &mut collator,
303                    &parachain_default_command,
304                    &parachain_default_image,
305                    &parachain_default_db_snapshot,
306                    &default_args,
307                );
308                let unique_name = generate_unique_node_name_from_names(collator.name(), &mut names);
309                collator.name = unique_name;
310                para.collator = Some(collator);
311            }
312        }
313
314        network_config.set_parachains(parachains);
315
316        // Validation checks for parachains
317        network_config.parachains().iter().for_each(|parachain| {
318            if let Some(default_image) = parachain.default_image() {
319                let _ = TryInto::<Image>::try_into(default_image.as_str());
320            }
321            if let Some(default_command) = parachain.default_command() {
322                let _ = TryInto::<Command>::try_into(default_command.as_str());
323            }
324        });
325
326        // Custom processes bypass their builder here, so apply its rules (see
327        // `CustomProcess::validate`), and a name that is not already a node's
328        // (they share one registry on the running network).
329        let mut errors: Vec<String> = vec![];
330        for process in &network_config.custom_processes {
331            if names.contains(process.name()) {
332                errors.push(format!(
333                    "custom_processes['{}'].name: already used by a node or custom process",
334                    process.name()
335                ));
336            }
337            names.insert(process.name().to_string());
338            if let Err(port_errors) = process.validate() {
339                errors.extend(
340                    port_errors
341                        .into_iter()
342                        .map(|e| format!("custom_processes['{}'].{e}", process.name())),
343                );
344            }
345        }
346        if let Err(dependency_errors) =
347            custom_process::validate_custom_processes(&network_config.custom_processes)
348        {
349            errors.extend(dependency_errors);
350        }
351        if !errors.is_empty() {
352            Err(anyhow!(errors.join("\n")))?
353        }
354
355        Ok(network_config)
356    }
357}
358
359fn maybe_absolute(cmd: &str, base_dir: &Path) -> String {
360    if cmd.starts_with(".") {
361        let mut full = PathBuf::from(base_dir);
362        full.push(cmd);
363        trace!("full_path: {:?}", full);
364        std::path::absolute(&full)
365            .expect(&format!(
366                "absolute path shoud be valid: {:?}",
367                full.as_os_str()
368            ))
369            .to_string_lossy()
370            .to_string()
371    } else {
372        cmd.to_string()
373    }
374}
375
376fn populate_collator_with_defaults(
377    collator: &mut NodeConfig,
378    parachain_default_command: &Option<Command>,
379    parachain_default_image: &Option<Image>,
380    parachain_default_db_snapshot: &Option<AssetLocation>,
381    default_args: &[Arg],
382) {
383    if parachain_default_command.is_some() {
384        // we modify only nodes which don't already have a command
385        if collator.command.is_none() {
386            collator.command.clone_from(parachain_default_command);
387        }
388    }
389
390    if parachain_default_image.is_some() && collator.image.is_none() {
391        collator.image.clone_from(parachain_default_image);
392    }
393
394    if parachain_default_db_snapshot.is_some() && collator.db_snapshot.is_none() {
395        collator
396            .db_snapshot
397            .clone_from(parachain_default_db_snapshot);
398    }
399
400    if !default_args.is_empty() && collator.args().is_empty() {
401        collator.set_args(default_args.to_owned());
402    }
403}
404
405states! {
406    Initial,
407    Buildable
408}
409
410/// A network configuration builder, used to build a [`NetworkConfig`] declaratively with fields validation.
411///
412/// # Example:
413///
414/// ```
415/// use zombienet_configuration::NetworkConfigBuilder;
416///
417/// let network_config = NetworkConfigBuilder::new()
418///     .with_relaychain(|relaychain| {
419///         relaychain
420///             .with_chain("polkadot")
421///             .with_random_nominators_count(10)
422///             .with_default_resources(|resources| {
423///                 resources
424///                     .with_limit_cpu("1000m")
425///                     .with_request_memory("1Gi")
426///                     .with_request_cpu(100_000)
427///             })
428///             .with_validator(|node| node.with_name("node").with_command("command"))
429///             .with_validator(|node| node.with_name("node1").with_command("command"))
430///     })
431///     .with_parachain(|parachain| {
432///         parachain
433///             .with_id(1000)
434///             .with_chain("myparachain1")
435///             .with_initial_balance(100_000)
436///             .with_default_image("myimage:version")
437///             .with_collator(|collator| {
438///                 collator
439///                     .with_name("collator1")
440///                     .with_command("command1")
441///                     .validator(true)
442///             })
443///     })
444///     .with_parachain(|parachain| {
445///         parachain
446///             .with_id(2000)
447///             .with_chain("myparachain2")
448///             .with_initial_balance(50_0000)
449///             .with_collator(|collator| {
450///                 collator
451///                     .with_name("collator2")
452///                     .with_command("command2")
453///                     .validator(true)
454///             })
455///     })
456///     .with_hrmp_channel(|hrmp_channel1| {
457///         hrmp_channel1
458///             .with_sender(1)
459///             .with_recipient(2)
460///             .with_max_capacity(200)
461///             .with_max_message_size(500)
462///     })
463///     .with_hrmp_channel(|hrmp_channel2| {
464///         hrmp_channel2
465///             .with_sender(2)
466///             .with_recipient(1)
467///             .with_max_capacity(100)
468///             .with_max_message_size(250)
469///     })
470///     .with_global_settings(|global_settings| {
471///         global_settings
472///             .with_network_spawn_timeout(1200)
473///             .with_node_spawn_timeout(240)
474///     })
475///     .build();
476///
477/// assert!(network_config.is_ok())
478/// ```
479pub struct NetworkConfigBuilder<State> {
480    config: NetworkConfig,
481    validation_context: Rc<RefCell<ValidationContext>>,
482    errors: Vec<anyhow::Error>,
483    _state: PhantomData<State>,
484}
485
486impl Default for NetworkConfigBuilder<Initial> {
487    fn default() -> Self {
488        Self {
489            config: NetworkConfig {
490                global_settings: GlobalSettingsBuilder::new()
491                    .build()
492                    .expect(&format!("{NO_ERR_DEF_BUILDER}, {THIS_IS_A_BUG}")),
493                relaychain: None,
494                jamchain: None,
495                parachains: vec![],
496                hrmp_channels: vec![],
497                custom_processes: vec![],
498            },
499            validation_context: Default::default(),
500            errors: vec![],
501            _state: PhantomData,
502        }
503    }
504}
505
506impl<A> NetworkConfigBuilder<A> {
507    fn transition<B>(
508        config: NetworkConfig,
509        validation_context: Rc<RefCell<ValidationContext>>,
510        errors: Vec<anyhow::Error>,
511    ) -> NetworkConfigBuilder<B> {
512        NetworkConfigBuilder {
513            config,
514            errors,
515            validation_context,
516            _state: PhantomData,
517        }
518    }
519
520    /// Set the default tiny configuration for JAM
521    /// id: dev
522    /// 6 validators with names jam0..jam5
523    /// 1 ordinary with name jam-or
524    /// NO corevm builder
525    /// NO corevm monitor
526    fn setup_tiny_jamchain(self) -> NetworkConfigBuilder<Buildable> {
527        let mut builder = JamchainConfigBuilder::new(self.validation_context.clone())
528            .with_id("dev")
529            .with_validator(|n| n.with_name("jam0"));
530
531        for i in 1..6 {
532            builder = builder.with_validator(|n| n.with_name(&format!("jam{i}")));
533        }
534
535        let builder = builder.with_ordinary(|n| n.with_name("jam-or"));
536
537        match builder.build() {
538            Ok(jamchain) => Self::transition(
539                NetworkConfig {
540                    jamchain: Some(jamchain),
541                    ..self.config
542                },
543                self.validation_context,
544                self.errors,
545            ),
546            Err(errors) => Self::transition(self.config, self.validation_context, errors),
547        }
548    }
549}
550
551impl NetworkConfigBuilder<Initial> {
552    pub fn new() -> NetworkConfigBuilder<Initial> {
553        Self::default()
554    }
555
556    /// uses the default options for both the relay chain and the validator nodes
557    /// the only required fields are the name of the validator nodes,
558    /// and the name of the relay chain ("rococo-local", "polkadot", etc.)
559    pub fn with_chain_and_nodes(
560        relay_name: &str,
561        node_names: Vec<String>,
562    ) -> NetworkConfigBuilder<Buildable> {
563        let network_config = NetworkConfigBuilder::new().with_relaychain(|relaychain| {
564            let mut relaychain_with_node =
565                relaychain.with_chain(relay_name).with_validator(|node| {
566                    node.with_name(node_names.first().unwrap_or(&"".to_string()))
567                });
568
569            for node_name in node_names.iter().skip(1) {
570                relaychain_with_node = relaychain_with_node
571                    .with_validator(|node_builder| node_builder.with_name(node_name));
572            }
573            relaychain_with_node
574        });
575
576        Self::transition(
577            network_config.config,
578            network_config.validation_context,
579            network_config.errors,
580        )
581    }
582
583    /// Set the relay chain using a nested [`RelaychainConfigBuilder`].
584    pub fn with_relaychain(
585        self,
586        f: impl FnOnce(
587            RelaychainConfigBuilder<relaychain::Initial>,
588        ) -> RelaychainConfigBuilder<relaychain::WithAtLeastOneNode>,
589    ) -> NetworkConfigBuilder<Buildable> {
590        match f(RelaychainConfigBuilder::new(
591            self.validation_context.clone(),
592        ))
593        .build()
594        {
595            Ok(relaychain) => Self::transition(
596                NetworkConfig {
597                    relaychain: Some(relaychain),
598                    ..self.config
599                },
600                self.validation_context,
601                self.errors,
602            ),
603            Err(errors) => Self::transition(self.config, self.validation_context, errors),
604        }
605    }
606
607    /// Set the jam chain using a nested [`JamchainConfigBuilder`].
608    pub fn with_jamchain(
609        self,
610        f: impl FnOnce(
611            JamchainConfigBuilder<jamchain::Initial>,
612        ) -> JamchainConfigBuilder<jamchain::WithAtLeastOneNode>,
613    ) -> NetworkConfigBuilder<Buildable> {
614        match f(JamchainConfigBuilder::new(self.validation_context.clone())).build() {
615            Ok(jamchain) => Self::transition(
616                NetworkConfig {
617                    jamchain: Some(jamchain),
618                    ..self.config
619                },
620                self.validation_context,
621                self.errors,
622            ),
623            Err(errors) => Self::transition(self.config, self.validation_context, errors),
624        }
625    }
626
627    /// Set the default tiny configuration for JAM
628    /// see [`setup_tiny_jamchain`] fn.
629    pub fn with_tiny_jamchain(self) -> NetworkConfigBuilder<Buildable> {
630        self.setup_tiny_jamchain()
631    }
632}
633
634impl NetworkConfigBuilder<Buildable> {
635    // Allow to set jam/relay in buildable state (override if already set)
636
637    /// Set the relay chain using a nested [`RelaychainConfigBuilder`].
638    pub fn with_relaychain(
639        self,
640        f: impl FnOnce(
641            RelaychainConfigBuilder<relaychain::Initial>,
642        ) -> RelaychainConfigBuilder<relaychain::WithAtLeastOneNode>,
643    ) -> NetworkConfigBuilder<Buildable> {
644        match f(RelaychainConfigBuilder::new(
645            self.validation_context.clone(),
646        ))
647        .build()
648        {
649            Ok(relaychain) => Self::transition(
650                NetworkConfig {
651                    relaychain: Some(relaychain),
652                    ..self.config
653                },
654                self.validation_context,
655                self.errors,
656            ),
657            Err(errors) => Self::transition(self.config, self.validation_context, errors),
658        }
659    }
660
661    /// Set the jam chain using a nested [`JamchainConfigBuilder`].
662    pub fn with_jamchain(
663        self,
664        f: impl FnOnce(
665            JamchainConfigBuilder<jamchain::Initial>,
666        ) -> JamchainConfigBuilder<jamchain::WithAtLeastOneNode>,
667    ) -> NetworkConfigBuilder<Buildable> {
668        match f(JamchainConfigBuilder::new(self.validation_context.clone())).build() {
669            Ok(jamchain) => Self::transition(
670                NetworkConfig {
671                    jamchain: Some(jamchain),
672                    ..self.config
673                },
674                self.validation_context,
675                self.errors,
676            ),
677            Err(errors) => Self::transition(self.config, self.validation_context, errors),
678        }
679    }
680
681    /// Set the default tiny configuration for JAM
682    /// see [`setup_tiny_jamchain`] fn.
683    pub fn with_tiny_jamchain(self) -> NetworkConfigBuilder<Buildable> {
684        self.setup_tiny_jamchain()
685    }
686
687    /// Set the global settings using a nested [`GlobalSettingsBuilder`].
688    pub fn with_global_settings(
689        self,
690        f: impl FnOnce(GlobalSettingsBuilder) -> GlobalSettingsBuilder,
691    ) -> Self {
692        match f(GlobalSettingsBuilder::new()).build() {
693            Ok(global_settings) => Self::transition(
694                NetworkConfig {
695                    global_settings,
696                    ..self.config
697                },
698                self.validation_context,
699                self.errors,
700            ),
701            Err(errors) => Self::transition(
702                self.config,
703                self.validation_context,
704                merge_errors_vecs(self.errors, errors),
705            ),
706        }
707    }
708
709    /// Add a parachain using a nested [`ParachainConfigBuilder`].
710    pub fn with_parachain(
711        self,
712        f: impl FnOnce(
713            ParachainConfigBuilder<parachain::states::Initial, parachain::states::Bootstrap>,
714        ) -> ParachainConfigBuilder<
715            parachain::states::WithAtLeastOneCollator,
716            parachain::states::Bootstrap,
717        >,
718    ) -> Self {
719        match f(ParachainConfigBuilder::new(self.validation_context.clone())).build() {
720            Ok(parachain) => Self::transition(
721                NetworkConfig {
722                    parachains: [self.config.parachains, vec![parachain]].concat(),
723                    ..self.config
724                },
725                self.validation_context,
726                self.errors,
727            ),
728            Err(errors) => Self::transition(
729                self.config,
730                self.validation_context,
731                merge_errors_vecs(self.errors, errors),
732            ),
733        }
734    }
735
736    /// uses default settings for setting for:
737    /// - the parachain,
738    /// - the global settings
739    /// - the hrmp channels
740    ///
741    /// the only required parameters are the names of the collators as a vector,
742    /// and the id of the parachain
743    pub fn with_parachain_id_and_collators(self, id: u32, collator_names: Vec<String>) -> Self {
744        if collator_names.is_empty() {
745            return Self::transition(
746                self.config,
747                self.validation_context,
748                merge_errors(
749                    self.errors,
750                    ConfigError::Parachain(id, ValidationError::CantBeEmpty().into()).into(),
751                ),
752            );
753        }
754
755        self.with_parachain(|parachain| {
756            let mut parachain_config = parachain.with_id(id).with_collator(|collator| {
757                collator
758                    .with_name(collator_names.first().unwrap_or(&"".to_string()))
759                    .validator(true)
760            });
761
762            for collator_name in collator_names.iter().skip(1) {
763                parachain_config = parachain_config
764                    .with_collator(|collator| collator.with_name(collator_name).validator(true));
765            }
766            parachain_config
767        })
768
769        // TODO: if need to set global settings and hrmp channels
770        // we can also do in here
771    }
772
773    /// Add an HRMP channel using a nested [`HrmpChannelConfigBuilder`].
774    pub fn with_hrmp_channel(
775        self,
776        f: impl FnOnce(
777            HrmpChannelConfigBuilder<hrmp_channel::Initial>,
778        ) -> HrmpChannelConfigBuilder<hrmp_channel::WithRecipient>,
779    ) -> Self {
780        let new_hrmp_channel = f(HrmpChannelConfigBuilder::new()).build();
781
782        Self::transition(
783            NetworkConfig {
784                hrmp_channels: [self.config.hrmp_channels, vec![new_hrmp_channel]].concat(),
785                ..self.config
786            },
787            self.validation_context,
788            self.errors,
789        )
790    }
791
792    /// Add a custom process using a nested [`CustomProcessBuilder`].
793    ///
794    /// Processes and nodes share one registry on the running network, so the
795    /// name must be free. Declare processes after the nodes: a node declared
796    /// later with a process's name is renamed (`<name>-1`), as it would be
797    /// after another node, where a process taking a node's name is an error.
798    pub fn with_custom_process(
799        self,
800        f: impl FnOnce(
801            CustomProcessBuilder<custom_process::WithOutName, custom_process::WithOutCmd>,
802        )
803            -> CustomProcessBuilder<custom_process::WithName, custom_process::WithCmd>,
804    ) -> Self {
805        match f(CustomProcessBuilder::new()).build() {
806            Ok(custom_process) => {
807                // Processes and nodes share one registry on the running network,
808                // so a process may not take a node's name. Recording it also keeps
809                // a node declared later from taking the process's.
810                let taken = !self
811                    .validation_context
812                    .borrow_mut()
813                    .used_nodes_names
814                    .insert(custom_process.name().to_string());
815                if taken {
816                    return Self::transition(
817                        self.config,
818                        self.validation_context,
819                        merge_errors(
820                            self.errors,
821                            ConfigError::CustomProcess(
822                                custom_process.name().to_string(),
823                                anyhow!("name: already used by a node or custom process"),
824                            )
825                            .into(),
826                        ),
827                    );
828                }
829                Self::transition(
830                    NetworkConfig {
831                        custom_processes: [self.config.custom_processes, vec![custom_process]]
832                            .concat(),
833                        ..self.config
834                    },
835                    self.validation_context,
836                    self.errors,
837                )
838            },
839            Err((name, errors)) => Self::transition(
840                self.config,
841                self.validation_context,
842                merge_errors_vecs(
843                    self.errors,
844                    errors
845                        .into_iter()
846                        .map(|error| ConfigError::CustomProcess(name.clone(), error).into())
847                        .collect::<Vec<_>>(),
848                ),
849            ),
850        }
851    }
852
853    /// Seals the builder and returns a [`NetworkConfig`] if there are no validation errors, else returns errors.
854    pub fn build(self) -> Result<NetworkConfig, Vec<anyhow::Error>> {
855        let mut paras_to_register: HashSet<ParaId> = Default::default();
856        let mut num_of_requested_cores = 0;
857
858        let mut errs: Vec<anyhow::Error> = self
859            .config
860            .parachains
861            .iter()
862            .filter_map(|para| {
863                if let Some(cores) = para.num_cores() {
864                    num_of_requested_cores += cores;
865                } else if let Some(RegistrationStrategy::InGenesis) = para.registration_strategy() {
866                    // add 1 by default
867                    num_of_requested_cores += 1;
868                }
869
870                if let Some(RegistrationStrategy::Manual) = para.registration_strategy() {
871                    return None;
872                };
873
874                if paras_to_register.insert(para.id()) {
875                    None
876                } else {
877                    // already in the set
878                    Some(anyhow!(
879                        "ParaId {} already set to be registered, only one should be.",
880                        para.id()
881                    ))
882                }
883            })
884            .collect();
885
886        if let Err(dependency_errors) =
887            custom_process::validate_custom_processes(&self.config.custom_processes)
888        {
889            errs.extend(dependency_errors.into_iter().map(|e| anyhow!(e)));
890        }
891
892        // ensure we can make this check
893        if let Some(rc_config) = self.config.relaychain.as_ref() {
894            // check we have num_validators >= num_requested_cores
895            let mut num_validators = rc_config.nodes().iter().fold(0u32, |mut acc, node| {
896                if node.is_validator {
897                    acc += 1;
898                }
899
900                acc
901            });
902
903            let node_groups = rc_config.group_node_configs();
904            for ng in &node_groups {
905                if ng.base_config.is_validator {
906                    num_validators += ng.count as u32;
907                }
908            }
909
910            if num_validators < num_of_requested_cores {
911                errs.push(anyhow!("Number of assigned cores at genesis {num_of_requested_cores} is greater than validators ({num_validators})"));
912            }
913        }
914
915        if !self.errors.is_empty() || !errs.is_empty() {
916            let mut ret_errs = self.errors;
917            ret_errs.append(&mut errs);
918            return Err(ret_errs);
919        }
920
921        Ok(self.config)
922    }
923}
924
925#[cfg(test)]
926mod tests {
927    use std::path::PathBuf;
928
929    use super::*;
930    use crate::{parachain::RegistrationStrategy, types::JamNodeMode, DependencyCondition};
931
932    #[test]
933    fn network_config_builder_should_succeeds_and_returns_a_network_config() {
934        let network_config = NetworkConfigBuilder::new()
935            .with_relaychain(|relaychain| {
936                relaychain
937                    .with_chain("polkadot")
938                    .with_random_nominators_count(10)
939                    .with_validator(|node| node.with_name("node").with_command("command"))
940                    .with_validator(|node| node.with_name("node-1").with_command("command"))
941            })
942            .with_parachain(|parachain| {
943                parachain
944                    .with_id(1)
945                    .with_chain("myparachain1")
946                    .with_initial_balance(100_000)
947                    .with_collator(|collator| {
948                        collator
949                            .with_name("collator1")
950                            .with_command("command1")
951                            .validator(true)
952                    })
953            })
954            .with_parachain(|parachain| {
955                parachain
956                    .with_id(2)
957                    .with_chain("myparachain2")
958                    .with_initial_balance(0)
959                    .with_collator(|collator| {
960                        collator
961                            .with_name("collator2")
962                            .with_command("command2")
963                            .validator(true)
964                    })
965            })
966            .with_hrmp_channel(|hrmp_channel1| {
967                hrmp_channel1
968                    .with_sender(1)
969                    .with_recipient(2)
970                    .with_max_capacity(200)
971                    .with_max_message_size(500)
972            })
973            .with_hrmp_channel(|hrmp_channel2| {
974                hrmp_channel2
975                    .with_sender(2)
976                    .with_recipient(1)
977                    .with_max_capacity(100)
978                    .with_max_message_size(250)
979            })
980            .with_global_settings(|global_settings| {
981                global_settings
982                    .with_network_spawn_timeout(1200)
983                    .with_node_spawn_timeout(240)
984            })
985            .build()
986            .unwrap();
987
988        // relaychain
989        assert_eq!(network_config.relaychain().chain().as_str(), "polkadot");
990        assert_eq!(network_config.relaychain().nodes().len(), 2);
991        let &node = network_config.relaychain().nodes().first().unwrap();
992        assert_eq!(node.name(), "node");
993        assert_eq!(node.command().unwrap().as_str(), "command");
994        assert!(node.is_validator());
995        assert_eq!(
996            network_config
997                .relaychain()
998                .random_nominators_count()
999                .unwrap(),
1000            10
1001        );
1002
1003        // parachains
1004        assert_eq!(network_config.parachains().len(), 2);
1005
1006        // parachain1
1007        let &parachain1 = network_config.parachains().first().unwrap();
1008        assert_eq!(parachain1.id(), 1);
1009        assert_eq!(parachain1.collators().len(), 1);
1010        let &collator = parachain1.collators().first().unwrap();
1011        assert_eq!(collator.name(), "collator1");
1012        assert_eq!(collator.command().unwrap().as_str(), "command1");
1013        assert!(collator.is_validator());
1014        assert_eq!(parachain1.initial_balance(), 100_000);
1015        assert_eq!(parachain1.unique_id(), "1");
1016
1017        // parachain2
1018        let &parachain2 = network_config.parachains().last().unwrap();
1019        assert_eq!(parachain2.id(), 2);
1020        assert_eq!(parachain2.collators().len(), 1);
1021        let &collator = parachain2.collators().first().unwrap();
1022        assert_eq!(collator.name(), "collator2");
1023        assert_eq!(collator.command().unwrap().as_str(), "command2");
1024        assert!(collator.is_validator());
1025        assert_eq!(parachain2.initial_balance(), 0);
1026
1027        // hrmp_channels
1028        assert_eq!(network_config.hrmp_channels().len(), 2);
1029
1030        // hrmp_channel1
1031        let &hrmp_channel1 = network_config.hrmp_channels().first().unwrap();
1032        assert_eq!(hrmp_channel1.sender(), 1);
1033        assert_eq!(hrmp_channel1.recipient(), 2);
1034        assert_eq!(hrmp_channel1.max_capacity(), 200);
1035        assert_eq!(hrmp_channel1.max_message_size(), 500);
1036
1037        // hrmp_channel2
1038        let &hrmp_channel2 = network_config.hrmp_channels().last().unwrap();
1039        assert_eq!(hrmp_channel2.sender(), 2);
1040        assert_eq!(hrmp_channel2.recipient(), 1);
1041        assert_eq!(hrmp_channel2.max_capacity(), 100);
1042        assert_eq!(hrmp_channel2.max_message_size(), 250);
1043
1044        // global settings
1045        assert_eq!(
1046            network_config.global_settings().network_spawn_timeout(),
1047            1200
1048        );
1049        assert_eq!(network_config.global_settings().node_spawn_timeout(), 240);
1050        assert!(network_config.global_settings().tear_down_on_failure());
1051    }
1052
1053    #[test]
1054    fn network_config_builder_should_fails_and_returns_multiple_errors_if_relaychain_is_invalid() {
1055        let errors = NetworkConfigBuilder::new()
1056            .with_relaychain(|relaychain| {
1057                relaychain
1058                    .with_chain("polkadot")
1059                    .with_random_nominators_count(10)
1060                    .with_default_image("invalid.image")
1061                    .with_validator(|node| node.with_name("node").with_command("invalid command"))
1062            })
1063            .with_parachain(|parachain| {
1064                parachain
1065                    .with_id(1)
1066                    .with_chain("myparachain")
1067                    .with_initial_balance(100_000)
1068                    .with_collator(|collator| {
1069                        collator
1070                            .with_name("collator1")
1071                            .with_command("command1")
1072                            .validator(true)
1073                    })
1074            })
1075            .build()
1076            .unwrap_err();
1077
1078        assert_eq!(errors.len(), 2);
1079        assert_eq!(
1080            errors.first().unwrap().to_string(),
1081            "relaychain.default_image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1082        );
1083        assert_eq!(
1084            errors.get(1).unwrap().to_string(),
1085            "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
1086        );
1087    }
1088
1089    #[test]
1090    fn network_config_builder_should_fails_and_returns_multiple_errors_if_parachain_is_invalid() {
1091        let errors = NetworkConfigBuilder::new()
1092            .with_relaychain(|relaychain| {
1093                relaychain
1094                    .with_chain("polkadot")
1095                    .with_random_nominators_count(10)
1096                    .with_validator(|node| node.with_name("node").with_command("command"))
1097            })
1098            .with_parachain(|parachain| {
1099                parachain
1100                    .with_id(1000)
1101                    .with_chain("myparachain")
1102                    .with_initial_balance(100_000)
1103                    .with_collator(|collator| {
1104                        collator
1105                            .with_name("collator1")
1106                            .with_command("invalid command")
1107                            .with_image("invalid.image")
1108                            .validator(true)
1109                    })
1110            })
1111            .build()
1112            .unwrap_err();
1113
1114        assert_eq!(errors.len(), 2);
1115        assert_eq!(
1116            errors.first().unwrap().to_string(),
1117            "parachain[1000].collators['collator1'].command: 'invalid command' shouldn't contains whitespace"
1118        );
1119        assert_eq!(
1120            errors.get(1).unwrap().to_string(),
1121            "parachain[1000].collators['collator1'].image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1122        );
1123    }
1124
1125    #[test]
1126    fn network_config_builder_should_fails_and_returns_multiple_errors_if_multiple_parachains_are_invalid(
1127    ) {
1128        let errors = NetworkConfigBuilder::new()
1129            .with_relaychain(|relaychain| {
1130                relaychain
1131                    .with_chain("polkadot")
1132                    .with_random_nominators_count(10)
1133                    .with_validator(|node| node.with_name("node").with_command("command"))
1134            })
1135            .with_parachain(|parachain| {
1136                parachain
1137                    .with_id(1000)
1138                    .with_chain("myparachain1")
1139                    .with_initial_balance(100_000)
1140                    .with_collator(|collator| {
1141                        collator
1142                            .with_name("collator1")
1143                            .with_command("invalid command")
1144                    })
1145            })
1146            .with_parachain(|parachain| {
1147                parachain
1148                    .with_id(2000)
1149                    .with_chain("myparachain2")
1150                    .with_initial_balance(100_000)
1151                    .with_collator(|collator| {
1152                        collator
1153                            .with_name("collator2")
1154                            .validator(true)
1155                            .with_resources(|resources| {
1156                                resources
1157                                    .with_limit_cpu("1000m")
1158                                    .with_request_memory("1Gi")
1159                                    .with_request_cpu("invalid")
1160                            })
1161                    })
1162            })
1163            .build()
1164            .unwrap_err();
1165
1166        assert_eq!(errors.len(), 2);
1167        assert_eq!(
1168            errors.first().unwrap().to_string(),
1169            "parachain[1000].collators['collator1'].command: 'invalid command' shouldn't contains whitespace"
1170        );
1171        assert_eq!(
1172            errors.get(1).unwrap().to_string(),
1173            "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)?$'"
1174        );
1175    }
1176
1177    #[test]
1178    fn network_config_builder_should_fails_and_returns_multiple_errors_if_global_settings_is_invalid(
1179    ) {
1180        let errors = NetworkConfigBuilder::new()
1181            .with_relaychain(|relaychain| {
1182                relaychain
1183                    .with_chain("polkadot")
1184                    .with_random_nominators_count(10)
1185                    .with_validator(|node| node.with_name("node").with_command("command"))
1186            })
1187            .with_parachain(|parachain| {
1188                parachain
1189                    .with_id(1000)
1190                    .with_chain("myparachain")
1191                    .with_initial_balance(100_000)
1192                    .with_collator(|collator| {
1193                        collator
1194                            .with_name("collator")
1195                            .with_command("command")
1196                            .validator(true)
1197                    })
1198            })
1199            .with_global_settings(|global_settings| {
1200                global_settings
1201                    .with_local_ip("127.0.0000.1")
1202                    .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
1203            })
1204            .build()
1205            .unwrap_err();
1206
1207        assert_eq!(errors.len(), 2);
1208        assert_eq!(
1209            errors.first().unwrap().to_string(),
1210            "global_settings.local_ip: invalid IP address syntax"
1211        );
1212        assert_eq!(
1213            errors.get(1).unwrap().to_string(),
1214            "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1215        );
1216    }
1217
1218    #[test]
1219    fn network_config_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid(
1220    ) {
1221        let errors = NetworkConfigBuilder::new()
1222            .with_relaychain(|relaychain| {
1223                relaychain
1224                    .with_chain("polkadot")
1225                    .with_random_nominators_count(10)
1226                    .with_validator(|node| node.with_name("node").with_command("invalid command"))
1227            })
1228            .with_parachain(|parachain| {
1229                parachain
1230                    .with_id(1000)
1231                    .with_chain("myparachain")
1232                    .with_initial_balance(100_000)
1233                    .with_collator(|collator| {
1234                        collator
1235                            .with_name("collator")
1236                            .with_command("command")
1237                            .with_image("invalid.image")
1238                    })
1239            })
1240            .with_global_settings(|global_settings| global_settings.with_local_ip("127.0.0000.1"))
1241            .build()
1242            .unwrap_err();
1243
1244        assert_eq!(errors.len(), 3);
1245        assert_eq!(
1246            errors.first().unwrap().to_string(),
1247            "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
1248        );
1249        assert_eq!(
1250            errors.get(1).unwrap().to_string(),
1251            "parachain[1000].collators['collator'].image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1252        );
1253        assert_eq!(
1254            errors.get(2).unwrap().to_string(),
1255            "global_settings.local_ip: invalid IP address syntax"
1256        );
1257    }
1258
1259    #[test]
1260    fn network_config_should_be_dumpable_to_a_toml_config_for_a_small_network() {
1261        let network_config = NetworkConfigBuilder::new()
1262            .with_relaychain(|relaychain| {
1263                relaychain
1264                    .with_chain("rococo-local")
1265                    .with_default_command("polkadot")
1266                    .with_default_image("docker.io/parity/polkadot:latest")
1267                    .with_default_args(vec![("-lparachain", "debug").into()])
1268                    .with_validator(|node| node.with_name("alice"))
1269                    .with_validator(|node| {
1270                        node.with_name("bob")
1271                            .invulnerable(false)
1272                            .bootnode(true)
1273                            .with_args(vec![("--database", "paritydb-experimental").into()])
1274                    })
1275            })
1276            .build()
1277            .unwrap();
1278
1279        let got = network_config.dump_to_toml().unwrap();
1280        let expected = fs::read_to_string("./testing/snapshots/0000-small-network.toml").unwrap();
1281        assert_eq!(got, expected);
1282    }
1283
1284    #[test]
1285    fn network_config_should_be_dumpable_to_a_toml_config_for_a_big_network() {
1286        let network_config = NetworkConfigBuilder::new()
1287            .with_relaychain(|relaychain| {
1288                relaychain
1289                    .with_chain("polkadot")
1290                    .with_default_command("polkadot")
1291                    .with_default_image("docker.io/parity/polkadot:latest")
1292                    .with_default_resources(|resources| {
1293                        resources
1294                            .with_request_cpu(100000)
1295                            .with_request_memory("500M")
1296                            .with_limit_cpu("10Gi")
1297                            .with_limit_memory("4000M")
1298                    })
1299                    .with_validator(|node| {
1300                        node.with_name("alice")
1301                            .with_initial_balance(1_000_000_000)
1302                            .bootnode(true)
1303                            .invulnerable(true)
1304                    })
1305                    .with_validator(|node| node.with_name("bob").invulnerable(true).bootnode(true))
1306            })
1307            .with_parachain(|parachain| {
1308                parachain
1309                    .with_id(1000)
1310                    .with_chain("myparachain")
1311                    .with_chain_spec_path("/path/to/my/chain/spec.json")
1312                    .with_registration_strategy(RegistrationStrategy::UsingExtrinsic)
1313                    .onboard_as_parachain(false)
1314                    .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz")
1315                    .with_collator(|collator| {
1316                        collator
1317                            .with_name("john")
1318                            .bootnode(true)
1319                            .invulnerable(true)
1320                            .with_initial_balance(5_000_000_000)
1321                    })
1322                    .with_fullnode(|collator| {
1323                        collator
1324                            .with_name("charles")
1325                            .bootnode(true)
1326                            .invulnerable(true)
1327                            .with_initial_balance(0)
1328                    })
1329                    .with_collator(|collator| {
1330                        collator
1331                            .with_name("frank")
1332                            .invulnerable(false)
1333                            .bootnode(true)
1334                            .with_initial_balance(1_000_000_000)
1335                    })
1336            })
1337            .with_parachain(|parachain| {
1338                parachain
1339                    .with_id(2000)
1340                    .with_chain("myotherparachain")
1341                    .with_chain_spec_path("/path/to/my/other/chain/spec.json")
1342                    .with_collator(|collator| {
1343                        collator
1344                            .with_name("mike")
1345                            .bootnode(true)
1346                            .invulnerable(true)
1347                            .with_initial_balance(5_000_000_000)
1348                    })
1349                    .with_fullnode(|collator| {
1350                        collator
1351                            .with_name("georges")
1352                            .bootnode(true)
1353                            .invulnerable(true)
1354                            .with_initial_balance(0)
1355                    })
1356                    .with_collator(|collator| {
1357                        collator
1358                            .with_name("victor")
1359                            .invulnerable(false)
1360                            .bootnode(true)
1361                            .with_initial_balance(1_000_000_000)
1362                    })
1363            })
1364            .with_hrmp_channel(|hrmp_channel| {
1365                hrmp_channel
1366                    .with_sender(1000)
1367                    .with_recipient(2000)
1368                    .with_max_capacity(150)
1369                    .with_max_message_size(5000)
1370            })
1371            .with_hrmp_channel(|hrmp_channel| {
1372                hrmp_channel
1373                    .with_sender(2000)
1374                    .with_recipient(1000)
1375                    .with_max_capacity(200)
1376                    .with_max_message_size(8000)
1377            })
1378            .with_custom_process(|c| c.with_name("eth-rpc").with_command("eth-rpc"))
1379            .build()
1380            .unwrap();
1381
1382        let got = network_config.dump_to_toml().unwrap();
1383        let expected = fs::read_to_string("./testing/snapshots/0001-big-network.toml").unwrap();
1384        assert_eq!(got, expected);
1385    }
1386
1387    #[test]
1388    fn network_config_builder_should_be_dumplable_to_a_toml_config_a_overrides_default_correctly() {
1389        let network_config = NetworkConfigBuilder::new()
1390            .with_relaychain(|relaychain| {
1391                relaychain
1392                    .with_chain("polkadot")
1393                    .with_default_command("polkadot")
1394                    .with_default_image("docker.io/parity/polkadot:latest")
1395                    .with_default_args(vec![("-name", "value").into(), "--flag".into()])
1396                    .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz")
1397                    .with_default_resources(|resources| {
1398                        resources
1399                            .with_request_cpu(100000)
1400                            .with_request_memory("500M")
1401                            .with_limit_cpu("10Gi")
1402                            .with_limit_memory("4000M")
1403                    })
1404                    .with_validator(|node| {
1405                        node.with_name("alice")
1406                            .with_initial_balance(1_000_000_000)
1407                            .bootnode(true)
1408                            .invulnerable(true)
1409                    })
1410                    .with_validator(|node| {
1411                        node.with_name("bob")
1412                            .invulnerable(true)
1413                            .bootnode(true)
1414                            .with_image("mycustomimage:latest")
1415                            .with_command("my-custom-command")
1416                            .with_db_snapshot("https://storage.com/path/to/other/db_snapshot.tgz")
1417                            .with_resources(|resources| {
1418                                resources
1419                                    .with_request_cpu(1000)
1420                                    .with_request_memory("250Mi")
1421                                    .with_limit_cpu("5Gi")
1422                                    .with_limit_memory("2Gi")
1423                            })
1424                            .with_args(vec![("-myothername", "value").into()])
1425                    })
1426            })
1427            .with_parachain(|parachain| {
1428                parachain
1429                    .with_id(1000)
1430                    .with_chain("myparachain")
1431                    .with_chain_spec_path("/path/to/my/chain/spec.json")
1432                    .with_default_db_snapshot("https://storage.com/path/to/other_snapshot.tgz")
1433                    .with_default_command("my-default-command")
1434                    .with_default_image("mydefaultimage:latest")
1435                    .with_collator(|collator| {
1436                        collator
1437                            .with_name("john")
1438                            .bootnode(true)
1439                            .invulnerable(true)
1440                            .with_initial_balance(5_000_000_000)
1441                            .with_command("my-non-default-command")
1442                            .with_image("anotherimage:latest")
1443                    })
1444                    .with_fullnode(|collator| {
1445                        collator
1446                            .with_name("charles")
1447                            .bootnode(true)
1448                            .invulnerable(true)
1449                            .with_initial_balance(0)
1450                    })
1451            })
1452            .build()
1453            .unwrap();
1454
1455        let got = network_config.dump_to_toml().unwrap();
1456        let expected =
1457            fs::read_to_string("./testing/snapshots/0002-overridden-defaults.toml").unwrap();
1458        assert_eq!(got, expected);
1459    }
1460
1461    #[test]
1462    fn the_toml_config_with_custom_settings() {
1463        let settings = GlobalSettingsBuilder::new()
1464            .with_base_dir("/tmp/test-demo")
1465            .build()
1466            .unwrap();
1467
1468        let load_from_toml = NetworkConfig::load_from_toml_with_settings(
1469            "./testing/snapshots/0000-small-network.toml",
1470            &settings,
1471        )
1472        .unwrap();
1473
1474        assert_eq!(
1475            Some(PathBuf::from("/tmp/test-demo").as_path()),
1476            load_from_toml.global_settings.base_dir()
1477        );
1478    }
1479
1480    #[test]
1481    fn the_toml_config_should_be_imported_and_match_a_network() {
1482        let load_from_toml =
1483            NetworkConfig::load_from_toml("./testing/snapshots/0000-small-network.toml").unwrap();
1484
1485        let expected = NetworkConfigBuilder::new()
1486            .with_relaychain(|relaychain| {
1487                relaychain
1488                    .with_chain("rococo-local")
1489                    .with_default_command("polkadot")
1490                    .with_default_image("docker.io/parity/polkadot:latest")
1491                    .with_default_args(vec![("-lparachain=debug").into()])
1492                    .with_validator(|node| {
1493                        node.with_name("alice")
1494                            .validator(true)
1495                            .invulnerable(true)
1496                            .bootnode(false)
1497                            .with_initial_balance(2000000000000)
1498                    })
1499                    .with_validator(|node| {
1500                        node.with_name("bob")
1501                            .with_args(vec![("--database", "paritydb-experimental").into()])
1502                            .invulnerable(false)
1503                            .bootnode(true)
1504                            .with_initial_balance(2000000000000)
1505                    })
1506            })
1507            .build()
1508            .unwrap();
1509
1510        // We need to assert parts of the network config separately because the expected one contains the chain default context which
1511        // is used for dumbing to tomp while the
1512        // while loaded
1513        assert_eq!(
1514            expected.relaychain().chain(),
1515            load_from_toml.relaychain().chain()
1516        );
1517        assert_eq!(
1518            expected.relaychain().default_args(),
1519            load_from_toml.relaychain().default_args()
1520        );
1521        assert_eq!(
1522            expected.relaychain().default_command(),
1523            load_from_toml.relaychain().default_command()
1524        );
1525        assert_eq!(
1526            expected.relaychain().default_image(),
1527            load_from_toml.relaychain().default_image()
1528        );
1529
1530        // Check the nodes without the Chain Default Context
1531        expected
1532            .relaychain()
1533            .nodes()
1534            .iter()
1535            .zip(load_from_toml.relaychain().nodes().iter())
1536            .for_each(|(expected_node, loaded_node)| {
1537                assert_eq!(expected_node.name(), loaded_node.name());
1538                assert_eq!(expected_node.command(), loaded_node.command());
1539                assert_eq!(expected_node.args(), loaded_node.args());
1540                assert_eq!(
1541                    expected_node.is_invulnerable(),
1542                    loaded_node.is_invulnerable()
1543                );
1544                assert_eq!(expected_node.is_validator(), loaded_node.is_validator());
1545                assert_eq!(expected_node.is_bootnode(), loaded_node.is_bootnode());
1546                assert_eq!(
1547                    expected_node.initial_balance(),
1548                    loaded_node.initial_balance()
1549                );
1550            });
1551    }
1552
1553    #[test]
1554    fn the_toml_config_without_settings_should_be_imported_and_match_a_network() {
1555        let load_from_toml = NetworkConfig::load_from_toml(
1556            "./testing/snapshots/0004-small-network-without-settings.toml",
1557        )
1558        .unwrap();
1559
1560        let expected = NetworkConfigBuilder::new()
1561            .with_relaychain(|relaychain| {
1562                relaychain
1563                    .with_chain("rococo-local")
1564                    .with_default_command("polkadot")
1565                    .with_validator(|node| node.with_name("alice"))
1566                    .with_validator(|node| node.with_name("bob"))
1567            })
1568            .build()
1569            .unwrap();
1570
1571        assert_eq!(
1572            load_from_toml.global_settings().network_spawn_timeout(),
1573            expected.global_settings().network_spawn_timeout()
1574        )
1575    }
1576
1577    #[test]
1578    fn the_toml_config_should_be_imported_and_match_a_network_with_parachains() {
1579        let load_from_toml =
1580            NetworkConfig::load_from_toml("./testing/snapshots/0001-big-network.toml").unwrap();
1581
1582        let expected = NetworkConfigBuilder::new()
1583            .with_relaychain(|relaychain| {
1584                relaychain
1585                    .with_chain("polkadot")
1586                    .with_default_command("polkadot")
1587                    .with_default_image("docker.io/parity/polkadot:latest")
1588                    .with_default_resources(|resources| {
1589                        resources
1590                            .with_request_cpu(100000)
1591                            .with_request_memory("500M")
1592                            .with_limit_cpu("10Gi")
1593                            .with_limit_memory("4000M")
1594                    })
1595                    .with_validator(|node| {
1596                        node.with_name("alice")
1597                            .with_initial_balance(1_000_000_000)
1598                            .bootnode(true)
1599                            .invulnerable(true)
1600                    })
1601                    .with_validator(|node| node.with_name("bob").invulnerable(true).bootnode(true))
1602            })
1603            .with_parachain(|parachain| {
1604                parachain
1605                    .with_id(1000)
1606                    .with_chain("myparachain")
1607                    .with_chain_spec_path("/path/to/my/chain/spec.json")
1608                    .with_registration_strategy(RegistrationStrategy::UsingExtrinsic)
1609                    .onboard_as_parachain(false)
1610                    .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz")
1611                    .with_collator(|collator| {
1612                        collator
1613                            .with_name("john")
1614                            .bootnode(true)
1615                            .invulnerable(true)
1616                            .with_initial_balance(5_000_000_000)
1617                    })
1618                    .with_fullnode(|collator| {
1619                        collator
1620                            .with_name("charles")
1621                            .bootnode(true)
1622                            .invulnerable(true)
1623                            .with_initial_balance(0)
1624                    })
1625                    .with_collator(|collator| {
1626                        collator
1627                            .with_name("frank")
1628                            .invulnerable(false)
1629                            .bootnode(true)
1630                            .with_initial_balance(1_000_000_000)
1631                    })
1632            })
1633            .with_parachain(|parachain| {
1634                parachain
1635                    .with_id(2000)
1636                    .with_chain("myotherparachain")
1637                    .with_chain_spec_path("/path/to/my/other/chain/spec.json")
1638                    .with_collator(|collator| {
1639                        collator
1640                            .with_name("mike")
1641                            .bootnode(true)
1642                            .invulnerable(true)
1643                            .with_initial_balance(5_000_000_000)
1644                    })
1645                    .with_fullnode(|collator| {
1646                        collator
1647                            .with_name("georges")
1648                            .bootnode(true)
1649                            .invulnerable(true)
1650                            .with_initial_balance(0)
1651                    })
1652                    .with_collator(|collator| {
1653                        collator
1654                            .with_name("victor")
1655                            .invulnerable(false)
1656                            .bootnode(true)
1657                            .with_initial_balance(1_000_000_000)
1658                    })
1659            })
1660            .with_hrmp_channel(|hrmp_channel| {
1661                hrmp_channel
1662                    .with_sender(1000)
1663                    .with_recipient(2000)
1664                    .with_max_capacity(150)
1665                    .with_max_message_size(5000)
1666            })
1667            .with_hrmp_channel(|hrmp_channel| {
1668                hrmp_channel
1669                    .with_sender(2000)
1670                    .with_recipient(1000)
1671                    .with_max_capacity(200)
1672                    .with_max_message_size(8000)
1673            })
1674            .build()
1675            .unwrap();
1676
1677        // Check the relay chain
1678        assert_eq!(
1679            expected.relaychain().default_resources(),
1680            load_from_toml.relaychain().default_resources()
1681        );
1682
1683        // Check the nodes without the Chain Default Context
1684        expected
1685            .relaychain()
1686            .nodes()
1687            .iter()
1688            .zip(load_from_toml.relaychain().nodes().iter())
1689            .for_each(|(expected_node, loaded_node)| {
1690                assert_eq!(expected_node.name(), loaded_node.name());
1691                assert_eq!(expected_node.command(), loaded_node.command());
1692                assert_eq!(expected_node.args(), loaded_node.args());
1693                assert_eq!(expected_node.is_validator(), loaded_node.is_validator());
1694                assert_eq!(expected_node.is_bootnode(), loaded_node.is_bootnode());
1695                assert_eq!(
1696                    expected_node.initial_balance(),
1697                    loaded_node.initial_balance()
1698                );
1699                assert_eq!(
1700                    expected_node.is_invulnerable(),
1701                    loaded_node.is_invulnerable()
1702                );
1703            });
1704
1705        expected
1706            .parachains()
1707            .iter()
1708            .zip(load_from_toml.parachains().iter())
1709            .for_each(|(expected_parachain, loaded_parachain)| {
1710                assert_eq!(expected_parachain.id(), loaded_parachain.id());
1711                assert_eq!(expected_parachain.chain(), loaded_parachain.chain());
1712                assert_eq!(
1713                    expected_parachain.chain_spec_path(),
1714                    loaded_parachain.chain_spec_path()
1715                );
1716                assert_eq!(
1717                    expected_parachain.registration_strategy(),
1718                    loaded_parachain.registration_strategy()
1719                );
1720                assert_eq!(
1721                    expected_parachain.onboard_as_parachain(),
1722                    loaded_parachain.onboard_as_parachain()
1723                );
1724                assert_eq!(
1725                    expected_parachain.default_db_snapshot(),
1726                    loaded_parachain.default_db_snapshot()
1727                );
1728                assert_eq!(
1729                    expected_parachain.default_command(),
1730                    loaded_parachain.default_command()
1731                );
1732                assert_eq!(
1733                    expected_parachain.default_image(),
1734                    loaded_parachain.default_image()
1735                );
1736                assert_eq!(
1737                    expected_parachain.collators().len(),
1738                    loaded_parachain.collators().len()
1739                );
1740                expected_parachain
1741                    .collators()
1742                    .iter()
1743                    .zip(loaded_parachain.collators().iter())
1744                    .for_each(|(expected_collator, loaded_collator)| {
1745                        assert_eq!(expected_collator.name(), loaded_collator.name());
1746                        assert_eq!(expected_collator.command(), loaded_collator.command());
1747                        assert_eq!(expected_collator.image(), loaded_collator.image());
1748                        assert_eq!(
1749                            expected_collator.is_validator(),
1750                            loaded_collator.is_validator()
1751                        );
1752                        assert_eq!(
1753                            expected_collator.is_bootnode(),
1754                            loaded_collator.is_bootnode()
1755                        );
1756                        assert_eq!(
1757                            expected_collator.is_invulnerable(),
1758                            loaded_collator.is_invulnerable()
1759                        );
1760                        assert_eq!(
1761                            expected_collator.initial_balance(),
1762                            loaded_collator.initial_balance()
1763                        );
1764                    });
1765            });
1766
1767        expected
1768            .hrmp_channels()
1769            .iter()
1770            .zip(load_from_toml.hrmp_channels().iter())
1771            .for_each(|(expected_hrmp_channel, loaded_hrmp_channel)| {
1772                assert_eq!(expected_hrmp_channel.sender(), loaded_hrmp_channel.sender());
1773                assert_eq!(
1774                    expected_hrmp_channel.recipient(),
1775                    loaded_hrmp_channel.recipient()
1776                );
1777                assert_eq!(
1778                    expected_hrmp_channel.max_capacity(),
1779                    loaded_hrmp_channel.max_capacity()
1780                );
1781                assert_eq!(
1782                    expected_hrmp_channel.max_message_size(),
1783                    loaded_hrmp_channel.max_message_size()
1784                );
1785            });
1786    }
1787
1788    #[test]
1789    fn the_toml_config_should_be_imported_and_match_a_network_with_overriden_defaults() {
1790        let load_from_toml =
1791            NetworkConfig::load_from_toml("./testing/snapshots/0002-overridden-defaults.toml")
1792                .unwrap();
1793
1794        let expected = NetworkConfigBuilder::new()
1795            .with_relaychain(|relaychain| {
1796                relaychain
1797                    .with_chain("polkadot")
1798                    .with_default_command("polkadot")
1799                    .with_default_image("docker.io/parity/polkadot:latest")
1800                    .with_default_args(vec![("-name", "value").into(), "--flag".into()])
1801                    .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz")
1802                    .with_default_resources(|resources| {
1803                        resources
1804                            .with_request_cpu(100000)
1805                            .with_request_memory("500M")
1806                            .with_limit_cpu("10Gi")
1807                            .with_limit_memory("4000M")
1808                    })
1809                    .with_validator(|node| {
1810                        node.with_name("alice")
1811                            .with_initial_balance(1_000_000_000)
1812                            .bootnode(true)
1813                            .invulnerable(true)
1814                    })
1815                    .with_validator(|node| {
1816                        node.with_name("bob")
1817                            .invulnerable(true)
1818                            .bootnode(true)
1819                            .with_image("mycustomimage:latest")
1820                            .with_command("my-custom-command")
1821                            .with_db_snapshot("https://storage.com/path/to/other/db_snapshot.tgz")
1822                            .with_resources(|resources| {
1823                                resources
1824                                    .with_request_cpu(1000)
1825                                    .with_request_memory("250Mi")
1826                                    .with_limit_cpu("5Gi")
1827                                    .with_limit_memory("2Gi")
1828                            })
1829                            .with_args(vec![("-myothername", "value").into()])
1830                    })
1831            })
1832            .with_parachain(|parachain| {
1833                parachain
1834                    .with_id(1000)
1835                    .with_chain("myparachain")
1836                    .with_chain_spec_path("/path/to/my/chain/spec.json")
1837                    .with_default_db_snapshot("https://storage.com/path/to/other_snapshot.tgz")
1838                    .with_default_command("my-default-command")
1839                    .with_default_image("mydefaultimage:latest")
1840                    .with_collator(|collator| {
1841                        collator
1842                            .with_name("john")
1843                            .bootnode(true)
1844                            .validator(true)
1845                            .invulnerable(true)
1846                            .with_initial_balance(5_000_000_000)
1847                            .with_command("my-non-default-command")
1848                            .with_image("anotherimage:latest")
1849                    })
1850                    .with_fullnode(|collator| {
1851                        collator
1852                            .with_name("charles")
1853                            .bootnode(true)
1854                            .invulnerable(true)
1855                            .with_initial_balance(0)
1856                    })
1857            })
1858            .build()
1859            .unwrap();
1860
1861        expected
1862            .parachains()
1863            .iter()
1864            .zip(load_from_toml.parachains().iter())
1865            .for_each(|(expected_parachain, loaded_parachain)| {
1866                assert_eq!(expected_parachain.id(), loaded_parachain.id());
1867                assert_eq!(expected_parachain.chain(), loaded_parachain.chain());
1868                assert_eq!(
1869                    expected_parachain.chain_spec_path(),
1870                    loaded_parachain.chain_spec_path()
1871                );
1872                assert_eq!(
1873                    expected_parachain.registration_strategy(),
1874                    loaded_parachain.registration_strategy()
1875                );
1876                assert_eq!(
1877                    expected_parachain.onboard_as_parachain(),
1878                    loaded_parachain.onboard_as_parachain()
1879                );
1880                assert_eq!(
1881                    expected_parachain.default_db_snapshot(),
1882                    loaded_parachain.default_db_snapshot()
1883                );
1884                assert_eq!(
1885                    expected_parachain.default_command(),
1886                    loaded_parachain.default_command()
1887                );
1888                assert_eq!(
1889                    expected_parachain.default_image(),
1890                    loaded_parachain.default_image()
1891                );
1892                assert_eq!(
1893                    expected_parachain.collators().len(),
1894                    loaded_parachain.collators().len()
1895                );
1896                expected_parachain
1897                    .collators()
1898                    .iter()
1899                    .zip(loaded_parachain.collators().iter())
1900                    .for_each(|(expected_collator, loaded_collator)| {
1901                        assert_eq!(expected_collator.name(), loaded_collator.name());
1902                        assert_eq!(expected_collator.command(), loaded_collator.command());
1903                        assert_eq!(expected_collator.image(), loaded_collator.image());
1904                        assert_eq!(
1905                            expected_collator.is_validator(),
1906                            loaded_collator.is_validator()
1907                        );
1908                        assert_eq!(
1909                            expected_collator.is_bootnode(),
1910                            loaded_collator.is_bootnode()
1911                        );
1912                        assert_eq!(
1913                            expected_collator.is_invulnerable(),
1914                            loaded_collator.is_invulnerable()
1915                        );
1916                        assert_eq!(
1917                            expected_collator.initial_balance(),
1918                            loaded_collator.initial_balance()
1919                        );
1920                    });
1921            });
1922    }
1923
1924    #[test]
1925    fn with_chain_and_nodes_works() {
1926        let network_config = NetworkConfigBuilder::with_chain_and_nodes(
1927            "rococo-local",
1928            vec!["alice".to_string(), "bob".to_string()],
1929        )
1930        .build()
1931        .unwrap();
1932
1933        // relaychain
1934        assert_eq!(network_config.relaychain().chain().as_str(), "rococo-local");
1935        assert_eq!(network_config.relaychain().nodes().len(), 2);
1936        let mut node_names = network_config.relaychain().nodes().into_iter();
1937        let node1 = node_names.next().unwrap().name();
1938        assert_eq!(node1, "alice");
1939        let node2 = node_names.next().unwrap().name();
1940        assert_eq!(node2, "bob");
1941
1942        // parachains
1943        assert_eq!(network_config.parachains().len(), 0);
1944    }
1945
1946    #[test]
1947    fn with_chain_and_nodes_should_fail_with_empty_relay_name() {
1948        let errors = NetworkConfigBuilder::with_chain_and_nodes("", vec!["alice".to_string()])
1949            .build()
1950            .unwrap_err();
1951
1952        assert_eq!(
1953            errors.first().unwrap().to_string(),
1954            "relaychain.chain: can't be empty"
1955        );
1956    }
1957
1958    #[test]
1959    fn with_chain_and_nodes_should_fail_with_empty_node_list() {
1960        let errors = NetworkConfigBuilder::with_chain_and_nodes("rococo-local", vec![])
1961            .build()
1962            .unwrap_err();
1963
1964        assert_eq!(
1965            errors.first().unwrap().to_string(),
1966            "relaychain.nodes[''].name: can't be empty"
1967        );
1968    }
1969
1970    #[test]
1971    fn with_chain_and_nodes_should_fail_with_empty_node_name() {
1972        let errors = NetworkConfigBuilder::with_chain_and_nodes(
1973            "rococo-local",
1974            vec!["alice".to_string(), "".to_string()],
1975        )
1976        .build()
1977        .unwrap_err();
1978
1979        assert_eq!(
1980            errors.first().unwrap().to_string(),
1981            "relaychain.nodes[''].name: can't be empty"
1982        );
1983    }
1984
1985    #[test]
1986    fn with_parachain_id_and_collators_works() {
1987        let network_config = NetworkConfigBuilder::with_chain_and_nodes(
1988            "rococo-local",
1989            vec!["alice".to_string(), "bob".to_string()],
1990        )
1991        .with_parachain_id_and_collators(
1992            100,
1993            vec!["collator1".to_string(), "collator2".to_string()],
1994        )
1995        .build()
1996        .unwrap();
1997
1998        // relaychain
1999        assert_eq!(network_config.relaychain().chain().as_str(), "rococo-local");
2000        assert_eq!(network_config.relaychain().nodes().len(), 2);
2001        let mut node_names = network_config.relaychain().nodes().into_iter();
2002        let node1 = node_names.next().unwrap().name();
2003        assert_eq!(node1, "alice");
2004        let node2 = node_names.next().unwrap().name();
2005        assert_eq!(node2, "bob");
2006
2007        // parachains
2008        assert_eq!(network_config.parachains().len(), 1);
2009        let &parachain1 = network_config.parachains().first().unwrap();
2010        assert_eq!(parachain1.id(), 100);
2011        assert_eq!(parachain1.collators().len(), 2);
2012        let mut collator_names = parachain1.collators().into_iter();
2013        let collator1 = collator_names.next().unwrap().name();
2014        assert_eq!(collator1, "collator1");
2015        let collator2 = collator_names.next().unwrap().name();
2016        assert_eq!(collator2, "collator2");
2017
2018        assert_eq!(parachain1.initial_balance(), 2_000_000_000_000);
2019    }
2020
2021    #[test]
2022    fn with_parachain_id_and_collators_should_fail_with_empty_collator_list() {
2023        let errors =
2024            NetworkConfigBuilder::with_chain_and_nodes("polkadot", vec!["alice".to_string()])
2025                .with_parachain_id_and_collators(1, vec![])
2026                .build()
2027                .unwrap_err();
2028
2029        assert_eq!(
2030            errors.first().unwrap().to_string(),
2031            "parachain[1].can't be empty"
2032        );
2033    }
2034
2035    #[test]
2036    fn with_parachain_id_and_collators_should_fail_with_empty_collator_name() {
2037        let errors =
2038            NetworkConfigBuilder::with_chain_and_nodes("polkadot", vec!["alice".to_string()])
2039                .with_parachain_id_and_collators(1, vec!["collator1".to_string(), "".to_string()])
2040                .build()
2041                .unwrap_err();
2042
2043        assert_eq!(
2044            errors.first().unwrap().to_string(),
2045            "parachain[1].collators[''].name: can't be empty"
2046        );
2047    }
2048
2049    #[test]
2050    fn wasm_override_in_toml_should_work() {
2051        let load_from_toml = NetworkConfig::load_from_toml(
2052            "./testing/snapshots/0005-small-networl-with-wasm-override.toml",
2053        )
2054        .unwrap();
2055
2056        let expected = NetworkConfigBuilder::new()
2057            .with_relaychain(|relaychain| {
2058                relaychain
2059                    .with_chain("rococo-local")
2060                    .with_default_command("polkadot")
2061                    .with_wasm_override("/some/path/runtime.wasm")
2062                    .with_validator(|node| node.with_name("alice"))
2063                    .with_validator(|node| node.with_name("bob"))
2064            })
2065            .with_parachain(|p| {
2066                p.with_id(1000)
2067                    .with_wasm_override("https://some.com/runtime.wasm")
2068                    .with_collator(|c| c.with_name("john"))
2069            })
2070            .build()
2071            .unwrap();
2072
2073        assert_eq!(
2074            load_from_toml.relaychain().wasm_override(),
2075            expected.relaychain().wasm_override()
2076        );
2077        assert_eq!(
2078            load_from_toml.parachains()[0].wasm_override(),
2079            expected.parachains()[0].wasm_override()
2080        );
2081    }
2082
2083    #[test]
2084    fn multiple_paras_with_same_id_should_work() {
2085        let network_config = NetworkConfigBuilder::new()
2086            .with_relaychain(|relaychain| {
2087                relaychain
2088                    .with_chain("polkadot")
2089                    .with_fullnode(|node| node.with_name("node").with_command("command"))
2090                    .with_validator(|n| n.with_name("v"))
2091                    .with_validator(|n| n.with_name("v1"))
2092            })
2093            .with_parachain(|parachain| {
2094                parachain
2095                    .with_id(1)
2096                    .with_chain("myparachain1")
2097                    .with_collator(|collator| {
2098                        collator.with_name("collator1").with_command("command1")
2099                    })
2100            })
2101            .with_parachain(|parachain| {
2102                parachain
2103                    .with_id(1)
2104                    .with_chain("myparachain1")
2105                    .with_registration_strategy(RegistrationStrategy::Manual)
2106                    .with_collator(|collator| {
2107                        collator.with_name("collator2").with_command("command1")
2108                    })
2109            })
2110            .build()
2111            .unwrap();
2112
2113        let &parachain2 = network_config.parachains().last().unwrap();
2114        assert_eq!(parachain2.unique_id(), "1-1");
2115    }
2116
2117    #[test]
2118    fn multiple_paras_with_same_id_both_for_register_should_fail() {
2119        let errors = NetworkConfigBuilder::new()
2120            .with_relaychain(|relaychain| {
2121                relaychain
2122                    .with_chain("polkadot")
2123                    .with_fullnode(|node| node.with_name("node").with_command("command"))
2124            })
2125            .with_parachain(|parachain| {
2126                parachain
2127                    .with_id(1)
2128                    .with_chain("myparachain1")
2129                    .with_collator(|collator| {
2130                        collator.with_name("collator1").with_command("command1")
2131                    })
2132            })
2133            .with_parachain(|parachain| {
2134                parachain
2135                    .with_id(1)
2136                    .with_chain("myparachain1")
2137                    // .with_registration_strategy(RegistrationStrategy::UsingExtrinsic)
2138                    .with_collator(|collator| {
2139                        collator.with_name("collator2").with_command("command1")
2140                    })
2141            })
2142            .build()
2143            .unwrap_err();
2144
2145        assert_eq!(
2146            errors.first().unwrap().to_string(),
2147            "ParaId 1 already set to be registered, only one should be."
2148        );
2149    }
2150
2151    #[test]
2152    fn network_config_should_work_from_toml_without_chain_name() {
2153        let loaded_from_toml =
2154            NetworkConfig::load_from_toml("./testing/snapshots/0006-without-rc-chain-name.toml")
2155                .unwrap();
2156
2157        assert_eq!(
2158            "rococo-local",
2159            loaded_from_toml.relaychain().chain().as_str()
2160        );
2161    }
2162
2163    #[test]
2164    fn network_config_should_work_from_toml_with_duplicate_name_between_collator_and_relay_node() {
2165        let loaded_from_toml = NetworkConfig::load_from_toml(
2166            "./testing/snapshots/0007-small-network_w_parachain_w_duplicate_node_names.toml",
2167        )
2168        .unwrap();
2169
2170        assert_eq!(
2171            loaded_from_toml
2172                .relaychain()
2173                .nodes()
2174                .iter()
2175                .filter(|n| n.name() == "alice")
2176                .count(),
2177            1
2178        );
2179        assert_eq!(
2180            loaded_from_toml
2181                .parachains()
2182                .iter()
2183                .flat_map(|para| para.collators())
2184                .filter(|n| n.name() == "alice-1")
2185                .count(),
2186            1
2187        );
2188    }
2189
2190    #[test]
2191    fn raw_spec_override_in_toml_should_work() {
2192        let load_from_toml = NetworkConfig::load_from_toml(
2193            "./testing/snapshots/0008-small-network-with-raw-spec-override.toml",
2194        )
2195        .unwrap();
2196
2197        let expected = NetworkConfigBuilder::new()
2198            .with_relaychain(|relaychain| {
2199                relaychain
2200                    .with_chain("rococo-local")
2201                    .with_default_command("polkadot")
2202                    .with_raw_spec_override("/some/path/raw_spec_override.json")
2203                    .with_validator(|node| node.with_name("alice"))
2204                    .with_validator(|node| node.with_name("bob"))
2205            })
2206            .with_parachain(|p| {
2207                p.with_id(1000)
2208                    .with_raw_spec_override("https://some.com/raw_spec_override.json")
2209                    .with_collator(|c| c.with_name("john"))
2210            })
2211            .build()
2212            .unwrap();
2213
2214        assert_eq!(
2215            load_from_toml.relaychain().raw_spec_override(),
2216            expected.relaychain().raw_spec_override()
2217        );
2218        assert_eq!(
2219            load_from_toml.parachains()[0].raw_spec_override(),
2220            expected.parachains()[0].raw_spec_override()
2221        );
2222    }
2223
2224    const TOML_WITH_RELATIVE_PATHS: &str = r#"
2225[relaychain]
2226chain = "rococo-local"
2227default_command = "./bin/polkadot"
2228chain_spec_path = "./rc.json"
2229
2230[[relaychain.nodes]]
2231name = "alice"
2232validator = true
2233command = "polkadot"
2234"#;
2235
2236    #[test]
2237    fn toml_string_should_resolve_relative_paths_against_the_given_base_dir() {
2238        let config = NetworkConfig::load_from_toml_string_with_base_dir(
2239            TOML_WITH_RELATIVE_PATHS,
2240            Path::new("/tmp/some-base-dir"),
2241        )
2242        .unwrap();
2243
2244        assert_eq!(
2245            config.relaychain().default_command().unwrap().as_str(),
2246            "/tmp/some-base-dir/bin/polkadot"
2247        );
2248        assert_eq!(
2249            config.relaychain().chain_spec_path().unwrap().to_string(),
2250            "/tmp/some-base-dir/rc.json"
2251        );
2252        // non-relative values are left untouched
2253        assert_eq!(
2254            config.relaychain().nodes()[0].command().unwrap().as_str(),
2255            "polkadot"
2256        );
2257    }
2258
2259    #[test]
2260    fn toml_string_should_resolve_relative_paths_against_the_cwd_by_default() {
2261        let from_cwd = NetworkConfig::load_from_toml_string(TOML_WITH_RELATIVE_PATHS).unwrap();
2262        let expected = NetworkConfig::load_from_toml_string_with_base_dir(
2263            TOML_WITH_RELATIVE_PATHS,
2264            &std::env::current_dir().unwrap(),
2265        )
2266        .unwrap();
2267
2268        assert_eq!(from_cwd, expected);
2269    }
2270
2271    #[test]
2272    fn toml_string_and_toml_file_should_produce_the_same_config() {
2273        let path = "./testing/snapshots/0006-without-rc-chain-name.toml";
2274        let from_file = NetworkConfig::load_from_toml(path).unwrap();
2275
2276        let file_path = PathBuf::from(path).canonicalize().unwrap();
2277        let base_dir = file_path.parent().unwrap();
2278        let from_string = NetworkConfig::load_from_toml_string_with_base_dir(
2279            &fs::read_to_string(path).unwrap(),
2280            base_dir,
2281        )
2282        .unwrap();
2283
2284        assert_eq!(from_file, from_string);
2285
2286        // and the relative paths were actually rewritten, not just rewritten identically
2287        assert_eq!(
2288            from_string
2289                .relaychain()
2290                .chain_spec_path()
2291                .unwrap()
2292                .to_string(),
2293            base_dir.join("rc.json").to_string_lossy()
2294        );
2295        assert_eq!(
2296            from_string.parachains()[0]
2297                .chain_spec_path()
2298                .unwrap()
2299                .to_string(),
2300            base_dir.join("parachain.json").to_string_lossy()
2301        );
2302    }
2303
2304    #[test]
2305    fn with_jamchain() {
2306        let network_config = NetworkConfigBuilder::new()
2307            .with_relaychain(|relaychain| {
2308                relaychain
2309                    .with_chain("westend")
2310                    .with_default_command("polkadot")
2311                    .with_default_image("docker.io/parity/polkadot:stable2603-3")
2312                    .with_validator(|n| n.with_name("validator-0"))
2313                    .with_validator(|n| n.with_name("validator-1"))
2314            })
2315            .with_parachain(|parachain| {
2316                parachain
2317                    .with_id(1004)
2318                    .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")
2319                    .with_collator_group(|g| {
2320                        g.with_count(100)
2321                        .with_base_node(|n|{
2322                            n.with_name("collator")
2323                            .with_command("polkadot-parachain")
2324                            .with_args(vec!["--enable-statement-store".into()])
2325                            .with_image("docker.io/parity/polkadot-parachain:stable2603-3")
2326                        })
2327                })
2328            })
2329            .with_jamchain(|jamchain| {
2330                jamchain
2331                .with_id("dev")
2332                .with_validator(|jamnode|{
2333                    jamnode.with_name("jam1")
2334                })
2335            })
2336            .build()
2337            .unwrap();
2338
2339        let toml_string = network_config.dump_to_toml().unwrap();
2340        println!("{}", toml_string);
2341    }
2342
2343    /// A network file is the other way to describe a jamchain, and the override is plain data,
2344    /// so a `[jamchain.genesis_overrides]` table has to come back as the same JSON the builder
2345    /// would have been given — and survive being dumped and loaded again.
2346    #[test]
2347    fn jamchain_genesis_overrides_round_trip_through_toml() {
2348        let toml = r#"
2349[relaychain]
2350chain = "rococo-local"
2351default_command = "polkadot"
2352
2353[[relaychain.nodes]]
2354name = "alice"
2355
2356[jamchain]
2357id = "dev"
2358
2359[jamchain.genesis_overrides]
2360auth_queues = { "0" = "2bbda8cb" }
2361assigners = { "0" = 5 }
2362
2363[[jamchain.genesis_overrides.services]]
2364id = 5
2365code = "/blobs/parasim-service.jam"
2366preimages = ["/blobs/parachain-authorizer-sr25519.jam"]
2367
2368[[jamchain.nodes]]
2369name = "jam0"
2370mode = "validator"
2371"#;
2372        let expected = serde_json::json!({
2373            "auth_queues": { "0": "2bbda8cb" },
2374            "assigners": { "0": 5 },
2375            "services": [{
2376                "id": 5,
2377                "code": "/blobs/parasim-service.jam",
2378                "preimages": ["/blobs/parachain-authorizer-sr25519.jam"],
2379            }],
2380        });
2381
2382        let loaded = NetworkConfig::load_from_toml_string(toml).unwrap();
2383        let jamchain = loaded.jamchain().expect("the file has a jamchain");
2384        assert_eq!(jamchain.genesis_overrides(), Some(&expected));
2385
2386        let reloaded =
2387            NetworkConfig::load_from_toml_string(&loaded.dump_to_toml().unwrap()).unwrap();
2388        assert_eq!(reloaded.jamchain(), Some(jamchain));
2389    }
2390
2391    #[test]
2392    fn genesis_patch_round_trips_through_toml() {
2393        let toml = r#"
2394[relaychain]
2395chain = "rococo-local"
2396default_command = "polkadot"
2397
2398[[relaychain.genesis_patch]]
2399op = "add"
2400path = "/balances/balances/-"
2401value = ["5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", 1000000000000]
2402
2403[[relaychain.genesis_patch]]
2404op = "replace"
2405path = "/staking/minimumValidatorCount"
2406value = 7
2407
2408[[relaychain.nodes]]
2409name = "alice"
2410
2411[[parachains]]
2412id = 100
2413default_command = "polkadot-parachain"
2414
2415[[parachains.genesis_patch]]
2416op = "remove"
2417path = "/sudo"
2418
2419[[parachains.collators]]
2420name = "collator"
2421"#;
2422        let loaded = NetworkConfig::load_from_toml_string(toml).unwrap();
2423        let relay_patch = loaded.relaychain().genesis_patch().unwrap();
2424        assert_eq!(relay_patch.0.len(), 2);
2425        assert_eq!(
2426            serde_json::to_value(&relay_patch.0[1]).unwrap(),
2427            serde_json::json!({"op": "replace", "path": "/staking/minimumValidatorCount", "value": 7})
2428        );
2429        let para_patch = loaded.parachains()[0].genesis_patch().unwrap();
2430        assert_eq!(
2431            serde_json::to_value(para_patch).unwrap(),
2432            serde_json::json!([{"op": "remove", "path": "/sudo"}])
2433        );
2434
2435        let reloaded =
2436            NetworkConfig::load_from_toml_string(&loaded.dump_to_toml().unwrap()).unwrap();
2437        assert_eq!(reloaded.relaychain().genesis_patch(), Some(relay_patch));
2438        assert_eq!(reloaded.parachains()[0].genesis_patch(), Some(para_patch));
2439    }
2440
2441    #[test]
2442    fn jamchain_toml_without_relaychain_works() {
2443        let config = NetworkConfig::load_from_toml_string(
2444            r#"
2445[jamchain]
2446id = "dev"
2447default_command = "polkajam"
2448
2449[[jamchain.nodes]]
2450name = "jam0"
2451mode = "validator"
2452
2453[[jamchain.nodes]]
2454name = "jam-or"
2455mode = "ordinary"
2456"#,
2457        )
2458        .unwrap();
2459
2460        assert!(config.try_relaychain().is_none());
2461        let jamchain = config.jamchain().unwrap();
2462        assert_eq!(jamchain.id().as_str(), "dev");
2463        assert_eq!(jamchain.nodes().len(), 2);
2464        assert_eq!(jamchain.nodes()[1].mode(), &JamNodeMode::Ordinary);
2465    }
2466
2467    #[test]
2468    fn jamchain_toml_with_parachains_works() {
2469        let config = NetworkConfig::load_from_toml_string(
2470            r#"
2471[jamchain]
2472id = "dev"
2473default_command = "polkajam"
2474
2475[[jamchain.nodes]]
2476name = "jam-or"
2477mode = "ordinary"
2478
2479[[parachains]]
2480id = 1000
2481default_command = "polkadot-omni-node"
2482default_args = ["--jam-rpc-url http://{{ZOMBIE:jam-or:rpc_uri}}"]
2483
2484[[parachains.collators]]
2485name = "collator"
2486
2487[[parachains.collators]]
2488name = "collator"
2489"#,
2490        )
2491        .unwrap();
2492
2493        assert!(config.try_relaychain().is_none());
2494        assert!(config.jamchain().is_some());
2495
2496        let para = config.parachains()[0];
2497        assert_eq!(para.id(), 1000);
2498
2499        // the parachain defaults are applied to the collators, and their names deduplicated
2500        let collators = para.collators();
2501        assert_eq!(collators.len(), 2);
2502        assert_eq!(collators[0].name(), "collator");
2503        assert_eq!(collators[1].name(), "collator-1");
2504        for collator in collators {
2505            assert_eq!(collator.command().unwrap().as_str(), "polkadot-omni-node");
2506            assert_eq!(
2507                collator.args(),
2508                vec![&Arg::Option(
2509                    "--jam-rpc-url".into(),
2510                    "http://{{ZOMBIE:jam-or:rpc_uri}}".into()
2511                )]
2512            );
2513        }
2514    }
2515
2516    #[test]
2517    fn toml_without_relaychain_nor_jamchain_fails() {
2518        let err = NetworkConfig::load_from_toml_string(
2519            r#"
2520[[parachains]]
2521id = 1000
2522"#,
2523        )
2524        .unwrap_err();
2525
2526        assert_eq!(
2527            err.to_string(),
2528            "Relay chain or Jam definition does not exist."
2529        );
2530    }
2531
2532    #[test]
2533    fn with_tiny_jamchain() {
2534        let network_config = NetworkConfigBuilder::new()
2535            .with_relaychain(|relaychain| {
2536                relaychain
2537                    .with_chain("westend")
2538                    .with_default_command("polkadot")
2539                    .with_default_image("docker.io/parity/polkadot:stable2603-3")
2540                    .with_validator(|n| n.with_name("validator-0"))
2541                    .with_validator(|n| n.with_name("validator-1"))
2542            })
2543            .with_tiny_jamchain()
2544            .build()
2545            .unwrap();
2546
2547        let toml_string = network_config.dump_to_toml().unwrap();
2548        println!("{}", toml_string);
2549    }
2550    #[test]
2551    fn demo_denis() {
2552        let network_config = NetworkConfigBuilder::new()
2553            .with_relaychain(|relaychain| {
2554                relaychain
2555                    .with_chain("westend")
2556                    .with_default_command("polkadot")
2557                    .with_default_image("docker.io/parity/polkadot:stable2603-3")
2558                    .with_validator(|n| n.with_name("validator-0"))
2559                    .with_validator(|n| n.with_name("validator-1"))
2560            })
2561            .with_parachain(|parachain| {
2562                parachain
2563                    .with_id(1004)
2564                    .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")
2565                    .with_collator_group(|g| {
2566                        g.with_count(100)
2567                        .with_base_node(|n|{
2568                            n.with_name("collator")
2569                            .with_command("polkadot-parachain")
2570                            .with_args(vec!["--enable-statement-store".into()])
2571                            .with_image("docker.io/parity/polkadot-parachain:stable2603-3")
2572                        })
2573                })
2574            })
2575            .build()
2576            .unwrap();
2577
2578        let toml_string = network_config.dump_to_toml().unwrap();
2579        println!("{}", toml_string);
2580    }
2581
2582    #[test]
2583    fn node_names_loaded_from_toml_are_made_valid_for_every_provider() {
2584        // zombie-bite writes `Collator-<id>`, which no kubernetes pod can be named.
2585        let network_config = NetworkConfig::load_from_toml_string(
2586            r#"
2587            [relaychain]
2588            chain = "rococo-local"
2589            default_command = "polkadot"
2590
2591            [[relaychain.nodes]]
2592            name = "Alice"
2593
2594            [[relaychain.nodes]]
2595            name = "my_node"
2596
2597            [[parachains]]
2598            id = 1000
2599            chain = "asset-hub-rococo"
2600            default_command = "polkadot-parachain"
2601
2602            [[parachains.collators]]
2603            name = "Collator-1000"
2604            "#,
2605        )
2606        .unwrap();
2607
2608        let validators: Vec<&str> = network_config
2609            .relaychain()
2610            .nodes()
2611            .iter()
2612            .map(|node| node.name())
2613            .collect();
2614        assert_eq!(validators, ["alice", "my-node"]);
2615        assert_eq!(
2616            network_config.parachains()[0].collators()[0].name(),
2617            "collator-1000"
2618        );
2619    }
2620
2621    /// A one-node relaychain with the given `[[custom_processes]]` entries.
2622    fn load_with_processes(processes: &str) -> Result<NetworkConfig, anyhow::Error> {
2623        NetworkConfig::load_from_toml_string(&format!(
2624            r#"
2625            [relaychain]
2626            chain = "rococo-local"
2627            default_command = "polkadot"
2628
2629            [[relaychain.nodes]]
2630            name = "alice"
2631
2632            [[custom_processes]]
2633            {processes}
2634            "#
2635        ))
2636    }
2637
2638    #[test]
2639    fn custom_process_lifecycle_loads_from_toml_and_is_validated() {
2640        let config = load_with_processes(
2641            r#"
2642            name = "init"
2643            command = "sh"
2644            one_shot = true
2645            timeout = 30
2646
2647            [[custom_processes]]
2648            name = "api"
2649            command = "api"
2650            ports = [{ name = "http", port = 8080 }]
2651            ready_check = { http = { port = "http", path = "/health" }, interval = 2 }
2652            timeout = 120
2653            depends_on = ["init", { name = "cache", condition = "service_started" }]
2654
2655            [[custom_processes]]
2656            name = "cache"
2657            command = "cache"
2658            "#,
2659        )
2660        .unwrap();
2661        let processes = config.custom_processes();
2662        let api = processes[1];
2663        assert_eq!(api.timeout(), Some(120));
2664        assert_eq!(api.ready_check().unwrap().interval, 2);
2665        assert_eq!(api.depends_on().len(), 2);
2666        assert_eq!(
2667            api.depends_on()[1].condition,
2668            Some(DependencyCondition::ServiceStarted)
2669        );
2670        assert!(processes[0].one_shot());
2671        let dumped = config.dump_to_toml().unwrap();
2672        let reloaded = NetworkConfig::load_from_toml_string(&dumped).unwrap();
2673        assert_eq!(reloaded.custom_processes(), config.custom_processes());
2674
2675        // the cross-process rules apply on this path too
2676        let err = load_with_processes(
2677            r#"
2678            name = "api"
2679            command = "api"
2680            ports = [{ name = "http", port = 8080 }]
2681            ready_check = { http = { port = "http", path = "health" } }
2682            depends_on = [{ name = "cache", condition = "service_healthy" }]
2683
2684            [[custom_processes]]
2685            name = "cache"
2686            command = "cache"
2687            depends_on = ["api"]
2688            "#,
2689        )
2690        .unwrap_err()
2691        .to_string();
2692        assert!(
2693            err.contains("http path 'health' must start with '/'"),
2694            "{err}"
2695        );
2696        assert!(err.contains("'cache' cannot be service_healthy"), "{err}");
2697        assert!(err.contains("cycle among api, cache"), "{err}");
2698    }
2699
2700    #[test]
2701    fn custom_process_ports_and_resources_load_from_toml_and_dump_back() {
2702        let network_config = load_with_processes(
2703            r#"
2704            name = "ipfs"
2705            command = "ipfs"
2706            image = "docker.io/ipfs/kubo:v0.39.0"
2707            args = ["daemon"]
2708            ports = [{ name = "api", port = 5001 }, { name = "gateway", port = 8080 }]
2709
2710            [custom_processes.resources]
2711            limits = { cpu = "500m", memory = "512Mi" }
2712
2713            [[custom_processes]]
2714            name = "eth-rpc"
2715            command = "eth-rpc"
2716            "#,
2717        )
2718        .unwrap();
2719
2720        let processes = network_config.custom_processes();
2721        assert_eq!(processes.len(), 2);
2722        let ipfs = processes[0];
2723        assert_eq!(
2724            ipfs.ports()
2725                .iter()
2726                .map(|p| (p.name.as_str(), p.port))
2727                .collect::<Vec<_>>(),
2728            [("api", 5001), ("gateway", 8080)]
2729        );
2730        assert_eq!(
2731            ipfs.resources().unwrap().limit_cpu().unwrap().as_str(),
2732            "500m"
2733        );
2734        assert!(processes[1].ports().is_empty());
2735        assert!(processes[1].resources().is_none());
2736
2737        // What is dumped loads back to the same config: ports and resources included.
2738        let dumped = network_config.dump_to_toml().unwrap();
2739        assert!(dumped.contains("[[custom_processes.ports]]"));
2740        let reloaded = NetworkConfig::load_from_toml_string(&dumped).unwrap();
2741        assert_eq!(
2742            reloaded.custom_processes(),
2743            network_config.custom_processes()
2744        );
2745    }
2746
2747    #[test]
2748    fn custom_processes_loaded_from_toml_are_validated() {
2749        // A port name k8s would reject, caught before anything is spawned.
2750        let err = load_with_processes(
2751            r#"
2752            name = "eth-rpc"
2753            command = "eth-rpc"
2754            ports = [{ name = "json_rpc", port = 8545 }]
2755            "#,
2756        )
2757        .unwrap_err();
2758        assert!(
2759            err.to_string()
2760                .contains("custom_processes['eth-rpc'].ports: port name 'json_rpc'"),
2761            "{err}"
2762        );
2763
2764        // A process may not take a node's name: they share one registry.
2765        let err = load_with_processes(
2766            r#"
2767            name = "alice"
2768            command = "eth-rpc"
2769            "#,
2770        )
2771        .unwrap_err();
2772        assert!(
2773            err.to_string()
2774                .contains("custom_processes['alice'].name: already used by a node"),
2775            "{err}"
2776        );
2777    }
2778
2779    #[test]
2780    fn a_custom_process_name_is_rejected_where_a_node_name_would_be_corrected() {
2781        let err = load_with_processes(
2782            r#"
2783            name = "eth_rpc"
2784            command = "eth-rpc"
2785            "#,
2786        )
2787        .unwrap_err();
2788        assert!(
2789            err.to_string()
2790                .contains("custom_processes['eth_rpc'].name: 'eth_rpc' must be"),
2791            "{err}"
2792        );
2793        assert!(err.to_string().contains("(e.g. 'eth-rpc')"), "{err}");
2794
2795        let err = load_with_processes(
2796            r#"
2797            name = ""
2798            command = "eth-rpc"
2799            "#,
2800        )
2801        .unwrap_err();
2802        assert!(
2803            err.to_string()
2804                .contains("custom_processes[''].name: can't be empty"),
2805            "{err}"
2806        );
2807
2808        let errors = NetworkConfigBuilder::new()
2809            .with_relaychain(|r| {
2810                r.with_chain("rococo-local")
2811                    .with_default_command("polkadot")
2812                    .with_validator(|node| node.with_name("alice"))
2813            })
2814            .with_custom_process(|c| c.with_name("Eth-RPC").with_command("eth-rpc"))
2815            .build()
2816            .unwrap_err();
2817        assert!(
2818            errors
2819                .iter()
2820                .any(|e| e.to_string().contains("custom_processes['Eth-RPC'].name:")),
2821            "{errors:?}"
2822        );
2823    }
2824
2825    #[test]
2826    fn a_custom_process_may_not_take_a_node_name_in_the_builder() {
2827        let errors = NetworkConfigBuilder::new()
2828            .with_relaychain(|r| {
2829                r.with_chain("rococo-local")
2830                    .with_default_command("polkadot")
2831                    .with_validator(|node| node.with_name("alice"))
2832            })
2833            .with_custom_process(|c| c.with_name("alice").with_command("eth-rpc"))
2834            .build()
2835            .unwrap_err();
2836        assert!(
2837            errors.iter().any(|e| e
2838                .to_string()
2839                .contains("already used by a node or custom process")),
2840            "{errors:?}"
2841        );
2842    }
2843}